binrep-0.3.0: src/Binrep/Get.hs
{-# LANGUAGE FunctionalDependencies #-}
module Binrep.Get
( Getter, Get(..), runGet, runGetter
, E(..), EBase(..), EGeneric(..), EGenericSum(..)
, eBase
, getEWrap, getEBase
, cutEBase
, GetWith(..), runGetWith
) where
import FlatParse.Basic qualified as FP
import Data.ByteString qualified as B
import GHC.Exts ( TYPE, type LiftedRep )
import Data.Word
import Data.Int
import Data.Void ( Void )
import GHC.Generics ( Generic )
import Data.Text ( Text )
import Binrep.BLen ( BLenT )
import Numeric.Natural
type Getter a = FP.Parser E a
data E
= EBase EBase
| EGeneric String {- ^ datatype name -} EGeneric
deriving stock (Eq, Show, Generic)
eBase :: EBase -> Getter a
eBase = FP.err . EBase
-- | TODO confirm correct operation (error combination)
getEWrap :: Get a => (E -> E) -> Getter a
getEWrap f = FP.cutting get (f $ EBase EFail) (\e _ -> f e)
getEBase :: Get a => EBase -> Getter a
getEBase = FP.cut get . EBase
cutEBase :: Getter a -> EBase -> Getter a
cutEBase f e = FP.cut f $ EBase e
data EBase
= ENoVoid
| EFail
| EEof
| EExpectedByte Word8 Word8
-- ^ expected first, got second
| EOverlong BLenT BLenT
-- ^ expected first, got second
| EExpected B.ByteString B.ByteString
-- ^ expected first, got second
| EFailNamed String
-- ^ known fail
| EFailParse String B.ByteString Word8
-- ^ parse fail (where you parse a larger object, then a smaller one in it)
| ERanOut Natural
-- ^ ran out of input, needed precisely @n@ bytes for this part (n > 0)
deriving stock (Eq, Show, Generic)
data EGeneric
= EGenericSum EGenericSum
| EGenericField String (Maybe String) Natural E
deriving stock (Eq, Show, Generic)
data EGenericSum
= EGenericSumTag E
| EGenericSumTagNoMatch [String] Text
deriving stock (Eq, Show, Generic)
class Get a where
-- | Parse from binary.
get :: Getter a
runGet :: Get a => B.ByteString -> Either E (a, B.ByteString)
runGet = runGetter get
runGetter :: Getter a -> B.ByteString -> Either E (a, B.ByteString)
runGetter g bs = case FP.runParser g bs of
FP.OK a bs' -> Right (a, bs')
FP.Fail -> Left $ EBase EFail
FP.Err e -> Left e
-- | Impossible to parse 'Void'.
instance Get Void where
get = eBase ENoVoid
-- | Parse heterogeneous lists in order. No length indicator, so either fails or
-- succeeds by reaching EOF. Probably not what you usually want, but sometimes
-- used at the "top" of binary formats.
instance Get a => Get [a] where
get = do as <- FP.many get
cutEBase FP.eof EEof
return as
instance (Get a, Get b) => Get (a, b) where
get = do
a <- get
b <- get
return (a, b)
instance Get B.ByteString where
get = FP.takeRestBs
instance Get Word8 where get = cutEBase FP.anyWord8 (ERanOut 1)
instance Get Int8 where get = cutEBase FP.anyInt8 (ERanOut 1)
-- | A type that can be parsed from binary given some environment.
--
-- Making this levity polymorphic makes things pretty strange, but is useful.
-- See @Binrep.Example.FileTable@.
class GetWith (r :: TYPE rep) a | a -> r where
-- | Parse from binary with the given environment.
getWith :: r -> Getter a
-- can no longer provide default implementation due to levity polymorphism
--default getWith :: Get a => r -> Getter a
--getWith _ = get
--deriving anyclass instance Get a => GetWith r [a]
-- Note that @r@ is not levity polymorphic, GHC forces it to be lifted. You
-- can't bind (LHS) a levity polymorphic value.
runGetWith
:: GetWith (r :: TYPE LiftedRep) a
=> r -> B.ByteString -> Either E (a, B.ByteString)
runGetWith r bs = runGetter (getWith r) bs