packages feed

bytepatch-0.4.1: src/StreamPatch/Patch/Binary.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

-- TODO rewrite patch/check bits (some overlap)
-- TODO rewrite, rename checks (one for process, one for apply)

module StreamPatch.Patch.Binary where

import StreamPatch.Patch
import StreamPatch.HFunctorList

import Binrep

import GHC.Generics       ( Generic )
import GHC.Natural
import Data.ByteString qualified as BS
import Data.Vinyl
import Data.Functor.Const
import Data.Vinyl.TypeLevel

import StreamPatch.Patch.Compare qualified as Compare
import StreamPatch.Patch.Compare hiding ( Meta )

import Optics

data Meta a = Meta
  { mNullTerminates :: Maybe Natural
  -- ^ Stream segment should be null bytes (0x00) only from this index onwards.
  } deriving (Generic, Eq, Show, Functor, Foldable, Traversable)

data MetaPrep = MetaPrep
  { mpMaxBytes :: Maybe Natural
  -- ^ Maximum bytelength of binrepped data.
  --
  -- Though binrepping is a safe operation, this is a useful sanity check in
  -- cases where you know the maximum space available.
  --
  -- Note that this is only available for the patch data, not other meta data.
  -- (If you want that, you'll need to shove this field into the patch type.)
  -- itself. Probably not very useful.)
  } deriving (Eq, Show, Generic)

data Error a
  = ErrorBinRepOverlong BLenT BLenT a (Maybe BS.ByteString)
  -- ^ If the value was serialized, it's given in the 'Maybe'.
    deriving (Eq, Show, Generic, Functor, Foldable, Traversable)

-- Note that via binrep's 'BLen' typeclass, we can check sizing "for free",
-- without serializing. TODO, I could make another function that serializes,
-- then checks. That way, we can return the serialization attempt to the user,
-- and not require 'BLen'. In exchange, it's less efficient on errors. Both have
-- the same main path complexity.
binRepify
    :: forall a s ss is r rs
    .  ( Put a, BLen a
       , Traversable (HFunctorList rs)
       , r ~ Const MetaPrep
       , rs ~ RDelete r ss
       , RElem r ss (RIndex r ss)
       , RSubset rs ss is )
    => Patch s ss a
    -> Either (Error a) (Patch s rs BS.ByteString)
binRepify (Patch a s ms) = do
    let (m, ms') = hflStrip getConst ms
    checkMeta m
    let bs = runPut a
        bsms = fmap runPut ms'
    pure $ Patch bs s bsms
  where
    checkMeta m =
        case mpMaxBytes m of
          Nothing       -> pure ()
          Just maxBytes ->
            let maxBytes' = natToBLen maxBytes
             in if   blen a > maxBytes'
                then Left $ ErrorBinRepOverlong (blen a) maxBytes' a Nothing
                else pure ()

-- | Treat the nulls field as a "this is how many extra nulls there are", and
--   amend the compare meta for a patch by appending those nulls, and strip that
--   stream-time bin meta.
--
-- Correct thing to do, but needs lots of changes elsewhere too. Dang.
binRepifyNull
    :: forall ec s r1 r2 ss rs is
    .  ( r1 ~ Meta
       , r2 ~ Compare.Meta ('ViaEq ec)
       , rs ~ RDelete r1 ss
       , RElem r1 ss (RIndex r1 ss)
       , RSubset rs ss is
       , RElem r2 rs (RIndex r2 rs)
       , RecElem Rec r2 r2 rs rs (RIndex r2 rs)
       )
    => Patch s ss BS.ByteString
    -> Patch s rs BS.ByteString
binRepifyNull (Patch a s ms) = do
    let (mNT, ms') = hflStrip @r1 mNullTerminates ms
    case mNT of
      Nothing -> Patch a s ms'
      Just nt ->
        let mCmpLens = hflLens @r2 @r2 @rs @rs
            ms'' = over mCmpLens (\(Compare.Meta x) -> go nt x) ms'
         in Patch a s ms''
  where
    go nt = \case
      Nothing  -> Compare.Meta Nothing
      Just cmp ->
        let cmp' = cmp <> BS.replicate (fromIntegral nt) 0x00
         in Compare.Meta $ Just cmp'