bytepatch-0.3.1: src/StreamPatch/Patch/Binary.hs
{-# LANGUAGE RecordWildCards #-}
-- TODO rewrite patch/check bits (some overlap)
module StreamPatch.Patch.Binary
( Meta(..)
, MetaStream(..)
, Cfg(..)
, Error(..)
, patchBinRep
, BinRep(..)
, toBinRep'
, check
) where
import StreamPatch.Patch
import GHC.Generics ( Generic )
import GHC.Natural
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as Text
import qualified Data.Text as Text
import Data.Text ( Text )
import Data.Either.Combinators
import Data.Vinyl
import Data.Functor.Const
import Data.Vinyl.TypeLevel
data Meta = Meta
{ mMaxBytes :: Maybe (SeekRep 'FwdSeek)
-- ^ Maximum number of bytes permitted to write at the associated position.
} deriving (Eq, Show, Generic)
data MetaStream a = MetaStream
{ msNullTerminates :: Maybe (SeekRep 'FwdSeek)
-- ^ Stream segment should be null bytes (0x00) only from this index onwards.
, msExpected :: Maybe a
-- ^ Stream segment should be this.
} deriving (Eq, Show, Generic, Functor, Foldable, Traversable)
data Cfg = Cfg
{ cfgAllowPartialExpected :: Bool
-- ^ If enabled, allow partial expected bytes checking. If disabled, then even
-- if the expected bytes are a prefix of the actual, fail.
} deriving (Eq, Show, Generic)
data Error a
= ErrorBadBinRep a String
| ErrorUnexpectedNonNull BS.ByteString
| ErrorDidNotMatchExpected BS.ByteString BS.ByteString
| ErrorBinRepTooLong BS.ByteString Natural
deriving (Eq, Show, Generic, Functor, Foldable, Traversable)
patchBinRep
:: forall a s ss rs is i r
. ( BinRep a
, Traversable (FunctorRec rs)
, r ~ Const Meta
, rs ~ RDelete r ss
, RElem r ss i
, RSubset rs ss is )
=> Patch s ss a
-> Either (Error a) (Patch s rs BS.ByteString)
patchBinRep (Patch a s ms) = do
a' <- toBinRep' a
() <- case mMaxBytes m of
Nothing -> return ()
Just maxBytes -> if BS.length a' > fromIntegral maxBytes
then Left $ ErrorBinRepTooLong a' maxBytes
else return ()
let msDroppedMeta = FunctorRec $ rcast @rs $ getFunctorRec ms
ms' <- traverse toBinRep' msDroppedMeta
return $ Patch a' s ms'
where m = getConst @Meta $ getFlap $ rget $ getFunctorRec ms
-- | Type has a binary representation for using in patchscripts.
--
-- Patchscripts are parsed parameterized over the type to edit. That type needs
-- to become a bytestring for eventual patch application. We're forced into
-- newtypes and typeclasses by Aeson already, so this just enables us to define
-- some important patch generation behaviour in one place. Similarly to Aeson,
-- if you require custom behaviour for existing types (e.g. length-prefixed
-- strings instead of C-style null terminated), define a newtype over it.
--
-- Some values may not have valid patch representations, for example if you're
-- patching a 1-byte length-prefixed string and your string is too long (>255
-- encoded bytes). Thus, 'toPatchRep' is failable.
class BinRep a where
toBinRep :: a -> Either String BS.ByteString
toBinRep' :: BinRep a => a -> Either (Error a) BS.ByteString
toBinRep' a = mapLeft (ErrorBadBinRep a) $ toBinRep a
-- | Bytestrings are copied as-is.
instance BinRep BS.ByteString where
toBinRep = Right . id
-- | Text is converted to UTF-8 bytes and null-terminated.
instance BinRep Text where
toBinRep = Right . flip BS.snoc 0x00 . Text.encodeUtf8
-- | String is the same but goes the long way round, through Text.
instance BinRep String where
toBinRep = toBinRep . Text.pack
check :: BinRep a => Cfg -> BS.ByteString -> MetaStream a -> Either (Error a) ()
check cfg bs meta = do
case msExpected meta of
Nothing -> Right ()
Just aExpected -> do
bsExpected <- checkInner aExpected Nothing -- cheating a bit here
case msNullTerminates meta of
Nothing -> check' bs bsExpected
Just nullsFrom ->
let (bs', bsNulls) = BS.splitAt (fromIntegral nullsFrom) bs
in if bsNulls == BS.replicate (BS.length bsNulls) 0x00
then check' bs' bsExpected
else Left $ ErrorUnexpectedNonNull bs
where
check' bs' bsExpected =
case checkExpected bs' bsExpected of
True -> Right ()
False -> Left $ ErrorDidNotMatchExpected bs' bsExpected
checkInner a mn = do
bs' <- toBinRep' a
case mn of
Nothing -> Right bs'
Just n ->
if fromIntegral (BS.length bs') > n
then Left $ ErrorBinRepTooLong bs' n
else Right bs'
checkExpected bs' bsExpected =
case cfgAllowPartialExpected cfg of
True -> BS.isPrefixOf bs' bsExpected
False -> bs' == bsExpected