packages feed

bytepatch-0.4.0: src/StreamPatch/Apply.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module StreamPatch.Apply where

import GHC.Generics ( Generic )

import StreamPatch.Patch
import StreamPatch.Stream
import StreamPatch.HFunctorList
import StreamPatch.Patch.Binary qualified as Bin
import StreamPatch.Patch.Compare qualified as Compare
import StreamPatch.Patch.Compare ( Compare(..), compareTo )
import StreamPatch.Patch.Linearize.InPlace ( HasLength, getLength )

import Data.Vinyl
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as BB
import Data.ByteString.Lazy qualified as BL
import Control.Monad.State
import StreamPatch.Util ( traverseM_ )

import Control.Monad.Except

data Error
  = ErrorCompare String
  | ErrorBinUnexpectedNonNull BS.ByteString
    deriving (Generic, Eq, Show)

applyBinCompareFwd
    :: forall v m
    .  ( FwdInplaceStream m, Chunk m ~ BS.ByteString
       , Compare v BS.ByteString, Num (Index m) )
    => [Patch (Index m) '[Compare.Meta v, Bin.Meta] BS.ByteString]
    -> m (Either Error ())
applyBinCompareFwd = traverseM_ $ \(Patch bs s (HFunctorList (Flap cm :& Flap bm :& RNil))) -> runExceptT $ do
    -- advance to patch location
    lift $ advance s

    -- read same number of bytes as patch data
    bsStream  <- lift $ readahead $ fromIntegral $ getLength bs

    -- check for & strip expected terminating nulls
    bsStream' <- doNullTermCheck bsStream (Bin.mNullTerminates bm)

    -- compare with expected data
    doCompare bsStream' $ Compare.mCompare cm

    -- if that was all successful, write patch in-place
    lift $ overwrite bs
  where
    err = throwError
    doCompare bs' = \case
      Nothing   -> return ()
      Just cmp -> do
        case compareTo @v cmp bs' of
          Nothing -> return ()
          Just e -> err $ ErrorCompare e
    doNullTermCheck bs' = \case
      Nothing -> return bs'
      Just nt ->
        let (bs'', bsNulls) = BS.splitAt (fromIntegral nt) bs'
         in if   bsNulls == BS.replicate (BS.length bsNulls) 0x00
            then return bs''
            else err $ ErrorBinUnexpectedNonNull bs'

runPureBinCompareFwd
    :: (Compare v BS.ByteString)
    => [Patch Int '[Compare.Meta v, Bin.Meta] BS.ByteString]
    -> BS.ByteString
    -> Either Error BL.ByteString
runPureBinCompareFwd ps bs =
    let initState = (bs, mempty :: BB.Builder, 0 :: Int)
        (mErr, (bsRemaining, bbPatched, _)) = runState (applyBinCompareFwd ps) initState
        bbPatched' = bbPatched <> BB.byteString bsRemaining
     in case mErr of
          Left  e  -> Left e
          Right () -> Right $ BB.toLazyByteString bbPatched'

applyFwd
    :: (FwdInplaceStream m, Chunk m ~ a)
    => [Patch (Index m) '[] a]
    -> m ()
applyFwd =
    mapM_ $ \(Patch a s (HFunctorList RNil)) ->
        advance s >> overwrite a

runPureFwdList
    :: [Patch Int '[] [a]]
    -> [a]
    -> [a]
runPureFwdList ps start =
    let ((), (remaining, patched, _)) = runState (applyFwd ps) (start, mempty, 0 :: Int)
     in patched <> remaining

applyFwdCompare
    :: forall a v m
    .  ( FwdInplaceStream m, Chunk m ~ a
       , Compare v a, HasLength a, Num (Index m) )
    => [Patch (Index m) '[Compare.Meta v] a]
    -> m (Either Error ())
applyFwdCompare = traverseM_ $ \(Patch a s (HFunctorList (Flap cm :& RNil))) -> do
    advance s
    aStream <- readahead $ fromIntegral $ getLength a
    case Compare.mCompare cm of
      Nothing   -> do
        x <- overwrite a
        return $ Right x
      Just aCmp -> case compareTo @v aCmp aStream of
                     Nothing -> return $ Right ()
                     Just e  -> return $ Left $ ErrorCompare e

runPureFwdCompareString
    :: Compare v String
    => [Patch Int '[Compare.Meta v] String]
    -> String
    -> Either Error String
runPureFwdCompareString ps start =
    let (r, (remaining, patched, _)) = runState (applyFwdCompare ps) (start, "", 0 :: Int)
    in  case r of
          Left err -> Left err
          Right () -> Right $ patched <> remaining