packages feed

ron-0.13: lib/RON/Binary/Parse.hs

{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}

-- | Binary parser elements
module RON.Binary.Parse (
    parse,
    parseAtom,
    parseString,
) where

import RON.Prelude

import Attoparsec.Extra (
    Parser,
    anyWord8,
    endOfInputEx,
    label,
    parseOnly,
    takeL,
    withInputSize,
 )
import Attoparsec.Extra qualified as Atto
import Data.Binary qualified as Binary
import Data.Binary.Get (getDoublebe, runGet)
import Data.Bits (shiftR, testBit, (.&.))
import Data.ByteString.Lazy (cons, toStrict)
import Data.ByteString.Lazy qualified as BSL
import Data.Text.Encoding (decodeUtf8)
import Data.ZigZag (zzDecode64)

import RON.Binary.Types (Desc (..), Size, descIsOp)
import RON.Types (
    Atom (AFloat, AInteger, AString, AUuid),
    ClosedOp (..),
    Op (..),
    OpTerm (TClosed, THeader, TQuery, TReduced),
    Payload,
    UUID (UUID),
    WireChunk (Closed, Query, Value),
    WireFrame,
    WireReducedChunk (..),
 )
import RON.Util.Word (safeCast)

-- | 'Parser' for descriptor
parseDesc :: Parser (Desc, Size)
parseDesc = label "desc" $ do
    b <- label "start byte" anyWord8
    let typeCode = b `shiftR` 4
    let sizeCode = b .&. 0b1111
    let desc = toEnum $ fromIntegral typeCode
    size <- case (sizeCode, desc) of
        (0, DAtomString) -> extendedLength
        (0, d) | descIsOp d -> pure 0
        (0, _) -> pure 16
        _ -> pure $ fromIntegral sizeCode
    pure (desc, size)

-- | 'Parser' for extended length field
extendedLength :: Parser Size
extendedLength = do
    b <- anyWord8
    if testBit b 7 then do
        bbb <- takeL 3
        pure $ leastSignificant31 $ Binary.decode (b `cons` bbb)
    else
        pure $ safeCast b

-- | Parse frame
parse :: ByteStringL -> Either String WireFrame
parse = parseOnly $ parseFrame <* endOfInputEx

-- | 'Parser' for frame
parseFrame :: Parser WireFrame
parseFrame = label "WireFrame" $ do
    _ <-
        Atto.string "RON2" <|> do
            magic <- takeL 4
            fail $ "unsupported magic sequence " ++ show magic
    parseChunks

-- | 'Parser' for chunk sequence
parseChunks :: Parser [WireChunk]
parseChunks = do
    size :: Size <- Binary.decode <$> takeL 4
    if
        | testBit size 31 ->
            liftA2 (:) (parseChunk $ leastSignificant31 size) parseChunks
        | size > 0 ->
            (: []) <$> parseChunk size
        | True ->
            pure []

-- | Clear upper bit of 'Word32'
leastSignificant31 :: Word32 -> Word32
leastSignificant31 x = x .&. 0x7FFFFFFF

-- | 'Parser' for a chunk
parseChunk ::
    -- | expected input length
    Size ->
    Parser WireChunk
parseChunk size = label "WireChunk" $ do
    (consumed0, (term, op)) <- withInputSize parseDescAndClosedOp
    let parseReducedChunk wrcHeader isQuery = do
            wrcBody <- parseReducedOps $ fromIntegral size - consumed0
            pure $ (if isQuery then Query else Value) WireReducedChunk{..}
    case term of
        THeader -> parseReducedChunk op False
        TQuery -> parseReducedChunk op True
        TReduced -> fail "reduced op without a chunk"
        TClosed -> assertSize size consumed0 $> Closed op

-- | Assert that is such as expected
assertSize :: (MonadFail f) => Size -> Int -> f ()
assertSize expected consumed =
    when (consumed /= fromIntegral expected) $
        fail $
            "size mismatch: expected "
                ++ show expected
                ++ ", got "
                ++ show consumed

-- | 'Parser' for a sequence of reduced ops
parseReducedOps :: Int -> Parser [Op]
parseReducedOps = label "[Op]" . go
  where
    go = \case
        0 -> pure []
        expected -> do
            (consumed, (TReduced, op)) <- withInputSize parseDescAndReducedOp
            case compare consumed expected of
                LT -> (op :) <$> go (expected - consumed)
                EQ -> pure [op]
                GT -> fail "impossible"

-- | 'Parser' for closed op, returning the op's terminator along with the op
parseDescAndClosedOp :: Parser (OpTerm, ClosedOp)
parseDescAndClosedOp = label "d+ClosedOp" $ do
    (desc, size) <- parseDesc
    unless (size == 0) $
        fail $
            "desc = " ++ show desc ++ ", size = " ++ show size
    case desc of
        DOpClosed -> (TClosed,) <$> parseClosedOp
        DOpHeader -> (THeader,) <$> parseClosedOp
        DOpQueryHeader -> (TQuery,) <$> parseClosedOp
        _ -> fail $ "unimplemented " ++ show desc

-- | 'Parser' for reduced op, returning the op's terminator along with the op
parseDescAndReducedOp :: Parser (OpTerm, Op)
parseDescAndReducedOp = label "d+ClosedOp" $ do
    (desc, size) <- parseDesc
    unless (size == 0) $
        fail $
            "desc = " ++ show desc ++ ", size = " ++ show size
    case desc of
        DOpReduced -> (TReduced,) <$> parseOpenOp
        _ -> fail $ "unimplemented " ++ show desc

-- | 'Parser' for closed op without terminator
parseClosedOp :: Parser ClosedOp
parseClosedOp = label "ClosedOp" $ do
    reducerId <- parseOpKey DUuidReducer
    objectId <- parseOpKey DUuidObject
    op <- parseOpenOp
    pure ClosedOp{..}

-- | 'Parser' for reduced op without terminator
parseOpenOp :: Parser Op
parseOpenOp = label "Op" $ do
    opId <- parseOpKey DUuidOp
    refId <- parseOpKey DUuidRef
    payload <- parsePayload
    pure Op{..}

-- | 'Parser' for an op key (type, object, event, or reference)
parseOpKey :: Desc -> Parser UUID
parseOpKey expectedType = label "OpKey" $ do
    (desc, size) <- parseDesc
    let go = do
            guard $ desc == expectedType
            uuid size
    case desc of
        DUuidReducer -> go
        DUuidObject -> go
        DUuidOp -> go
        DUuidRef -> go
        _ -> fail $ show desc

-- | 'Parser' for UUID
uuid ::
    -- | expected input length
    Size ->
    Parser UUID
uuid size = label "UUID" $
    case size of
        16 -> do
            x <- Binary.decode <$> takeL 8
            y <- Binary.decode <$> takeL 8
            pure $ UUID x y
        _ -> fail "expected uuid of size 16"

-- | 'Parser' for a payload (sequence of atoms)
parsePayload :: Parser Payload
parsePayload = label "payload" $ many atom

-- | 'Parser' for an atom
atom :: Parser Atom
atom = label "Atom" $ do
    (desc, size) <- parseDesc
    case desc of
        DAtomFloat -> AFloat <$> float size
        DAtomInteger -> AInteger <$> integer size
        DAtomString -> AString <$> string size
        DAtomUuid -> AUuid <$> uuid size
        _ -> fail "expected Atom"

-- | Parse an 'Atom'
parseAtom :: ByteStringL -> Either String Atom
parseAtom = parseOnly $ atom <* endOfInputEx

-- | 'Parser' for a float atom
float ::
    -- | expected input length
    Size ->
    Parser Double
float = \case
    8 -> runGet getDoublebe <$> takeL 8
    _ -> undefined

-- | 'Parser' for an integer atom
integer ::
    -- | expected input length
    Size ->
    Parser Int64
integer size = label "Integer" $ do
    -- big-endian, zigzag-coded, lengths 1..8
    unless (size >= 1 && size <= 8) $ fail "integer size must be 1..8"
    unless (size == 8) $ fail "integer size /=8 not implemented"
    zzDecode64 . Binary.decode <$> takeL (fromIntegral size)

-- | 'Parser' for an string
string ::
    -- | expected input length
    Size ->
    Parser Text
string size = decodeUtf8 . toStrict <$> takeL (fromIntegral size)

-- | Parse a string atom
parseString :: ByteStringL -> Either String Text
parseString bs =
    parseOnly (string (fromIntegral $ BSL.length bs) <* endOfInputEx) bs