bytepatch 0.2.1 → 0.3.0
raw patch · 31 files changed
+1274/−778 lines, 31 filesdep +QuickCheckdep +eitherdep +vinyldep ~mtldep ~textPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, either, vinyl
Dependency ranges changed: mtl, text
API changes (from Hackage documentation)
- BytePatch.Core: AbsSeek :: SeekKind
- BytePatch.Core: CursorSeek :: SeekKind
- BytePatch.Core: Edit :: a -> EditMeta a -> Edit a
- BytePatch.Core: EditMeta :: Maybe Int -> Maybe a -> EditMeta a
- BytePatch.Core: FwdSeek :: SeekKind
- BytePatch.Core: Patch :: SeekRep s -> Edit a -> Patch (s :: SeekKind) a
- BytePatch.Core: [editData] :: Edit a -> a
- BytePatch.Core: [editMeta] :: Edit a -> EditMeta a
- BytePatch.Core: [emExpected] :: EditMeta a -> Maybe a
- BytePatch.Core: [emNullTerminates] :: EditMeta a -> Maybe Int
- BytePatch.Core: data Edit a
- BytePatch.Core: data EditMeta a
- BytePatch.Core: data Patch (s :: SeekKind) a
- BytePatch.Core: data SeekKind
- BytePatch.Core: instance (GHC.Classes.Eq (BytePatch.Core.SeekRep s), GHC.Classes.Eq a) => GHC.Classes.Eq (BytePatch.Core.Patch s a)
- BytePatch.Core: instance (GHC.Show.Show (BytePatch.Core.SeekRep s), GHC.Show.Show a) => GHC.Show.Show (BytePatch.Core.Patch s a)
- BytePatch.Core: instance Data.Foldable.Foldable (BytePatch.Core.Patch s)
- BytePatch.Core: instance Data.Foldable.Foldable BytePatch.Core.Edit
- BytePatch.Core: instance Data.Foldable.Foldable BytePatch.Core.EditMeta
- BytePatch.Core: instance Data.Traversable.Traversable (BytePatch.Core.Patch s)
- BytePatch.Core: instance Data.Traversable.Traversable BytePatch.Core.Edit
- BytePatch.Core: instance Data.Traversable.Traversable BytePatch.Core.EditMeta
- BytePatch.Core: instance GHC.Base.Functor (BytePatch.Core.Patch s)
- BytePatch.Core: instance GHC.Base.Functor BytePatch.Core.Edit
- BytePatch.Core: instance GHC.Base.Functor BytePatch.Core.EditMeta
- BytePatch.Core: instance GHC.Classes.Eq BytePatch.Core.SeekKind
- BytePatch.Core: instance GHC.Classes.Eq a => GHC.Classes.Eq (BytePatch.Core.Edit a)
- BytePatch.Core: instance GHC.Classes.Eq a => GHC.Classes.Eq (BytePatch.Core.EditMeta a)
- BytePatch.Core: instance GHC.Generics.Generic (BytePatch.Core.Edit a)
- BytePatch.Core: instance GHC.Generics.Generic (BytePatch.Core.EditMeta a)
- BytePatch.Core: instance GHC.Generics.Generic (BytePatch.Core.Patch s a)
- BytePatch.Core: instance GHC.Generics.Generic BytePatch.Core.SeekKind
- BytePatch.Core: instance GHC.Show.Show BytePatch.Core.SeekKind
- BytePatch.Core: instance GHC.Show.Show a => GHC.Show.Show (BytePatch.Core.Edit a)
- BytePatch.Core: instance GHC.Show.Show a => GHC.Show.Show (BytePatch.Core.EditMeta a)
- BytePatch.Core: type family SeekRep (s :: SeekKind)
- BytePatch.JSON: instance (Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Core.SeekRep s), Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Pretty.EditOffset s a)
- BytePatch.JSON: instance (Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Core.SeekRep s), Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Pretty.MultiEdit s a)
- BytePatch.JSON: instance (Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Core.SeekRep s), Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Pretty.EditOffset s a)
- BytePatch.JSON: instance (Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Core.SeekRep s), Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Pretty.MultiEdit s a)
- BytePatch.JSON: instance Data.Aeson.Types.FromJSON.FromJSON BytePatch.Pretty.HexByteString.HexByteString
- BytePatch.JSON: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Core.Edit a)
- BytePatch.JSON: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Core.EditMeta a)
- BytePatch.JSON: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Pretty.CommonMultiEdits a)
- BytePatch.JSON: instance Data.Aeson.Types.ToJSON.ToJSON BytePatch.Pretty.HexByteString.HexByteString
- BytePatch.JSON: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Core.Edit a)
- BytePatch.JSON: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Core.EditMeta a)
- BytePatch.JSON: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Pretty.CommonMultiEdits a)
- BytePatch.JSON: jsonCfgCamelDrop :: Int -> Options
- BytePatch.Linear.Gen: ErrorOverlap :: Patch s a -> Patch s a -> Error s a
- BytePatch.Linear.Gen: data Error s a
- BytePatch.Linear.Gen: gen :: [Patch 'AbsSeek Bytes] -> ([Patch 'FwdSeek Bytes], [Error 'AbsSeek Bytes])
- BytePatch.Linear.Gen: instance (GHC.Classes.Eq (BytePatch.Core.SeekRep s), GHC.Classes.Eq a) => GHC.Classes.Eq (BytePatch.Linear.Gen.Error s a)
- BytePatch.Linear.Gen: instance (GHC.Show.Show (BytePatch.Core.SeekRep s), GHC.Show.Show a) => GHC.Show.Show (BytePatch.Linear.Gen.Error s a)
- BytePatch.Linear.Patch: Cfg :: Bool -> Bool -> Cfg
- BytePatch.Linear.Patch: ErrorPatchDidNotMatchExpected :: Bytes -> Bytes -> Error
- BytePatch.Linear.Patch: ErrorPatchOverlong :: Error
- BytePatch.Linear.Patch: ErrorPatchUnexpectedNonnull :: Error
- BytePatch.Linear.Patch: [cfgAllowPartialExpected] :: Cfg -> Bool
- BytePatch.Linear.Patch: [cfgWarnIfLikelyReprocessing] :: Cfg -> Bool
- BytePatch.Linear.Patch: advance :: MonadFwdByteStream m => Natural -> m ()
- BytePatch.Linear.Patch: class Monad m => MonadFwdByteStream m
- BytePatch.Linear.Patch: data Cfg
- BytePatch.Linear.Patch: data Error
- BytePatch.Linear.Patch: instance Control.Monad.IO.Class.MonadIO m => BytePatch.Linear.Patch.MonadFwdByteStream (Control.Monad.Trans.Reader.ReaderT GHC.IO.Handle.Types.Handle m)
- BytePatch.Linear.Patch: instance GHC.Base.Monad m => BytePatch.Linear.Patch.MonadFwdByteStream (Control.Monad.Trans.State.Lazy.StateT (BytePatch.Linear.Patch.Bytes, Data.ByteString.Builder.Internal.Builder) m)
- BytePatch.Linear.Patch: instance GHC.Classes.Eq BytePatch.Linear.Patch.Cfg
- BytePatch.Linear.Patch: instance GHC.Classes.Eq BytePatch.Linear.Patch.Error
- BytePatch.Linear.Patch: instance GHC.Show.Show BytePatch.Linear.Patch.Cfg
- BytePatch.Linear.Patch: instance GHC.Show.Show BytePatch.Linear.Patch.Error
- BytePatch.Linear.Patch: overwrite :: MonadFwdByteStream m => Bytes -> m ()
- BytePatch.Linear.Patch: patch :: MonadFwdByteStream m => Cfg -> [Patch 'FwdSeek Bytes] -> m (Maybe Error)
- BytePatch.Linear.Patch: patchPure :: Cfg -> [Patch 'FwdSeek Bytes] -> ByteString -> Either Error ByteString
- BytePatch.Linear.Patch: readahead :: MonadFwdByteStream m => Natural -> m Bytes
- BytePatch.Pretty: CommonMultiEdits :: Maybe (SeekRep 'CursorSeek) -> [MultiEdit 'CursorSeek a] -> CommonMultiEdits a
- BytePatch.Pretty: EditOffset :: SeekRep s -> Maybe (SeekRep 'AbsSeek) -> Maybe Natural -> Maybe (EditMeta a) -> EditOffset (s :: SeekKind) a
- BytePatch.Pretty: MultiEdit :: a -> [EditOffset s a] -> MultiEdit (s :: SeekKind) a
- BytePatch.Pretty: [cmesBaseOffset] :: CommonMultiEdits a -> Maybe (SeekRep 'CursorSeek)
- BytePatch.Pretty: [cmesEdits] :: CommonMultiEdits a -> [MultiEdit 'CursorSeek a]
- BytePatch.Pretty: [eoAbsOffset] :: EditOffset (s :: SeekKind) a -> Maybe (SeekRep 'AbsSeek)
- BytePatch.Pretty: [eoEditMeta] :: EditOffset (s :: SeekKind) a -> Maybe (EditMeta a)
- BytePatch.Pretty: [eoMaxLength] :: EditOffset (s :: SeekKind) a -> Maybe Natural
- BytePatch.Pretty: [eoOffset] :: EditOffset (s :: SeekKind) a -> SeekRep s
- BytePatch.Pretty: [meAt] :: MultiEdit (s :: SeekKind) a -> [EditOffset s a]
- BytePatch.Pretty: [meData] :: MultiEdit (s :: SeekKind) a -> a
- BytePatch.Pretty: applyBaseOffset :: CommonMultiEdits a -> (Integer, [(MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])])
- BytePatch.Pretty: data CommonMultiEdits a
- BytePatch.Pretty: data EditOffset (s :: SeekKind) a
- BytePatch.Pretty: data MultiEdit (s :: SeekKind) a
- BytePatch.Pretty: instance (GHC.Classes.Eq (BytePatch.Core.SeekRep s), GHC.Classes.Eq a) => GHC.Classes.Eq (BytePatch.Pretty.EditOffset s a)
- BytePatch.Pretty: instance (GHC.Classes.Eq (BytePatch.Core.SeekRep s), GHC.Classes.Eq a) => GHC.Classes.Eq (BytePatch.Pretty.MultiEdit s a)
- BytePatch.Pretty: instance (GHC.Show.Show (BytePatch.Core.SeekRep s), GHC.Show.Show a) => GHC.Show.Show (BytePatch.Pretty.EditOffset s a)
- BytePatch.Pretty: instance (GHC.Show.Show (BytePatch.Core.SeekRep s), GHC.Show.Show a) => GHC.Show.Show (BytePatch.Pretty.MultiEdit s a)
- BytePatch.Pretty: instance Data.Foldable.Foldable (BytePatch.Pretty.EditOffset s)
- BytePatch.Pretty: instance Data.Foldable.Foldable (BytePatch.Pretty.MultiEdit s)
- BytePatch.Pretty: instance Data.Foldable.Foldable BytePatch.Pretty.CommonMultiEdits
- BytePatch.Pretty: instance Data.Traversable.Traversable (BytePatch.Pretty.EditOffset s)
- BytePatch.Pretty: instance Data.Traversable.Traversable (BytePatch.Pretty.MultiEdit s)
- BytePatch.Pretty: instance Data.Traversable.Traversable BytePatch.Pretty.CommonMultiEdits
- BytePatch.Pretty: instance GHC.Base.Functor (BytePatch.Pretty.EditOffset s)
- BytePatch.Pretty: instance GHC.Base.Functor (BytePatch.Pretty.MultiEdit s)
- BytePatch.Pretty: instance GHC.Base.Functor BytePatch.Pretty.CommonMultiEdits
- BytePatch.Pretty: instance GHC.Classes.Eq a => GHC.Classes.Eq (BytePatch.Pretty.CommonMultiEdits a)
- BytePatch.Pretty: instance GHC.Generics.Generic (BytePatch.Pretty.CommonMultiEdits a)
- BytePatch.Pretty: instance GHC.Generics.Generic (BytePatch.Pretty.EditOffset s a)
- BytePatch.Pretty: instance GHC.Generics.Generic (BytePatch.Pretty.MultiEdit s a)
- BytePatch.Pretty: instance GHC.Show.Show a => GHC.Show.Show (BytePatch.Pretty.CommonMultiEdits a)
- BytePatch.Pretty: listAlgebraConcatEtc :: [(a, [(b, [c])])] -> ([b], [(c, a)])
- BytePatch.Pretty: normalize :: PatchRep a => [MultiEdit 'AbsSeek a] -> Maybe [Patch 'AbsSeek Bytes]
- BytePatch.Pretty: normalizeSimple :: PatchRep a => [CommonMultiEdits a] -> Maybe [Patch 'AbsSeek Bytes]
- BytePatch.Pretty.HexByteString: HexByteString :: Bytes -> HexByteString
- BytePatch.Pretty.HexByteString: [unHexByteString] :: HexByteString -> Bytes
- BytePatch.Pretty.HexByteString: instance BytePatch.Pretty.PatchRep.PatchRep BytePatch.Pretty.HexByteString.HexByteString
- BytePatch.Pretty.HexByteString: instance GHC.Classes.Eq BytePatch.Pretty.HexByteString.HexByteString
- BytePatch.Pretty.HexByteString: instance GHC.Show.Show BytePatch.Pretty.HexByteString.HexByteString
- BytePatch.Pretty.HexByteString: newtype HexByteString
- BytePatch.Pretty.HexByteString: parseHexByteString :: (MonadParsec e s m, Token s ~ Char) => m Bytes
- BytePatch.Pretty.HexByteString: prettyHexByteString :: Bytes -> Text
- BytePatch.Pretty.PascalText: PascalText :: Text -> PascalText (n :: Nat)
- BytePatch.Pretty.PascalText: [unPascalText] :: PascalText (n :: Nat) -> Text
- BytePatch.Pretty.PascalText: encodePascalText :: forall n. KnownNat n => PascalText n -> Maybe ByteString
- BytePatch.Pretty.PascalText: encodeToSizedBE :: (Integral a, Bits a) => Int -> a -> Maybe ByteString
- BytePatch.Pretty.PascalText: i2be :: (Integral a, Bits a) => a -> ByteString
- BytePatch.Pretty.PascalText: instance GHC.TypeNats.KnownNat n => BytePatch.Pretty.PatchRep.PatchRep (BytePatch.Pretty.PascalText.PascalText n)
- BytePatch.Pretty.PascalText: newtype PascalText (n :: Nat)
- BytePatch.Pretty.PascalText: ptLengthBytes :: forall n. KnownNat n => PascalText n -> Integer
- BytePatch.Pretty.PatchRep: class PatchRep a
- BytePatch.Pretty.PatchRep: instance BytePatch.Pretty.PatchRep.PatchRep Data.ByteString.Internal.ByteString
- BytePatch.Pretty.PatchRep: instance BytePatch.Pretty.PatchRep.PatchRep Data.Text.Internal.Text
- BytePatch.Pretty.PatchRep: toPatchRep :: PatchRep a => a -> Either String ByteString
+ BytePatch: Aligned :: SeekRep 'RelSeek -> [p] -> Aligned p
+ BytePatch: MultiPatch :: a -> [Seek s a] -> MultiPatch s a
+ BytePatch: Seek :: SeekRep s -> Maybe a -> Maybe (SeekRep 'FwdSeek) -> Maybe (SeekRep 'FwdSeek) -> Maybe Natural -> Seek s a
+ BytePatch: [alignedAlign] :: Aligned p -> SeekRep 'RelSeek
+ BytePatch: [alignedPatches] :: Aligned p -> [p]
+ BytePatch: [mpAt] :: MultiPatch s a -> [Seek s a]
+ BytePatch: [mpData] :: MultiPatch s a -> a
+ BytePatch: [sAligned] :: Seek s a -> Maybe Natural
+ BytePatch: [sExpected] :: Seek s a -> Maybe a
+ BytePatch: [sMaxBytes] :: Seek s a -> Maybe (SeekRep 'FwdSeek)
+ BytePatch: [sNullTerminates] :: Seek s a -> Maybe (SeekRep 'FwdSeek)
+ BytePatch: [sSeek] :: Seek s a -> SeekRep s
+ BytePatch: align :: forall s a ss rs is i r. (SeekRep s ~ Natural, r ~ Const (Meta s), rs ~ RDelete r ss, RElem r ss i, RSubset rs ss is) => Aligned (Patch 'RelSeek ss a) -> Either (Error s) [Patch s rs a]
+ BytePatch: cm :: f a -> Rec (Flap a) '[f]
+ BytePatch: cmAlign :: SeekRep s ~ Natural => Seek 'RelSeek a -> Rec (Flap a) '[Const (Meta s)]
+ BytePatch: cmBin :: Seek s a -> Rec (Flap a) '[Const Meta]
+ BytePatch: cmBinStream :: Seek s a -> Rec (Flap a) '[MetaStream]
+ BytePatch: convert :: (Seek s a -> Rec (Flap a) fs) -> MultiPatch s a -> [Patch s fs a]
+ BytePatch: convertAlign :: forall s a. SeekRep s ~ Natural => MultiPatch 'RelSeek a -> [Patch 'RelSeek '[Const (Meta s)] a]
+ BytePatch: convertBin :: forall s a. MultiPatch s a -> [Patch s '[Const Meta, MetaStream] a]
+ BytePatch: convertBinAlign :: forall s a. SeekRep s ~ Natural => MultiPatch 'RelSeek a -> [Patch 'RelSeek '[Const (Meta s), Const Meta, MetaStream] a]
+ BytePatch: convertEmpty :: forall s a. MultiPatch s a -> [Patch s '[] a]
+ BytePatch: data Aligned p
+ BytePatch: data MultiPatch s a
+ BytePatch: data Seek s a
+ BytePatch: instance (Data.Aeson.Types.FromJSON.FromJSON (StreamPatch.Patch.SeekRep s), Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.MultiPatch s a)
+ BytePatch: instance (Data.Aeson.Types.FromJSON.FromJSON (StreamPatch.Patch.SeekRep s), Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Seek s a)
+ BytePatch: instance (Data.Aeson.Types.ToJSON.ToJSON (StreamPatch.Patch.SeekRep s), Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.MultiPatch s a)
+ BytePatch: instance (Data.Aeson.Types.ToJSON.ToJSON (StreamPatch.Patch.SeekRep s), Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Seek s a)
+ BytePatch: instance (GHC.Classes.Eq a, GHC.Classes.Eq (StreamPatch.Patch.SeekRep s)) => GHC.Classes.Eq (BytePatch.MultiPatch s a)
+ BytePatch: instance (GHC.Classes.Eq a, GHC.Classes.Eq (StreamPatch.Patch.SeekRep s)) => GHC.Classes.Eq (BytePatch.Seek s a)
+ BytePatch: instance (GHC.Show.Show a, GHC.Show.Show (StreamPatch.Patch.SeekRep s)) => GHC.Show.Show (BytePatch.MultiPatch s a)
+ BytePatch: instance (GHC.Show.Show a, GHC.Show.Show (StreamPatch.Patch.SeekRep s)) => GHC.Show.Show (BytePatch.Seek s a)
+ BytePatch: instance Data.Aeson.Types.FromJSON.FromJSON p => Data.Aeson.Types.FromJSON.FromJSON (BytePatch.Aligned p)
+ BytePatch: instance Data.Aeson.Types.ToJSON.ToJSON p => Data.Aeson.Types.ToJSON.ToJSON (BytePatch.Aligned p)
+ BytePatch: instance GHC.Classes.Eq p => GHC.Classes.Eq (BytePatch.Aligned p)
+ BytePatch: instance GHC.Generics.Generic (BytePatch.Aligned p)
+ BytePatch: instance GHC.Generics.Generic (BytePatch.MultiPatch s a)
+ BytePatch: instance GHC.Generics.Generic (BytePatch.Seek s a)
+ BytePatch: instance GHC.Show.Show p => GHC.Show.Show (BytePatch.Aligned p)
+ BytePatch: jsonCfgCamelDrop :: Int -> Options
+ BytePatch.HexByteString: HexByteString :: Bytes -> HexByteString
+ BytePatch.HexByteString: [unHexByteString] :: HexByteString -> Bytes
+ BytePatch.HexByteString: instance Data.Aeson.Types.FromJSON.FromJSON BytePatch.HexByteString.HexByteString
+ BytePatch.HexByteString: instance Data.Aeson.Types.ToJSON.ToJSON BytePatch.HexByteString.HexByteString
+ BytePatch.HexByteString: instance GHC.Classes.Eq BytePatch.HexByteString.HexByteString
+ BytePatch.HexByteString: instance GHC.Show.Show BytePatch.HexByteString.HexByteString
+ BytePatch.HexByteString: instance StreamPatch.Patch.Binary.BinRep BytePatch.HexByteString.HexByteString
+ BytePatch.HexByteString: newtype HexByteString
+ BytePatch.HexByteString: parseHexByteString :: (MonadParsec e s m, Token s ~ Char) => m Bytes
+ BytePatch.HexByteString: prettyHexByteString :: Bytes -> Text
+ StreamPatch.Apply: applyBinFwd :: forall a m. (MonadFwdStream m, Chunk m ~ ByteString, BinRep a) => Cfg -> [Patch 'FwdSeek '[MetaStream] a] -> m (Either (Error a) ())
+ StreamPatch.Apply: applySimpleFwd :: (MonadFwdStream m, Chunk m ~ a) => [Patch 'FwdSeek '[] a] -> m ()
+ StreamPatch.Apply: runPureFwdBin :: BinRep a => Cfg -> [Patch 'FwdSeek '[MetaStream] a] -> ByteString -> Either (Error a) ByteString
+ StreamPatch.Apply: runPureSimpleFwdList :: [Patch 'FwdSeek '[] [a]] -> [a] -> [a]
+ StreamPatch.Patch: AbsSeek :: SeekKind
+ StreamPatch.Patch: Flap :: f a -> Flap a f
+ StreamPatch.Patch: FunctorRec :: Rec (Flap a) fs -> FunctorRec fs a
+ StreamPatch.Patch: FwdSeek :: SeekKind
+ StreamPatch.Patch: Patch :: a -> SeekRep s -> FunctorRec fs a -> Patch s fs a
+ StreamPatch.Patch: RelSeek :: SeekKind
+ StreamPatch.Patch: [getFlap] :: Flap a f -> f a
+ StreamPatch.Patch: [getFunctorRec] :: FunctorRec fs a -> Rec (Flap a) fs
+ StreamPatch.Patch: [patchData] :: Patch s fs a -> a
+ StreamPatch.Patch: [patchMeta] :: Patch s fs a -> FunctorRec fs a
+ StreamPatch.Patch: [patchSeek] :: Patch s fs a -> SeekRep s
+ StreamPatch.Patch: data Patch s fs a
+ StreamPatch.Patch: data SeekKind
+ StreamPatch.Patch: instance (Data.Foldable.Foldable r, Data.Foldable.Foldable (StreamPatch.Patch.FunctorRec rs)) => Data.Foldable.Foldable (StreamPatch.Patch.FunctorRec (r : rs))
+ StreamPatch.Patch: instance (Data.Traversable.Traversable r, Data.Traversable.Traversable (StreamPatch.Patch.FunctorRec rs)) => Data.Traversable.Traversable (StreamPatch.Patch.FunctorRec (r : rs))
+ StreamPatch.Patch: instance (Data.Vinyl.Core.ReifyConstraint GHC.Show.Show (StreamPatch.Patch.Flap a) fs, Data.Vinyl.Core.RMap fs, Data.Vinyl.Core.RecordToList fs) => GHC.Show.Show (StreamPatch.Patch.FunctorRec fs a)
+ StreamPatch.Patch: instance (GHC.Base.Applicative r, GHC.Base.Applicative (StreamPatch.Patch.FunctorRec rs)) => GHC.Base.Applicative (StreamPatch.Patch.FunctorRec (r : rs))
+ StreamPatch.Patch: instance (GHC.Base.Functor r, GHC.Base.Functor (StreamPatch.Patch.FunctorRec rs)) => GHC.Base.Functor (StreamPatch.Patch.FunctorRec (r : rs))
+ StreamPatch.Patch: instance (GHC.Classes.Eq a, GHC.Classes.Eq (StreamPatch.Patch.SeekRep s), GHC.Classes.Eq (Data.Vinyl.Core.Rec (StreamPatch.Patch.Flap a) fs)) => GHC.Classes.Eq (StreamPatch.Patch.Patch s fs a)
+ StreamPatch.Patch: instance (GHC.Show.Show a, GHC.Show.Show (StreamPatch.Patch.SeekRep s), Data.Vinyl.Core.ReifyConstraint GHC.Show.Show (StreamPatch.Patch.Flap a) fs, Data.Vinyl.Core.RMap fs, Data.Vinyl.Core.RecordToList fs) => GHC.Show.Show (StreamPatch.Patch.Patch s fs a)
+ StreamPatch.Patch: instance Data.Foldable.Foldable (StreamPatch.Patch.FunctorRec '[])
+ StreamPatch.Patch: instance Data.Traversable.Traversable (StreamPatch.Patch.FunctorRec '[])
+ StreamPatch.Patch: instance GHC.Base.Applicative (StreamPatch.Patch.FunctorRec '[])
+ StreamPatch.Patch: instance GHC.Base.Functor (StreamPatch.Patch.FunctorRec '[])
+ StreamPatch.Patch: instance GHC.Base.Functor (StreamPatch.Patch.FunctorRec fs) => GHC.Base.Functor (StreamPatch.Patch.Patch s fs)
+ StreamPatch.Patch: instance GHC.Classes.Eq (Data.Vinyl.Core.Rec (StreamPatch.Patch.Flap a) fs) => GHC.Classes.Eq (StreamPatch.Patch.FunctorRec fs a)
+ StreamPatch.Patch: instance GHC.Classes.Eq (f a) => GHC.Classes.Eq (StreamPatch.Patch.Flap a f)
+ StreamPatch.Patch: instance GHC.Classes.Eq StreamPatch.Patch.SeekKind
+ StreamPatch.Patch: instance GHC.Generics.Generic (StreamPatch.Patch.Flap a f)
+ StreamPatch.Patch: instance GHC.Generics.Generic (StreamPatch.Patch.FunctorRec fs a)
+ StreamPatch.Patch: instance GHC.Generics.Generic (StreamPatch.Patch.Patch s fs a)
+ StreamPatch.Patch: instance GHC.Generics.Generic StreamPatch.Patch.SeekKind
+ StreamPatch.Patch: instance GHC.Show.Show (f a) => GHC.Show.Show (StreamPatch.Patch.Flap a f)
+ StreamPatch.Patch: instance GHC.Show.Show StreamPatch.Patch.SeekKind
+ StreamPatch.Patch: newtype Flap a f
+ StreamPatch.Patch: newtype FunctorRec fs a
+ StreamPatch.Patch: type PatchLike = SeekKind -> [Type -> Type] -> Type -> Type
+ StreamPatch.Patch: type family SeekRep (s :: SeekKind)
+ StreamPatch.Patch.Align: ErrorDoesntMatchExpected :: SeekRep s -> SeekRep s -> Error s
+ StreamPatch.Patch.Align: ErrorSeekBelow0 :: SeekRep 'RelSeek -> Error s
+ StreamPatch.Patch.Align: Meta :: Maybe (SeekRep s) -> Meta s
+ StreamPatch.Patch.Align: [mExpected] :: Meta s -> Maybe (SeekRep s)
+ StreamPatch.Patch.Align: align :: forall s a ss rs is i r. (SeekRep s ~ Natural, r ~ Const (Meta s), rs ~ RDelete r ss, RElem r ss i, RSubset rs ss is) => SeekRep 'RelSeek -> Patch 'RelSeek ss a -> Either (Error s) (Patch s rs a)
+ StreamPatch.Patch.Align: data Error s
+ StreamPatch.Patch.Align: data Meta s
+ StreamPatch.Patch.Align: instance GHC.Classes.Eq (StreamPatch.Patch.SeekRep s) => GHC.Classes.Eq (StreamPatch.Patch.Align.Error s)
+ StreamPatch.Patch.Align: instance GHC.Classes.Eq (StreamPatch.Patch.SeekRep s) => GHC.Classes.Eq (StreamPatch.Patch.Align.Meta s)
+ StreamPatch.Patch.Align: instance GHC.Generics.Generic (StreamPatch.Patch.Align.Error s)
+ StreamPatch.Patch.Align: instance GHC.Generics.Generic (StreamPatch.Patch.Align.Meta s)
+ StreamPatch.Patch.Align: instance GHC.Show.Show (StreamPatch.Patch.SeekRep s) => GHC.Show.Show (StreamPatch.Patch.Align.Error s)
+ StreamPatch.Patch.Align: instance GHC.Show.Show (StreamPatch.Patch.SeekRep s) => GHC.Show.Show (StreamPatch.Patch.Align.Meta s)
+ StreamPatch.Patch.Align: tryIntegerToNatural :: Integer -> Maybe Natural
+ StreamPatch.Patch.Binary: Cfg :: Bool -> Cfg
+ StreamPatch.Patch.Binary: ErrorBadBinRep :: a -> String -> Error a
+ StreamPatch.Patch.Binary: ErrorBinRepTooLong :: Bytes -> Natural -> Error a
+ StreamPatch.Patch.Binary: ErrorDidNotMatchExpected :: Bytes -> Bytes -> Error a
+ StreamPatch.Patch.Binary: ErrorUnexpectedNonNull :: Bytes -> Error a
+ StreamPatch.Patch.Binary: Meta :: Maybe (SeekRep 'FwdSeek) -> Meta
+ StreamPatch.Patch.Binary: MetaStream :: Maybe (SeekRep 'FwdSeek) -> Maybe a -> MetaStream a
+ StreamPatch.Patch.Binary: [cfgAllowPartialExpected] :: Cfg -> Bool
+ StreamPatch.Patch.Binary: [mMaxBytes] :: Meta -> Maybe (SeekRep 'FwdSeek)
+ StreamPatch.Patch.Binary: [msExpected] :: MetaStream a -> Maybe a
+ StreamPatch.Patch.Binary: [msNullTerminates] :: MetaStream a -> Maybe (SeekRep 'FwdSeek)
+ StreamPatch.Patch.Binary: check :: BinRep a => Cfg -> Bytes -> MetaStream a -> Either (Error a) ()
+ StreamPatch.Patch.Binary: class BinRep a
+ StreamPatch.Patch.Binary: data Cfg
+ StreamPatch.Patch.Binary: data Error a
+ StreamPatch.Patch.Binary: data Meta
+ StreamPatch.Patch.Binary: data MetaStream a
+ StreamPatch.Patch.Binary: instance Data.Foldable.Foldable StreamPatch.Patch.Binary.Error
+ StreamPatch.Patch.Binary: instance Data.Foldable.Foldable StreamPatch.Patch.Binary.MetaStream
+ StreamPatch.Patch.Binary: instance Data.Traversable.Traversable StreamPatch.Patch.Binary.Error
+ StreamPatch.Patch.Binary: instance Data.Traversable.Traversable StreamPatch.Patch.Binary.MetaStream
+ StreamPatch.Patch.Binary: instance GHC.Base.Functor StreamPatch.Patch.Binary.Error
+ StreamPatch.Patch.Binary: instance GHC.Base.Functor StreamPatch.Patch.Binary.MetaStream
+ StreamPatch.Patch.Binary: instance GHC.Classes.Eq StreamPatch.Patch.Binary.Cfg
+ StreamPatch.Patch.Binary: instance GHC.Classes.Eq StreamPatch.Patch.Binary.Meta
+ StreamPatch.Patch.Binary: instance GHC.Classes.Eq a => GHC.Classes.Eq (StreamPatch.Patch.Binary.Error a)
+ StreamPatch.Patch.Binary: instance GHC.Classes.Eq a => GHC.Classes.Eq (StreamPatch.Patch.Binary.MetaStream a)
+ StreamPatch.Patch.Binary: instance GHC.Generics.Generic (StreamPatch.Patch.Binary.Error a)
+ StreamPatch.Patch.Binary: instance GHC.Generics.Generic (StreamPatch.Patch.Binary.MetaStream a)
+ StreamPatch.Patch.Binary: instance GHC.Generics.Generic StreamPatch.Patch.Binary.Cfg
+ StreamPatch.Patch.Binary: instance GHC.Generics.Generic StreamPatch.Patch.Binary.Meta
+ StreamPatch.Patch.Binary: instance GHC.Show.Show StreamPatch.Patch.Binary.Cfg
+ StreamPatch.Patch.Binary: instance GHC.Show.Show StreamPatch.Patch.Binary.Meta
+ StreamPatch.Patch.Binary: instance GHC.Show.Show a => GHC.Show.Show (StreamPatch.Patch.Binary.Error a)
+ StreamPatch.Patch.Binary: instance GHC.Show.Show a => GHC.Show.Show (StreamPatch.Patch.Binary.MetaStream a)
+ StreamPatch.Patch.Binary: instance StreamPatch.Patch.Binary.BinRep Data.ByteString.Internal.ByteString
+ StreamPatch.Patch.Binary: instance StreamPatch.Patch.Binary.BinRep Data.Text.Internal.Text
+ StreamPatch.Patch.Binary: instance StreamPatch.Patch.Binary.BinRep GHC.Base.String
+ StreamPatch.Patch.Binary: 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 Bytes)
+ StreamPatch.Patch.Binary: toBinRep :: BinRep a => a -> Either String Bytes
+ StreamPatch.Patch.Binary: toBinRep' :: BinRep a => a -> Either (Error a) Bytes
+ StreamPatch.Patch.Binary.PascalText: PascalText :: Text -> PascalText (n :: Nat)
+ StreamPatch.Patch.Binary.PascalText: [unPascalText] :: PascalText (n :: Nat) -> Text
+ StreamPatch.Patch.Binary.PascalText: encodePascalText :: forall n. KnownNat n => PascalText n -> Maybe ByteString
+ StreamPatch.Patch.Binary.PascalText: encodeToSizedBE :: (Integral a, Bits a) => Int -> a -> Maybe ByteString
+ StreamPatch.Patch.Binary.PascalText: i2be :: (Integral a, Bits a) => a -> ByteString
+ StreamPatch.Patch.Binary.PascalText: instance GHC.TypeNats.KnownNat n => StreamPatch.Patch.Binary.BinRep (StreamPatch.Patch.Binary.PascalText.PascalText n)
+ StreamPatch.Patch.Binary.PascalText: newtype PascalText (n :: Nat)
+ StreamPatch.Patch.Linearize: ErrorOverlap :: SeekRep 'AbsSeek -> Patch 'AbsSeek fs a -> Patch 'AbsSeek fs a -> Error fs a
+ StreamPatch.Patch.Linearize: class HasLength a
+ StreamPatch.Patch.Linearize: comparePatchSeeks :: Ord (SeekRep s) => Patch s fs a -> Patch s fs a -> Ordering
+ StreamPatch.Patch.Linearize: data Error fs a
+ StreamPatch.Patch.Linearize: getLength :: HasLength a => a -> Natural
+ StreamPatch.Patch.Linearize: instance (GHC.Classes.Eq a, GHC.Classes.Eq (StreamPatch.Patch.Patch 'StreamPatch.Patch.AbsSeek fs a)) => GHC.Classes.Eq (StreamPatch.Patch.Linearize.Error fs a)
+ StreamPatch.Patch.Linearize: instance (GHC.Show.Show a, Data.Vinyl.Core.ReifyConstraint GHC.Show.Show (StreamPatch.Patch.Flap a) fs, Data.Vinyl.Core.RMap fs, Data.Vinyl.Core.RecordToList fs) => GHC.Show.Show (StreamPatch.Patch.Linearize.Error fs a)
+ StreamPatch.Patch.Linearize: instance Data.Foldable.Foldable (StreamPatch.Patch.Patch 'StreamPatch.Patch.AbsSeek fs) => Data.Foldable.Foldable (StreamPatch.Patch.Linearize.Error fs)
+ StreamPatch.Patch.Linearize: instance Data.Traversable.Traversable (StreamPatch.Patch.Patch 'StreamPatch.Patch.AbsSeek fs) => Data.Traversable.Traversable (StreamPatch.Patch.Linearize.Error fs)
+ StreamPatch.Patch.Linearize: instance GHC.Base.Functor (StreamPatch.Patch.Patch 'StreamPatch.Patch.AbsSeek fs) => GHC.Base.Functor (StreamPatch.Patch.Linearize.Error fs)
+ StreamPatch.Patch.Linearize: instance GHC.Generics.Generic (StreamPatch.Patch.Linearize.Error fs a)
+ StreamPatch.Patch.Linearize: instance StreamPatch.Patch.Linearize.HasLength Data.ByteString.Internal.ByteString
+ StreamPatch.Patch.Linearize: instance StreamPatch.Patch.Linearize.HasLength Data.Text.Internal.Text
+ StreamPatch.Patch.Linearize: instance StreamPatch.Patch.Linearize.HasLength GHC.Base.String
+ StreamPatch.Patch.Linearize: linearize :: HasLength a => [Patch 'AbsSeek fs a] -> Either (Error fs a) [Patch 'FwdSeek fs a]
+ StreamPatch.Stream: advance :: MonadFwdStream m => Natural -> m ()
+ StreamPatch.Stream: class MonadFwdStream m => MonadCursorStream m
+ StreamPatch.Stream: class Monad m => MonadFwdStream m where {
+ StreamPatch.Stream: instance Control.Monad.IO.Class.MonadIO m => StreamPatch.Stream.MonadCursorStream (Control.Monad.Trans.Reader.ReaderT GHC.IO.Handle.Types.Handle m)
+ StreamPatch.Stream: instance Control.Monad.IO.Class.MonadIO m => StreamPatch.Stream.MonadFwdStream (Control.Monad.Trans.Reader.ReaderT GHC.IO.Handle.Types.Handle m)
+ StreamPatch.Stream: instance GHC.Base.Monad m => StreamPatch.Stream.MonadFwdStream (Control.Monad.Trans.State.Lazy.StateT (Data.ByteString.Internal.ByteString, Data.ByteString.Builder.Internal.Builder) m)
+ StreamPatch.Stream: instance GHC.Base.Monad m => StreamPatch.Stream.MonadFwdStream (Control.Monad.Trans.State.Lazy.StateT ([a], [a]) m)
+ StreamPatch.Stream: move :: MonadCursorStream m => Integer -> m ()
+ StreamPatch.Stream: overwrite :: MonadFwdStream m => Chunk m -> m ()
+ StreamPatch.Stream: readahead :: MonadFwdStream m => Natural -> m (Chunk m)
+ StreamPatch.Stream: type family Chunk m :: Type;
+ StreamPatch.Stream: }
+ Util: traverseM :: (Traversable t, Applicative f, Monad m) => (v -> m (f v')) -> t v -> m (f (t v'))
+ Util: traverseM_ :: (Traversable t, Applicative f, Monad m) => (v -> m (f ())) -> t v -> m (f ())
Files
- CHANGELOG.md +12/−0
- README.md +23/−20
- app/CLI.hs +66/−0
- app/Config.hs +29/−7
- app/Main.hs +169/−58
- app/Options.hs +0/−39
- bytepatch.cabal +49/−19
- src/BytePatch.hs +133/−0
- src/BytePatch/Core.hs +0/−38
- src/BytePatch/HexByteString.hs +70/−0
- src/BytePatch/JSON.hs +0/−55
- src/BytePatch/Linear/Gen.hs +0/−59
- src/BytePatch/Linear/Patch.hs +0/−146
- src/BytePatch/Pretty.hs +0/−166
- src/BytePatch/Pretty/HexByteString.hs +0/−62
- src/BytePatch/Pretty/PascalText.hs +0/−56
- src/BytePatch/Pretty/PatchRep.hs +0/−29
- src/StreamPatch/Apply.hs +60/−0
- src/StreamPatch/Patch.hs +79/−0
- src/StreamPatch/Patch/Align.hs +59/−0
- src/StreamPatch/Patch/Binary.hs +181/−0
- src/StreamPatch/Patch/Binary/PascalText.hs +48/−0
- src/StreamPatch/Patch/Linearize.hs +58/−0
- src/StreamPatch/Stream.hs +81/−0
- src/Util.hs +19/−0
- test/BytePatch/HexByteStringSpec.hs +23/−0
- test/BytePatch/Pretty/HexByteStringSpec.hs +0/−23
- test/StreamPatch/ApplySpec.hs +48/−0
- test/StreamPatch/Patch/AlignSpec.hs +28/−0
- test/StreamPatch/Patch/LinearizeSpec.hs +19/−0
- test/Util.hs +20/−1
CHANGELOG.md view
@@ -1,3 +1,15 @@+## 0.3.0 (Unreleased)+Large internal refactoring.++ * Various concepts surrounding stream patching have been decoupled and+ parameterized. Pre-apply patch transformations are modelled as "metadata+ layers" which can be stripped off, and patch application functions require+ that no irrelevant metadata is present.+ * A new CLI inspired by the original tool exposes the useful bits.+ * Internally, the package is split into streampatch and bytepatch. If I could+ be bothered, they would be different packages, streampatch not requiring+ things like Aeson and Megaparsec.+ ## 0.2.1 (2021-12-03) * large internal refactoring (still more to come!) * schema refactoring (patch -> edit, offsets -> at)
README.md view
@@ -1,12 +1,15 @@ # bytepatch-A Haskell library and CLI tool for patching byte-representable data in a-bytestream. Write **patchscripts** (in-place edits to a file) using a convenient-YAML format, and apply them with a safe patching algorithm. You choose how-explicit to be: edits may be written as a simple `(offset, content)` tuple, or-you can provide metadata regarding expected original data, so you know you're-patching the right file.+A Haskell library and CLI tool for patching data in a stream, primarily aimed at+handling byte-representable data. Write **patchscripts** (in-place edits to a+file) using a convenient YAML format, and apply them with a safe patching+algorithm. On the library side, define explicit patches over any type, apply+them to pure/impure streams, and write further patch types and streams to extend+as you desire. -Intended as a general tool for making simple binary editing more manageable.+The executable is intended as a general tool for making simple binary editing+more manageable. The codebase is really a fun investigation into some type-level+Haskell features. Regardless of which you're interested in, I hope you might+find some use from this project! ## What? If you're modifying binaries, you often end up needing to make edits in a hex@@ -14,13 +17,13 @@ mess up. bytepatch provides a simple, human read-writeable format for defining such edits, and can apply them for you. -bytepatch is intended as a developer tool for writing a *patchscript*. It's not-a binary patch tool like IPS, BPS: these take an input and a result file, and-generate a patch file that can be applied to the input in order to recreate the-result. By itself, the patch file isn't useful, being instructions to the tool-telling how to edit the input file. bytepatch uses a human-readable patch file-format that describes in plain terms the edits to make, so developers can read-and write patches in a structured, useable format.+bytepatch is intended as a developer tool for writing a *static patchscript*.+It's not a binary patch tool like IPS, BPS: these take an input and a result+file, and generate a patch file that can be applied to the input in order to+recreate the result. By itself, the patch file isn't useful, being instructions+to the tool telling how to edit the input file. bytepatch uses a human-readable+patch file format that describes in plain terms the edits to make, so developers+can read and write patches in a structured, useable format. ## When might I want to use this? You might find bytepatch useful if you want to define edits to be made on binary@@ -34,11 +37,11 @@ gaining much.)* You can't use bytepatch to make edits that aren't in-place. Edits are located-using a byte offset. Allowing edits to change the length of the segment they+using stream offsets. Allowing edits to change the length of the segment they replace would introduce issues for following edits: do we use the original offset, or do we implicitly shift them so they still write to the "same place"?-It may be possible to design a convenient format with useful behaviour. I'd like-to look into it at some point.+There *is* some support in the library for such edits, however, and it would be+interesting to explore. ### Original rationale I'm working on some binary files, and need to patch strings, machine code and@@ -48,9 +51,9 @@ others. But if I want to test more stuff, I may have to make those changes again. I'm gonna make a mistake eventually. -Instead, I described my patches in YAML, and wrote a tool to apply them. Now my-changes and my notes are the same file, which can be checked for correctness and-applied automatically.+Instead, I described my patches in YAML and wrote a tool to apply them. Now my+changes and my notes (thanks YAML comments!) are the same file, which can be+checked for correctness and applied automatically. ## License Provided under the MIT license. Please see `LICENSE` for the full license text.
+ app/CLI.hs view
@@ -0,0 +1,66 @@+module CLI ( parse ) where++import Config+import Options.Applicative+import Control.Monad.IO.Class+import qualified StreamPatch.Patch.Binary as Bin++parse :: MonadIO m => m Config+parse = execParserWithDefaults desc pConfig+ where desc = "Patch bytestrings (TODO)."++pConfig :: Parser Config+pConfig = Config+ <$> strArgument (metavar "PATCHSCRIPT" <> help "Path to patchscript")+ <*> pCStreamPair+ <*> pCPatchFormat+ <*> pPrintBinary++pCStreamPair :: Parser CStreamPair+pCStreamPair = CStreamPair <$> pCSIn <*> pCSOut+ where+ pCSIn = pFileArg <|> pStdin+ pCSOut = pFileOpt <|> pure CStreamStd+ pFileArg = CStreamFile <$> strArgument (metavar "FILE" <> help "Input file")+ pFileOpt = CStreamFile <$> strOption (metavar "FILE" <> long "out-file" <> short 'o' <> help "Output file")+ pStdin = flag' CStreamStd (long "stdin" <> help "Use stdin")++pCPatchFormat :: Parser CPatchFormat+pCPatchFormat = CPatchFormat <$> pCPatchDataType <*> pCPatchAlign <*> pBinCfg++pCPatchDataType :: Parser CPatchDataType+pCPatchDataType = option (maybeReader mapper) $+ long "type"+ <> short 't'+ <> help "How to interpret & use patch data (binary/text/text-only)"+ <> metavar "PATCH_TYPE"+ where mapper = \case "text-only" -> Just CTextPatch+ "bin" -> Just CBinPatch+ "binary" -> Just CBinPatch+ "text" -> Just CTextBinPatch+ _ -> Nothing++pCPatchAlign :: Parser CPatchAlign+pCPatchAlign = flag CNoAlignPatch CAlignPatch $+ long "aligned"+ <> help "Parse alignment data"++pBinCfg :: Parser Bin.Cfg+pBinCfg = Bin.Cfg <$> pExactMatch+ where+ pExactMatch = flag True False $+ long "match-exact"+ <> help "Expected binary data must match exactly (rather than be a prefix)"++pPrintBinary :: Parser Bool+pPrintBinary = switch $+ long "print-binary"+ <> help "Force print binary to stdout"++--------------------------------------------------------------------------------++-- | Execute a 'Parser' with decent defaults.+execParserWithDefaults :: MonadIO m => String -> Parser a -> m a+execParserWithDefaults desc p = liftIO $ customExecParser+ (prefs $ showHelpOnError)+ (info (helper <*> p) (progDesc desc))
app/Config.hs view
@@ -1,17 +1,39 @@ module Config where -import qualified BytePatch.Linear.Patch+import GHC.Generics ( Generic )+import qualified StreamPatch.Patch.Binary as Bin data Config = Config- { cfgPatchCfg :: BytePatch.Linear.Patch.Cfg- , cfgPatchscript :: FilePath- , cfgStreamInOut :: CStreamInOut- } deriving (Eq, Show)+ { cfgPatchscript :: FilePath+ , cfgStreamPair :: CStreamPair+ , cfgPatchFormat :: CPatchFormat+ , cfgPrintBinary :: Bool+ } deriving (Eq, Show, Generic) +data CStreamPair = CStreamPair+ { cStreamPairIn :: CStream+ , cStreamPairOut :: CStream+ } deriving (Eq, Show, Generic)+ -- | "Single file" stream. data CStream = CStreamFile FilePath | CStreamStd- deriving (Eq, Show)+ deriving (Eq, Show, Generic) -newtype CStreamInOut = CStreamInOut { unCStreamInOut :: (CStream, CStream) } deriving (Eq, Show)+data CPatchFormat = CPatchFormat+ { cpfDataType :: CPatchDataType+ , cpfAlign :: CPatchAlign+ , cpfBinCfg :: Bin.Cfg+ } deriving (Eq, Show, Generic)++data CPatchAlign+ = CAlignPatch -- ^ Patch has alignment data.+ | CNoAlignPatch -- ^ Patch does not have alignment data.+ deriving (Eq, Show, Generic)++data CPatchDataType+ = CTextPatch -- ^ Patch 'Text' over a 'Text' stream.+ | CBinPatch -- ^ Patch bytestrings over a bytestream.+ | CTextBinPatch -- ^ Patch 'Text' over a bytestream.+ deriving (Eq, Show, Generic)
app/Main.hs view
@@ -1,79 +1,190 @@-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE AllowAmbiguousTypes #-} module Main ( main ) where import Config-import qualified Options+import qualified CLI -import BytePatch.Pretty-import BytePatch.Pretty.PatchRep-import BytePatch.JSON()-import qualified BytePatch.Linear.Patch as Linear-import qualified BytePatch.Linear.Gen as Linear+import StreamPatch.Patch+import qualified StreamPatch.Patch.Align as Align+import qualified StreamPatch.Patch.Linearize as Linear+import qualified StreamPatch.Patch.Binary as Bin+import StreamPatch.Patch.Binary ( BinRep )+import qualified StreamPatch.Apply as Apply+import BytePatch as BP+import qualified BytePatch.HexByteString as HexBS+import BytePatch.HexByteString ( HexByteString ) +import qualified System.Exit as System+import Numeric.Natural+import Data.Vinyl+import Data.Vinyl.TypeLevel+import Data.Functor.Const+import Data.Either.Combinators ( mapLeft ) import Control.Monad.IO.Class-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BL-import qualified Data.Yaml as Yaml-import Data.Aeson ( FromJSON )+import Control.Monad.Reader+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.Yaml as Yaml+import Data.Yaml ( FromJSON )+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Data.Text ( Text )+import Optics+import Data.Generics.Product.Any -import Data.Text ( Text )-import BytePatch.Pretty.HexByteString+type Bytes = BS.ByteString +data Error+ = ErrorYaml Yaml.ParseException+ | ErrorAlign String+ | ErrorBin String+ | ErrorLinear String+ | ErrorBinPatch String+ | Error String+ deriving (Show)+ main :: IO ()-main = Options.parse >>= run+main = CLI.parse >>= runReaderT run -run :: MonadIO m => Config -> m ()-run cfg = do- return ()- tryReadPatchscript @HexByteString (cfgPatchscript cfg) >>= \case- Nothing -> quit "couldn't parse patchscript"- Just ps ->- case normalizeSimple ps of- Nothing -> quit "failed while normalizing patchscript"- Just p ->- case Linear.gen p of- (_, genErrs@(_:_)) -> quit' "patchscript generation failed" genErrs- (ps', []) -> do- bs <- readStream' io- case Linear.patchPure (cfgPatchCfg cfg) ps' bs of- Left patchErr -> quit' "patching failed" patchErr- Right bs' -> writeStream' io (BL.toStrict bs')+run :: forall m. (MonadIO m, MonadReader Config m) => m ()+run = readPatchscript >>= processPatchScript >>= writePatched where- io = cfgStreamInOut cfg+ readPatchscript = asks cfgPatchscript >>= readStream . CStreamFile -readStream :: MonadIO m => CStream -> m BS.ByteString-readStream stream = liftIO $- case stream of+ processPatchScript psBs = do+ cpf <- asks cfgPatchFormat+ case cpfDataType cpf of+ CTextPatch -> processText psBs+ CBinPatch -> processBin' @HexByteString psBs+ CTextBinPatch -> processBin' @Text psBs++ processText :: Bytes -> m Bytes+ processText psBs = do+ cpf <- asks cfgPatchFormat+ case processAny @Text psBs (cpfAlign cpf) of+ Left err -> quit err+ Right ps -> patchTextAwkward ps++ processBin' :: forall a. (BinRep a, FromJSON a, Show a) => Bytes -> m Bytes+ processBin' psBs = do+ cpf <- asks cfgPatchFormat+ case processBin @a psBs (cpfAlign cpf) of+ Left err -> quit err+ Right ps -> patchBin ps++ patchBin ps = do+ bsIn <- asks (cStreamPairIn . cfgStreamPair) >>= readStream+ binCfg <- asks $ cpfBinCfg . cfgPatchFormat+ case Apply.runPureFwdBin binCfg ps bsIn of+ Left err -> quit $ ErrorBinPatch (showErrorBin err)+ Right bsOut -> return $ BL.toStrict bsOut++ patchTextAwkward :: [Patch 'FwdSeek '[] Text] -> m Bytes+ patchTextAwkward ps = do+ bsIn <- asks (cStreamPairIn . cfgStreamPair) >>= readStream+ let psStr = map (fmap Text.unpack) ps+ strIn = Text.unpack $ Text.decodeUtf8 bsIn+ strOut = Apply.runPureSimpleFwdList psStr strIn+ bsOut = Text.encodeUtf8 $ Text.pack strOut+ return bsOut++ writePatched bs =+ asks (cStreamPairOut . cfgStreamPair) >>= \case+ CStreamFile fp -> liftIO $ BS.writeFile fp bs+ CStreamStd -> asks (cpfDataType . cfgPatchFormat) >>= \case+ CTextPatch -> liftIO $ BS.putStr bs+ _ -> asks cfgPrintBinary >>= \case+ True -> liftIO $ BS.putStr bs+ False -> liftIO $ logWarn $+ "refusing to print binary to stdout:"+ <> " write to a file with --out-file FILE"+ <> " or use --print-binary flag to override"++logWarn :: MonadIO m => String -> m ()+logWarn msg = liftIO $ putStrLn $ "bytepatch: warning: " <> msg++-- | Parses your bytes into some binreppable patch form (aligned or not,+-- depending on flag), converts it to the internal patch form, binreps it, and+-- linearizes. The binrep type is consumed inside the function, making it+-- ambiguous, so you need to pass it a type via @processBin \@Text@.+--+-- Note that we linearize *after* binrepping. This is important -- it means the+-- seeks are byte offsets, not using the pre-binrepped type.+processBin+ :: forall a. (BinRep a, FromJSON a, Show a) => Bytes -> CPatchAlign+ -> Either Error [Patch 'FwdSeek '[Bin.MetaStream] Bytes]+processBin bs = \case+ CAlignPatch -> mapLeft ErrorYaml (Yaml.decodeEither' @[Aligned (MultiPatch 'RelSeek a)] bs)+ >>= mapLeft (ErrorAlign . show) . fmap concat . traverse (wrapAlign (BP.convertBinAlign @'AbsSeek))+ >>= mapLeft (ErrorBin . showErrorBin) . traverse Bin.patchBinRep+ >>= mapLeft (ErrorLinear . show) . Linear.linearize+ CNoAlignPatch -> mapLeft ErrorYaml (Yaml.decodeEither' @[MultiPatch 'AbsSeek a] bs)+ >>= mapLeft (ErrorBin . showErrorBin) . traverse Bin.patchBinRep . concat . map BP.convertBin+ >>= mapLeft (ErrorLinear . show) . Linear.linearize++-- | Like 'processBin' but no binrepping. Similarly beautiful and ambiguous.+processAny+ :: forall a. (Show a, FromJSON a, Linear.HasLength a) => Bytes -> CPatchAlign+ -> Either Error [Patch 'FwdSeek '[] a]+processAny bs = \case+ CAlignPatch -> mapLeft ErrorYaml (Yaml.decodeEither' @[Aligned (MultiPatch 'RelSeek a)] bs)+ >>= mapLeft (ErrorAlign . show) . fmap concat . traverse (wrapAlign (BP.convertAlign @'AbsSeek))+ >>= mapLeft (ErrorLinear . show) . Linear.linearize+ CNoAlignPatch -> mapLeft ErrorYaml (Yaml.decodeEither' @[MultiPatch 'AbsSeek a] bs)+ >>= mapLeft (ErrorLinear . show) . Linear.linearize . concat . map BP.convertEmpty++wrapAlign+ :: ( SeekRep s ~ Natural+ , r ~ Const (Align.Meta s)+ , rs ~ RDelete r ss+ , RElem r ss i+ , RSubset rs ss is )+ => (MultiPatch 'RelSeek a -> [Patch 'RelSeek ss a])+ -> Aligned (MultiPatch 'RelSeek a)+ -> Either (Align.Error s) [Patch s rs a]+wrapAlign f = align . over (the @"alignedPatches") (concat . map f)++quit :: MonadIO m => Error -> m a+quit err = do+ liftIO $ putStrLn $ "bytepatch: error: " <> errStr+ liftIO $ System.exitWith $ System.ExitFailure errExitCode+ where+ (errExitCode, errStr) = case err of+ ErrorYaml e -> (1, "while parsing YAML: " <> show e)+ ErrorAlign e -> (2, "while aligning: " <> e)+ ErrorBin e -> (3, "while converting to binary representation: " <> e)+ ErrorLinear e -> (4, "while linearizing: " <> e)+ ErrorBinPatch e -> (5, "while applying binaryized patch: " <> e)+ Error e -> (10, e)++readStream :: MonadIO m => CStream -> m Bytes+readStream s = liftIO $+ case s of CStreamStd -> BS.getContents CStreamFile fp -> BS.readFile fp -readStream' :: MonadIO m => CStreamInOut -> m BS.ByteString-readStream' = readStream . fst . unCStreamInOut+-- stupid but how else do I do this without pluggin HexByteString into Binary+-- guess I just gotta write ACTUAL error handling. ffs+showErrorBin :: Show a => Bin.Error a -> String+showErrorBin = \case+ Bin.ErrorBadBinRep a s -> "ErrorBadBinRep " <> show a <> " " <> s+ Bin.ErrorUnexpectedNonNull bs -> "ErrorUnexpectedNonNull " <> go bs+ Bin.ErrorDidNotMatchExpected bs1 bs2 -> "ErrorDidNotMatchExpected " <> go bs1 <> " " <> go bs2+ Bin.ErrorBinRepTooLong bs n -> "ErrorBinRepTooLong " <> go bs <> " " <> show n+ where go bs = "\"" <> Text.unpack (HexBS.prettyHexByteString bs) <> "\"" -writeStream :: MonadIO m => CStream -> BS.ByteString -> m ()-writeStream stream bs = liftIO $- case stream of+{-++writeStream :: MonadIO m => Bytes -> CStream -> m ()+writeStream bs s = liftIO $+ case s of CStreamStd -> BS.putStr bs CStreamFile fp -> BS.writeFile fp bs -writeStream' :: MonadIO m => CStreamInOut -> BS.ByteString -> m ()-writeStream' stream = writeStream (snd (unCStreamInOut stream))--quit' :: (MonadIO m, Show a) => String -> a -> m ()-quit' msg a = liftIO $ do- putStrLn $ "bytepatch: error: " <> msg- print a--quit :: MonadIO m => String -> m ()-quit = liftIO . putStrLn--tryReadPatchscript :: forall a m. (PatchRep a, FromJSON a, MonadIO m) => FilePath -> m (Maybe [CommonMultiEdits a])-tryReadPatchscript = tryDecodeYaml+tryDecodeYaml+ :: forall a m. (FromJSON a, MonadIO m)+ => FilePath -> m (Either Yaml.ParseException a)+tryDecodeYaml fp = Yaml.decodeEither' <$> liftIO (BS.readFile fp) -tryDecodeYaml :: (FromJSON a, MonadIO m) => FilePath -> m (Maybe a)-tryDecodeYaml fp = do- bs <- liftIO $ BS.readFile fp- case Yaml.decodeEither' bs of- Left _ -> return Nothing- Right parsed -> return $ Just parsed+-}
− app/Options.hs
@@ -1,39 +0,0 @@-module Options ( parse ) where--import Config-import Options.Applicative-import qualified BytePatch.Linear.Patch as Patch-import Control.Monad.IO.Class--parse :: MonadIO m => m Config-parse = execParserWithDefaults desc pConfig- where desc = "Patch bytestrings (TODO)."--pConfig :: Parser Config-pConfig = Config- <$> pPatchCfg- <*> strArgument (metavar "PATCHSCRIPT" <> help "Path to patchscript")- <*> pCStreamInOut--pPatchCfg :: Parser Patch.Cfg-pPatchCfg = Patch.Cfg <$> pAllowRepatch <*> pExpectExact- where- pAllowRepatch = switch $ long "allow-repatch" <> help "Override safety checks and only warn if it appears we're repatching a patched file (CURRENTLY NONFUNCTIONAL)"- pExpectExact = flag True False $ long "expect-exact" <> help "When checking expected bytes, require an exact match (rather than the expected being a prefix of the actual)"--pCStreamInOut :: Parser CStreamInOut-pCStreamInOut = CStreamInOut <$> liftA2 (,) pCSIn pCSOut- where- pCSIn = pFileArg <|> pStdin- pCSOut = pFileOpt <|> pure CStreamStd- pFileArg = CStreamFile <$> strArgument (metavar "FILE" <> help "Input file")- pFileOpt = CStreamFile <$> strOption (metavar "FILE" <> long "out-file" <> short 'o' <> help "Output file")- pStdin = flag' CStreamStd (long "stdin" <> help "Use stdin")-------------------------------------------------------------------------------------- | Execute a 'Parser' with decent defaults.-execParserWithDefaults :: MonadIO m => String -> Parser a -> m a-execParserWithDefaults desc p = liftIO $ customExecParser- (prefs $ showHelpOnError)- (info (helper <*> p) (progDesc desc))
bytepatch.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: bytepatch-version: 0.2.1+version: 0.3.0 synopsis: Patch byte-representable data in a bytestream. description: Please see README.md. category: CLI@@ -17,7 +17,7 @@ license-file: LICENSE build-type: Simple tested-with:- GHC >= 8.6 && < 9.2+ GHC >= 8.10 && < 9.2 extra-source-files: README.md CHANGELOG.md@@ -28,14 +28,16 @@ library exposed-modules:- BytePatch.Core- BytePatch.JSON- BytePatch.Linear.Gen- BytePatch.Linear.Patch- BytePatch.Pretty- BytePatch.Pretty.HexByteString- BytePatch.Pretty.PascalText- BytePatch.Pretty.PatchRep+ BytePatch+ BytePatch.HexByteString+ StreamPatch.Apply+ StreamPatch.Patch+ StreamPatch.Patch.Align+ StreamPatch.Patch.Binary+ StreamPatch.Patch.Binary.PascalText+ StreamPatch.Patch.Linearize+ StreamPatch.Stream+ Util other-modules: Paths_bytepatch hs-source-dirs:@@ -60,22 +62,30 @@ KindSignatures TypeOperators TypeApplications+ DataKinds+ TypeFamilies+ UndecidableInstances+ StandaloneKindSignatures+ RankNTypes+ ScopedTypeVariables build-depends: aeson >=1.5 && <2.1 , base >=4.12 && <4.17 , bytestring >=0.10 && <0.12+ , either >=5.0.1.1 && <5.1 , generic-optics >=2.1 && <2.4 , megaparsec >=9.0 && <9.3- , mtl ==2.2.*+ , mtl >=2.2.2 && <2.3 , optics >=0.3 && <0.5- , text ==1.2.*+ , text >=1.2.4.1 && <1.3+ , vinyl default-language: Haskell2010 executable bytepatch main-is: Main.hs other-modules:+ CLI Config- Options Paths_bytepatch hs-source-dirs: app@@ -99,18 +109,26 @@ KindSignatures TypeOperators TypeApplications+ DataKinds+ TypeFamilies+ UndecidableInstances+ StandaloneKindSignatures+ RankNTypes+ ScopedTypeVariables ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N build-depends: aeson >=1.5 && <2.1 , base >=4.12 && <4.17 , bytepatch , bytestring >=0.10 && <0.12+ , either >=5.0.1.1 && <5.1 , generic-optics >=2.1 && <2.4 , megaparsec >=9.0 && <9.3- , mtl ==2.2.*+ , mtl >=2.2.2 && <2.3 , optics >=0.3 && <0.5 , optparse-applicative >=0.16.1 && <0.17- , text ==1.2.*+ , text >=1.2.4.1 && <1.3+ , vinyl , yaml >=0.11.7 && <0.12 default-language: Haskell2010 @@ -118,7 +136,10 @@ type: exitcode-stdio-1.0 main-is: Spec.hs other-modules:- BytePatch.Pretty.HexByteStringSpec+ BytePatch.HexByteStringSpec+ StreamPatch.ApplySpec+ StreamPatch.Patch.AlignSpec+ StreamPatch.Patch.LinearizeSpec Util Paths_bytepatch hs-source-dirs:@@ -143,17 +164,26 @@ KindSignatures TypeOperators TypeApplications+ DataKinds+ TypeFamilies+ UndecidableInstances+ StandaloneKindSignatures+ RankNTypes+ ScopedTypeVariables build-tool-depends: hspec-discover:hspec-discover build-depends:- aeson >=1.5 && <2.1+ QuickCheck+ , aeson >=1.5 && <2.1 , base >=4.12 && <4.17 , bytepatch , bytestring >=0.10 && <0.12+ , either >=5.0.1.1 && <5.1 , generic-optics >=2.1 && <2.4 , hspec , megaparsec >=9.0 && <9.3- , mtl ==2.2.*+ , mtl >=2.2.2 && <2.3 , optics >=0.3 && <0.5- , text ==1.2.*+ , text >=1.2.4.1 && <1.3+ , vinyl default-language: Haskell2010
+ src/BytePatch.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++module BytePatch where++import StreamPatch.Patch+import qualified StreamPatch.Patch.Binary as Bin+import qualified StreamPatch.Patch.Align as Align++import Numeric.Natural+import Data.Functor.Const+import Data.Vinyl+import Data.Vinyl.TypeLevel+import Data.Aeson+import GHC.Generics ( Generic )++data MultiPatch s a = MultiPatch+ { mpData :: a+ , mpAt :: [Seek s a]+ } deriving (Generic)++deriving instance (Eq a, Eq (SeekRep s)) => Eq (MultiPatch s a)+deriving instance (Show a, Show (SeekRep s)) => Show (MultiPatch s a)++data Seek s a = Seek+ { sSeek :: SeekRep s+ , sExpected :: Maybe a+ , sNullTerminates :: Maybe (SeekRep 'FwdSeek)+ , sMaxBytes :: Maybe (SeekRep 'FwdSeek) -- TODO confirm meaning, maybe improve name+ , sAligned :: Maybe Natural+ } deriving (Generic)++deriving instance (Eq a, Eq (SeekRep s)) => Eq (Seek s a)+deriving instance (Show a, Show (SeekRep s)) => Show (Seek s a)++-- TODO write a Text newtype for Haskell-style negated integers -- allowing+-- negative hex integers!+data Aligned p = Aligned+ { alignedAlign :: SeekRep 'RelSeek+ , alignedPatches :: [p]+ } deriving (Eq, Show, Generic)++--------------------------------------------------------------------------------++jsonCfgCamelDrop :: Int -> Options+jsonCfgCamelDrop x = defaultOptions+ { fieldLabelModifier = camelTo2 '_' . drop x+ , rejectUnknownFields = False }++instance (ToJSON (SeekRep s), ToJSON a) => ToJSON (MultiPatch s a) where+ toJSON = genericToJSON $ jsonCfgCamelDrop 2+ toEncoding = genericToEncoding $ jsonCfgCamelDrop 2+instance (FromJSON (SeekRep s), FromJSON a) => FromJSON (MultiPatch s a) where+ parseJSON = genericParseJSON $ jsonCfgCamelDrop 2++instance (ToJSON (SeekRep s), ToJSON a) => ToJSON (Seek s a) where+ toJSON = genericToJSON $ jsonCfgCamelDrop 1+ toEncoding = genericToEncoding $ jsonCfgCamelDrop 1+instance (FromJSON (SeekRep s), FromJSON a) => FromJSON (Seek s a) where+ parseJSON = genericParseJSON $ jsonCfgCamelDrop 1++instance (ToJSON p) => ToJSON (Aligned p) where+ toJSON = genericToJSON $ jsonCfgCamelDrop 7+ toEncoding = genericToEncoding $ jsonCfgCamelDrop 7+instance (FromJSON p) => FromJSON (Aligned p) where+ parseJSON = genericParseJSON $ jsonCfgCamelDrop 7++--------------------------------------------------------------------------------++convert :: (Seek s a -> Rec (Flap a) fs) -> MultiPatch s a -> [Patch s fs a]+convert f p = map go (mpAt p)+ where go s = Patch { patchData = mpData p+ , patchSeek = sSeek s+ , patchMeta = FunctorRec $ f s }++-- TODO how to clean up (it's like liftA2 over n instead of 2. like a fold)+-- likely 'ap'!+convertBinAlign+ :: forall s a+ . SeekRep s ~ Natural+ => MultiPatch 'RelSeek a+ -> [Patch 'RelSeek '[Const (Align.Meta s), Const Bin.Meta, Bin.MetaStream] a]+convertBinAlign p = convert go p+ where go s = cmAlign s <+> cmBin s <+> cmBinStream s++convertBin+ :: forall s a+ . MultiPatch s a+ -> [Patch s '[Const Bin.Meta, Bin.MetaStream] a]+convertBin p = convert go p+ where go s = cmBin s <+> cmBinStream s++convertAlign+ :: forall s a+ . SeekRep s ~ Natural+ => MultiPatch 'RelSeek a+ -> [Patch 'RelSeek '[Const (Align.Meta s)] a]+convertAlign p = convert cmAlign p++convertEmpty+ :: forall s a+ . MultiPatch s a+ -> [Patch s '[] a]+convertEmpty p = convert (const RNil) p++--------------------------------------------------------------------------------++align+ :: forall s a ss rs is i r+ . ( SeekRep s ~ Natural+ , r ~ Const (Align.Meta s)+ , rs ~ RDelete r ss+ , RElem r ss i+ , RSubset rs ss is )+ => Aligned (Patch 'RelSeek ss a)+ -> Either (Align.Error s) [Patch s rs a]+align (Aligned a ps) = traverse (Align.align a) ps++--------------------------------------------------------------------------------++cmBin :: Seek s a -> Rec (Flap a) '[Const Bin.Meta]+cmBin s = cm $ Const $ Bin.Meta { Bin.mMaxBytes = sMaxBytes s }++cmBinStream :: Seek s a -> Rec (Flap a) '[Bin.MetaStream]+cmBinStream s = cm $ Bin.MetaStream+ { Bin.msNullTerminates = sNullTerminates s+ , Bin.msExpected = sExpected s }++cmAlign :: SeekRep s ~ Natural => Seek 'RelSeek a -> Rec (Flap a) '[Const (Align.Meta s)]+cmAlign s = cm $ Const $ Align.Meta { Align.mExpected = sAligned s }++cm :: f a -> Rec (Flap a) '[f]+cm fa = Flap fa :& RNil
− src/BytePatch/Core.hs
@@ -1,38 +0,0 @@-{-# LANGUAGE DataKinds, TypeFamilies, UndecidableInstances #-}--module BytePatch.Core where--import GHC.Generics ( Generic )-import GHC.Natural--data Patch (s :: SeekKind) a = Patch (SeekRep s) (Edit a)- deriving (Generic, Functor, Foldable, Traversable)--deriving instance (Eq (SeekRep s), Eq a) => Eq (Patch s a)-deriving instance (Show (SeekRep s), Show a) => Show (Patch s a)--data SeekKind- = FwdSeek -- ^ seeks only move cursor forward- | CursorSeek -- ^ seeks move cursor forward or backward- | AbsSeek -- ^ seeks specify an exact offset in stream- deriving (Eq, Show, Generic)--type family SeekRep (s :: SeekKind) where- SeekRep 'FwdSeek = Natural- SeekRep 'CursorSeek = Integer- SeekRep 'AbsSeek = Natural---- | Data to add to a stream.-data Edit a = Edit- { editData :: a- , editMeta :: EditMeta a- } deriving (Eq, Show, Generic, Functor, Foldable, Traversable)---- | Various optional metadata defining expected existing data for an 'Edit'.-data EditMeta a = EditMeta- { emNullTerminates :: Maybe Int- -- ^ Stream segment should be null bytes (0x00) only from this index onwards.-- , emExpected :: Maybe a- -- ^ Stream segment should be this.- } deriving (Eq, Show, Generic, Functor, Foldable, Traversable)
+ src/BytePatch/HexByteString.hs view
@@ -0,0 +1,70 @@+{- |+A 'Data.ByteString.ByteString' newtype wrapper indicating a human-readable+bytestring, to be displayed in hex form (e.g. 00 12 AB FF).+-}++module BytePatch.HexByteString+ ( HexByteString(..)+ , parseHexByteString+ , prettyHexByteString+ ) where++import StreamPatch.Patch.Binary ( BinRep(..) )++import Data.Void+import Text.Megaparsec+import qualified Text.Megaparsec.Char as MC+import qualified Data.ByteString as BS+import qualified Data.Char as Char+import Data.Word+import qualified Data.Text as Text+import Data.Text ( Text )+import Data.List as List+import Data.Aeson++type Bytes = BS.ByteString++newtype HexByteString = HexByteString { unHexByteString :: Bytes }+ deriving (Eq)++instance Show HexByteString where+ show = Text.unpack . prettyHexByteString . unHexByteString++instance BinRep HexByteString where+ toBinRep = Right . unHexByteString++instance FromJSON HexByteString where+ parseJSON = withText "hex bytestring" $ \t ->+ case parseMaybe @Void parseHexByteString t of+ Nothing -> fail "failed to parse hex bytestring (TODO)"+ Just t' -> pure (HexByteString t')+instance ToJSON HexByteString where+ toJSON = String . prettyHexByteString . unHexByteString++-- | A hex bytestring looks like this: @00 01 89 8a FEff@. You can mix and+-- match capitalization and spacing, but I prefer to space each byte, full caps.+parseHexByteString :: (MonadParsec e s m, Token s ~ Char) => m Bytes+parseHexByteString = BS.pack <$> parseHexByte `sepBy` MC.hspace++-- | Parse a byte formatted as two hex digits e.g. EF. You _must_ provide both+-- nibbles e.g. @0F@, not @F@. They cannot be spaced e.g. @E F@ is invalid.+--+-- Returns a value 0-255, so can fit in any Num type that can store that.+parseHexByte :: (MonadParsec e s m, Token s ~ Char, Num a) => m a+parseHexByte = do+ c1 <- MC.hexDigitChar+ c2 <- MC.hexDigitChar+ return $ 0x10 * fromIntegral (Char.digitToInt c1) + fromIntegral (Char.digitToInt c2)++prettyHexByteString :: Bytes -> Text+prettyHexByteString =+ Text.concat . List.intersperse (Text.singleton ' ') . fmap (f . prettyHexByte) . BS.unpack+ where+ f :: (Char, Char) -> Text+ f (c1, c2) = Text.cons c1 $ Text.singleton c2++prettyHexByte :: Word8 -> (Char, Char)+prettyHexByte w = (prettyNibble h, prettyNibble l)+ where+ (h,l) = fromIntegral w `divMod` 0x10+ prettyNibble = Char.toUpper . Char.intToDigit
− src/BytePatch/JSON.hs
@@ -1,55 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# LANGUAGE UndecidableInstances #-}---- | Aeson instances for various types.-module BytePatch.JSON where--import BytePatch.Core-import qualified BytePatch.Pretty as Pretty-import BytePatch.Pretty.HexByteString-import Data.Aeson-import Text.Megaparsec-import Data.Void--instance FromJSON HexByteString where- parseJSON = withText "hex bytestring" $ \t ->- case parseMaybe @Void parseHexByteString t of- Nothing -> fail "failed to parse hex bytestring (TODO)"- Just t' -> pure (HexByteString t')-instance ToJSON HexByteString where- toJSON = String . prettyHexByteString . unHexByteString--jsonCfgCamelDrop :: Int -> Options-jsonCfgCamelDrop x = defaultOptions- { fieldLabelModifier = camelTo2 '_' . drop x- , rejectUnknownFields = True }--instance ToJSON a => ToJSON (Pretty.CommonMultiEdits a) where- toJSON = genericToJSON $ jsonCfgCamelDrop 4- toEncoding = genericToEncoding $ jsonCfgCamelDrop 4-instance FromJSON a => FromJSON (Pretty.CommonMultiEdits a) where- parseJSON = genericParseJSON $ jsonCfgCamelDrop 4--instance (ToJSON (SeekRep s), ToJSON a) => ToJSON (Pretty.MultiEdit s a) where- toJSON = genericToJSON $ jsonCfgCamelDrop 2- toEncoding = genericToEncoding $ jsonCfgCamelDrop 2-instance (FromJSON (SeekRep s), FromJSON a) => FromJSON (Pretty.MultiEdit s a) where- parseJSON = genericParseJSON $ jsonCfgCamelDrop 2--instance (ToJSON (SeekRep s), ToJSON a) => ToJSON (Pretty.EditOffset s a) where- toJSON = genericToJSON $ jsonCfgCamelDrop 2- toEncoding = genericToEncoding $ jsonCfgCamelDrop 2-instance (FromJSON (SeekRep s), FromJSON a) => FromJSON (Pretty.EditOffset s a) where- parseJSON = genericParseJSON $ jsonCfgCamelDrop 2--instance ToJSON a => ToJSON (EditMeta a) where- toJSON = genericToJSON $ jsonCfgCamelDrop 2- toEncoding = genericToEncoding $ jsonCfgCamelDrop 2-instance FromJSON a => FromJSON (EditMeta a) where- parseJSON = genericParseJSON $ jsonCfgCamelDrop 2--instance ToJSON a => ToJSON (Edit a) where- toJSON = genericToJSON defaultOptions- toEncoding = genericToEncoding defaultOptions-instance FromJSON a => FromJSON (Edit a) where- parseJSON = genericParseJSON defaultOptions
− src/BytePatch/Linear/Gen.hs
@@ -1,59 +0,0 @@-{-# LANGUAGE DataKinds, TypeFamilies, UndecidableInstances #-}--module BytePatch.Linear.Gen (gen, Error(..)) where--import BytePatch.Core--import qualified Data.ByteString as BS-import Control.Monad.State-import qualified Data.List as List-import GHC.Natural--type Bytes = BS.ByteString---- | Error encountered during linear patchscript generation.-data Error s a- = ErrorOverlap (Patch s a) (Patch s a)- -- ^ Two edits wrote to the same offset.--deriving instance (Eq (SeekRep s), Eq a) => Eq (Error s a)-deriving instance (Show (SeekRep s), Show a) => Show (Error s a)---- | Process a list of patches into a linear patch script.------ Errors are reported, but do not interrupt patch generation. The user could--- discard patchscripts that errored, or perhaps attempt to recover them. This--- is what we do for errors:------ * overlapping edit: later edit is skipped & overlapping edits reported-gen- :: [Patch 'AbsSeek Bytes]- -> ([Patch 'FwdSeek Bytes], [Error 'AbsSeek Bytes])-gen pList =- let pList' = List.sortBy comparePatchOffsets pList- (_, script, errors, _) = execState (go pList') (0, [], [], undefined)- -- I believe the undefined is inaccessible providing the first patch has- -- a non-negative offset (negative offsets are forbidden)- in (reverse script, reverse errors)- where- comparePatchOffsets (Patch o1 _) (Patch o2 _) = compare o1 o2- go- :: (MonadState (Natural, [Patch 'FwdSeek Bytes], [Error 'AbsSeek Bytes], Patch 'AbsSeek Bytes) m)- => [Patch 'AbsSeek Bytes]- -> m ()- go [] = return ()- go (p@(Patch offset edit) : ps) = do- (cursor, script, errors, prevPatch) <- get- case offset `minusNaturalMaybe` cursor of- -- next offset is behind current cursor: overlapping patches- -- record error, recover via dropping patch- Nothing -> do- let e = ErrorOverlap p prevPatch- let errors' = e : errors- put (cursor, script, errors', p)- go ps- Just skip -> do- let dataLen = fromIntegral $ BS.length $ editData edit- let cursor' = cursor + skip + dataLen- put (cursor', Patch skip edit : script, errors, p)- go ps
− src/BytePatch/Linear/Patch.hs
@@ -1,146 +0,0 @@-{-# LANGUAGE DataKinds, TypeFamilies #-}--{- | Low-level patchscript processing and application.--Patchscripts are applied as a list of @(skip x, write in-place y)@ commands. An-offset-based format is much simpler to use, however. This module processes such-offset patchscripts into a "linear" patchscript, and provides a stream patching-algorithm that can be applied to any forward-seeking byte stream.--Some core types are parameterized over the stream type/patch content. This-enables writing patches in any form (e.g. UTF-8 text), which are then processed-into an applicable patch by transforming edits into a concrete binary-representation (e.g. null-terminated UTF-8 bytestring). See TODO module for-more.--}--module BytePatch.Linear.Patch- (- -- * Patch interface- MonadFwdByteStream(..)- , Cfg(..)- , Error(..)-- -- * Prepared patchers- , patchPure-- -- * General patcher- , patch-- ) where--import BytePatch.Core--import GHC.Natural-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Builder as BB-import Control.Monad.State-import Control.Monad.Reader-import System.IO ( Handle, SeekMode(..), hSeek )-import Optics--type Bytes = BS.ByteString---- TODO also require reporting cursor position (for error reporting)-class Monad m => MonadFwdByteStream m where- -- | Read a number of bytes without advancing the cursor.- readahead :: Natural -> m Bytes-- -- | Advance cursor without reading.- advance :: Natural -> m ()-- -- | Insert bytes into the stream at the cursor position, overwriting- -- existing bytes.- overwrite :: Bytes -> m ()--instance Monad m => MonadFwdByteStream (StateT (Bytes, BB.Builder) m) where- readahead n = BS.take (fromIntegral n) <$> gets fst- advance n = do- (src, out) <- get- let (bs, src') = BS.splitAt (fromIntegral n) src- put (src', out <> BB.byteString bs)- overwrite bs = do- (src, out) <- get- let (_, src') = BS.splitAt (BS.length bs) src- put (src', out <> BB.byteString bs)--instance MonadIO m => MonadFwdByteStream (ReaderT Handle m) where- readahead n = do- hdl <- ask- bs <- liftIO $ BS.hGet hdl (fromIntegral n)- liftIO $ hSeek hdl RelativeSeek (- fromIntegral n)- return bs- advance n = do- hdl <- ask- liftIO $ hSeek hdl RelativeSeek (fromIntegral n)- overwrite bs = do- hdl <- ask- liftIO $ BS.hPut hdl bs---- | Patch time config.-data Cfg = Cfg- { cfgWarnIfLikelyReprocessing :: Bool- -- ^ If we determine that we're repatching an already-patched stream, continue- -- with a warning instead of failing.-- , 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)---- | Errors encountered during patch time.-data Error- = ErrorPatchOverlong- | ErrorPatchUnexpectedNonnull- | ErrorPatchDidNotMatchExpected Bytes Bytes- deriving (Eq, Show)--patch- :: MonadFwdByteStream m- => Cfg -> [Patch 'FwdSeek Bytes]- -> m (Maybe Error)-patch cfg = go- where- go [] = return Nothing- go (Patch n (Edit bs meta):es) = do- advance n- bsStream <- readahead $ fromIntegral $ BS.length bs -- TODO catch overlong error-- -- if provided, strip trailing nulls from to-overwrite bytestring- case tryStripNulls bsStream (emNullTerminates meta) of- Nothing -> return $ Just ErrorPatchUnexpectedNonnull- Just bsStream' -> do-- -- if provided, check the to-overwrite bytestring matches expected- case checkExpected bsStream' (emExpected meta) of- Just (bsa, bse) -> return $ Just $ ErrorPatchDidNotMatchExpected bsa bse- Nothing -> overwrite bs >> go es-- tryStripNulls bs = \case- Nothing -> Just bs- Just nullsFrom ->- let (bs', bsNulls) = BS.splitAt nullsFrom bs- in if bsNulls == BS.replicate (BS.length bsNulls) 0x00- then Just bs'- else Nothing-- checkExpected bs = \case- Nothing -> Nothing- Just bsExpected ->- case cfgAllowPartialExpected cfg of- True -> if BS.isPrefixOf bs bsExpected- then Nothing- else Just (bs, bsExpected)- False -> if bs == bsExpected- then Nothing- else Just (bs, bsExpected)---- | Attempt to apply a patchscript to a 'Data.ByteString.ByteString'.-patchPure :: Cfg -> [Patch 'FwdSeek Bytes] -> BS.ByteString -> Either Error BL.ByteString-patchPure cfg ps bs =- let (mErr, (bsRemaining, bbPatched)) = runState (patch cfg ps) (bs, mempty)- bbPatched' = bbPatched <> BB.byteString bsRemaining- in case mErr of- Just err -> Left err- Nothing -> Right $ BB.toLazyByteString bbPatched'
− src/BytePatch/Pretty.hs
@@ -1,166 +0,0 @@-{-# LANGUAGE DataKinds, TypeFamilies, UndecidableInstances #-}--{-|-Convenience interface to enable defining edits at offsets with some optional-safety checks.--Redefines some types to enable us to easily leverage Aeson's generic JSON schema-deriving. That sadly means we can't use some of the interesting offset plumbing.--TODO I should definitely bite the bullet and use the plumbing now that it's so-cool.--}--module BytePatch.Pretty- (- -- * Core types- CommonMultiEdits(..)- , MultiEdit(..)- , EditOffset(..)-- -- * Convenience functions- , normalizeSimple-- -- * Low-level interface- , applyBaseOffset- , listAlgebraConcatEtc- , normalize- ) where--import BytePatch.Core-import BytePatch.Pretty.PatchRep--import qualified Data.ByteString as BS-import Data.Maybe ( fromMaybe )-import GHC.Generics ( Generic )-import GHC.Natural--type Bytes = BS.ByteString---- | A list of 'MultiEdit's with some common configuration.-data CommonMultiEdits a = CommonMultiEdits- { cmesBaseOffset :: Maybe (SeekRep 'CursorSeek)- -- ^- -- The base offset from which all offsets are located. An actual offset is- -- calculated by adding the base offset to an offset. Actual offsets below 0- -- are invalid, meaning for an offset @o@ with base offset @bo@, the actual- -- offset is only valid when @o >= bo@. Negative base offsets are allowed.-- , cmesEdits :: [MultiEdit 'CursorSeek a]- } deriving (Eq, Show, Generic, Functor, Foldable, Traversable)---- | A single edit to be applied at a list of offsets.-data MultiEdit (s :: SeekKind) a = MultiEdit- { meData :: a -- ^ The value (e.g. bytes, text) to add.- , meAt :: [EditOffset s a] -- ^ Offsets to apply edit at.- } deriving (Generic, Functor, Foldable, Traversable)--deriving instance (Eq (SeekRep s), Eq a) => Eq (MultiEdit s a)-deriving instance (Show (SeekRep s), Show a) => Show (MultiEdit s a)---- | An edit offset, with metadata to use for preparing and applying the edit.-data EditOffset (s :: SeekKind) a = EditOffset- { eoOffset :: SeekRep s- -- ^ Stream offset for edit.-- , eoAbsOffset :: Maybe (SeekRep 'AbsSeek)- -- ^ Absolute stream offset for edit. Used for checking against actual offset.-- , eoMaxLength :: Maybe Natural- -- ^ Maximum number of bytes allowed to write at this offset.-- , eoEditMeta :: Maybe (EditMeta a)- -- ^ Optional apply time metadata for the edit at this offset.-- } deriving (Generic, Functor, Foldable, Traversable)--deriving instance (Eq (SeekRep s), Eq a) => Eq (EditOffset s a)-deriving instance (Show (SeekRep s), Show a) => Show (EditOffset s a)---- | Normalize a list of 'CommonMultiEdits's, discarding everything on error.-normalizeSimple :: PatchRep a => [CommonMultiEdits a] -> Maybe [Patch 'AbsSeek Bytes]-normalizeSimple cmess =- let (p, errs) = listAlgebraConcatEtc . map applyBaseOffset $ cmess- in case errs of- _:_ -> Nothing- [] -> normalize p---- Drops no info, not easy to consume.-applyBaseOffset- :: CommonMultiEdits a- -> (Integer, [(MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])])-applyBaseOffset cmes =- (baseOffset, recalculateMultiPatchOffsets baseOffset (cmesEdits cmes))- where baseOffset = fromMaybe 0 (cmesBaseOffset cmes)---- lmao this sucks. generalisation bad-listAlgebraConcatEtc :: [(a, [(b, [c])])] -> ([b], [(c, a)])-listAlgebraConcatEtc = mconcat . map go- where- go (baseOffset, inps) = tuplemconcat (map (go' baseOffset) inps)- go' x (mp, offs) = (mp, map (\o -> (o, x)) offs)- tuplemconcat = foldr (\(a, bs) (as, bs') -> (a:as, bs <> bs')) ([], mempty)--recalculateMultiPatchOffsets- :: Integer- -> [MultiEdit 'CursorSeek a]- -> [(MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])]-recalculateMultiPatchOffsets baseOffset = map go- where- go :: MultiEdit 'CursorSeek a -> (MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])- go me =- let (osRecalculated, osInvalid) = recalculateOffsets baseOffset (meAt me)- in (me { meAt = osRecalculated }, osInvalid)--recalculateOffsets- :: Integer- -> [EditOffset 'CursorSeek a]- -> ([EditOffset 'AbsSeek a], [EditOffset 'CursorSeek a])-recalculateOffsets baseOffset = partitionMaybe go- where- go o = let actualOffset = baseOffset + eoOffset o- in case tryIntegerToNatural actualOffset of- Nothing -> Nothing- Just actualOffset' -> Just $ o { eoOffset = actualOffset' }--tryIntegerToNatural :: Integer -> Maybe Natural-tryIntegerToNatural n | n < 0 = Nothing- | otherwise = Just $ naturalFromInteger n--normalize :: PatchRep a => [MultiEdit 'AbsSeek a] -> Maybe [Patch 'AbsSeek Bytes]-normalize xs = concat <$> mapM go xs- where go (MultiEdit contents os) = mapM (tryMakeSingleReplace contents) os---- TODO now can error with "[expected] content has no valid patch rep"-tryMakeSingleReplace :: PatchRep a => a -> EditOffset 'AbsSeek a -> Maybe (Patch 'AbsSeek Bytes)-tryMakeSingleReplace contents (EditOffset os maos mMaxLen mMeta) =- case toPatchRep contents of- Left errStr -> error errStr -- TODO- Right bs ->- if offsetIsCorrect- then case mMaxLen of- Just maxLen -> if BS.length bs > fromIntegral maxLen then Nothing else overwrite bs- Nothing -> overwrite bs- else Nothing- where- overwrite bs = case traverse toPatchRep meta of- Left errStr -> error errStr -- TODO- Right meta' ->- Just $ Patch os $ Edit { editData = bs- , editMeta = meta' }- meta = fromMaybe (EditMeta Nothing Nothing) mMeta- offsetIsCorrect = case maos of Nothing -> True- Just aos -> os == aos-------------------------------------------------------------------------------------- | Map a failable function over a list, retaining "failed" 'Nothing' results.-partitionMaybe :: (a -> Maybe b) -> [a] -> ([b], [a])-partitionMaybe f =- foldr (\x -> maybe (mapSnd (x:)) (\y -> mapFst (y:)) (f x)) ([], [])--mapFst :: (a -> c) -> (a, b) -> (c, b)-mapFst f (a, b) = (f a, b)--mapSnd :: (b -> c) -> (a, b) -> (a, c)-mapSnd f (a, b) = (a, f b)
− src/BytePatch/Pretty/HexByteString.hs
@@ -1,62 +0,0 @@-{- |-A 'Data.ByteString.ByteString' newtype wrapper indicating a human-readable-bytestring, to be displayed in hex form (e.g. 00 12 AB FF).--}--{-# LANGUAGE TypeFamilies #-}--module BytePatch.Pretty.HexByteString- ( HexByteString(..)- , parseHexByteString- , prettyHexByteString- ) where--import BytePatch.Pretty.PatchRep--import Text.Megaparsec-import qualified Text.Megaparsec.Char as MC-import qualified Data.ByteString as BS-import qualified Data.Char as Char-import Data.Word-import qualified Data.Text as Text-import Data.Text ( Text )-import Data.List as List--type Bytes = BS.ByteString--newtype HexByteString = HexByteString { unHexByteString :: Bytes }- deriving (Eq)--instance Show HexByteString where- show = Text.unpack . prettyHexByteString . unHexByteString--instance PatchRep HexByteString where- toPatchRep = Right . unHexByteString---- | A hex bytestring looks like this: @00 01 89 8a FEff@. You can mix and--- match capitalization and spacing, but I prefer to space each byte, full caps.-parseHexByteString :: (MonadParsec e s m, Token s ~ Char) => m Bytes-parseHexByteString = BS.pack <$> parseHexByte `sepBy` MC.hspace---- | Parse a byte formatted as two hex digits e.g. EF. You _must_ provide both--- nibbles e.g. @0F@, not @F@. They cannot be spaced e.g. @E F@ is invalid.------ Returns a value 0-255, so can fit in any Num type that can store that.-parseHexByte :: (MonadParsec e s m, Token s ~ Char, Num a) => m a-parseHexByte = do- c1 <- MC.hexDigitChar- c2 <- MC.hexDigitChar- return $ 0x10 * fromIntegral (Char.digitToInt c1) + fromIntegral (Char.digitToInt c2)--prettyHexByteString :: Bytes -> Text-prettyHexByteString =- Text.concat . List.intersperse (Text.singleton ' ') . fmap (f . prettyHexByte) . BS.unpack- where- f :: (Char, Char) -> Text- f (c1, c2) = Text.cons c1 $ Text.singleton c2--prettyHexByte :: Word8 -> (Char, Char)-prettyHexByte w = (prettyNibble h, prettyNibble l)- where- (h,l) = fromIntegral w `divMod` 0x10- prettyNibble = Char.toUpper . Char.intToDigit
− src/BytePatch/Pretty/PascalText.hs
@@ -1,56 +0,0 @@-{-| Newtype for manipulating length-prefixed strings.--This type is for UTF-8 'Text' that you intend to write out to a length-prefixed-bytestring. The size of the length field is static. You essentially have to-decide the maximum bytesize of the string on creation.--}--{-# LANGUAGE DataKinds, ScopedTypeVariables#-}--module BytePatch.Pretty.PascalText where--import BytePatch.Pretty.PatchRep--import qualified Data.Text.Encoding as Text-import Data.Text ( Text )-import qualified Data.ByteString as BS-import GHC.TypeLits-import Data.Proxy-import Data.Bits--newtype PascalText (n :: Nat) = PascalText { unPascalText :: Text }--instance KnownNat n => PatchRep (PascalText n) where- toPatchRep t =- case encodePascalText t of- Nothing -> Left "UTF-8 encoded text too long for length prefix field"- Just bs -> Right bs--encodePascalText :: forall n. KnownNat n => PascalText n -> Maybe BS.ByteString-encodePascalText t = do- lenBs <- encodeToSizedBE (fromIntegral (natVal (Proxy @n))) (BS.length bs)- return $ lenBs <> bs- where- bs = Text.encodeUtf8 . unPascalText $ t--encodeToSizedBE :: (Integral a, Bits a) => Int -> a -> Maybe BS.ByteString-encodeToSizedBE byteSize x =- let bs = i2be x- nulls = byteSize - BS.length bs- in if nulls < 0- then Nothing- else Just $ BS.replicate nulls 0x00 <> bs---- | Re-encode an 'Integer' to a little-endian integer stored as a 'ByteString'--- using the fewest bytes needed to represent it.------ adapated from crypto-api 0.13.3, Crypto.Util.i2bs_unsized-i2be :: (Integral a, Bits a) => a -> BS.ByteString-i2be 0 = BS.singleton 0-i2be i = BS.reverse $ BS.unfoldr (\i' -> if i' <= 0 then Nothing else Just (fromIntegral i', (i' `shiftR` 8))) i---- 0 -> 255 = 1--- 256 -> 65535 = 2--ptLengthBytes :: forall n. KnownNat n => PascalText n -> Integer-ptLengthBytes _ = natVal (Proxy @n)
− src/BytePatch/Pretty/PatchRep.hs
@@ -1,29 +0,0 @@-module BytePatch.Pretty.PatchRep where--import qualified Data.ByteString as BS-import qualified Data.Text.Encoding as Text-import Data.Text ( Text )---- | 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 PatchRep a where- toPatchRep :: a -> Either String BS.ByteString---- | Bytestrings are copied as-is.-instance PatchRep BS.ByteString where- toPatchRep = Right . id---- | Text is converted to UTF-8 bytes and null-terminated.-instance PatchRep Text where- --toPatchRep t = BS.snoc (Text.encodeUtf8 t) 0x00- toPatchRep = Right . flip BS.snoc 0x00 . Text.encodeUtf8
+ src/StreamPatch/Apply.hs view
@@ -0,0 +1,60 @@+module StreamPatch.Apply where++import StreamPatch.Stream+import StreamPatch.Patch+import qualified StreamPatch.Patch.Binary as Bin+import StreamPatch.Patch.Binary ( BinRep )++import Data.Vinyl+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Lazy as BL+import Control.Monad.State+import Util++-- TODO how to clean up, use Either monad inside m? (lift didn't work)+applyBinFwd+ :: forall a m. (MonadFwdStream m, Chunk m ~ BS.ByteString, BinRep a)+ => Bin.Cfg+ -> [Patch 'FwdSeek '[Bin.MetaStream] a]+ -> m (Either (Bin.Error a) ())+applyBinFwd cfg = traverseM_ $ \(Patch a s (FunctorRec (Flap m :& RNil))) -> do+ case Bin.toBinRep' a of+ Left err -> return $ Left err+ Right bs -> do+ advance s+ bsStream <- readahead $ fromIntegral $ BS.length bs+ case Bin.check cfg bsStream m of+ Left err -> return $ Left err+ Right () -> do+ overwrite bs+ return $ Right ()++runPureFwdBin+ :: BinRep a+ => Bin.Cfg+ -> [Patch 'FwdSeek '[Bin.MetaStream] a]+ -> BS.ByteString+ -> Either (Bin.Error a) BL.ByteString+runPureFwdBin cfg ps bs =+ let (mErr, (bsRemaining, bbPatched)) = runState (applyBinFwd cfg ps) (bs, mempty)+ bbPatched' = bbPatched <> BB.byteString bsRemaining+ in case mErr of+ Left err -> Left err+ Right () -> Right $ BB.toLazyByteString bbPatched'++applySimpleFwd+ :: (MonadFwdStream m, Chunk m ~ a)+ => [Patch 'FwdSeek '[] a]+ -> m ()+applySimpleFwd =+ mapM_ $ \(Patch a s (FunctorRec RNil)) -> advance s >> overwrite a++-- stupid because no monotraversable :<+runPureSimpleFwdList+ :: [Patch 'FwdSeek '[] [a]]+ -> [a]+ -> [a]+runPureSimpleFwdList ps start =+ let ((), (remaining, patched)) = runState (applySimpleFwd ps) (start, mempty)+ in patched <> remaining
+ src/StreamPatch/Patch.hs view
@@ -0,0 +1,79 @@+-- | Core patch type definitions: patches, seeks, metadata.++module StreamPatch.Patch where++import Data.Kind+import GHC.Generics ( Generic )+import GHC.Natural+import Data.Vinyl+import Control.Applicative ( liftA2 )++type PatchLike = SeekKind -> [Type -> Type] -> Type -> Type+type Patch :: PatchLike+data Patch s fs a = Patch+ { patchData :: a+ , patchSeek :: SeekRep s+ , patchMeta :: FunctorRec fs a+ } deriving (Generic)++deriving instance (Eq a, Eq (SeekRep s), Eq (Rec (Flap a) fs)) => Eq (Patch s fs a)+deriving instance (Show a, Show (SeekRep s), ReifyConstraint Show (Flap a) fs, RMap fs, RecordToList fs) => Show (Patch s fs a)+deriving instance (Functor (FunctorRec fs)) => Functor (Patch s fs)++-- Taken from vinyl-plus. Functor and Applicative instances were provided.+newtype Flap a f = Flap { getFlap :: f a } deriving (Eq, Show, Generic)+newtype FunctorRec fs a = FunctorRec { getFunctorRec :: Rec (Flap a) fs } deriving (Generic)+deriving instance (ReifyConstraint Show (Flap a) fs, RMap fs, RecordToList fs) => Show (FunctorRec fs a)+deriving instance Eq (Rec (Flap a) fs) => Eq (FunctorRec fs a)++instance Functor (FunctorRec '[]) where+ fmap _ (FunctorRec RNil) = FunctorRec RNil+instance (Functor r, Functor (FunctorRec rs)) => Functor (FunctorRec (r ': rs)) where+ fmap f (FunctorRec (Flap r :& rs)) =+ FunctorRec (Flap (fmap f r) :& getFunctorRec (fmap f (FunctorRec rs)))++instance Applicative (FunctorRec '[]) where+ pure _ = FunctorRec RNil+ FunctorRec RNil <*> FunctorRec RNil = FunctorRec RNil+instance (Applicative r, Applicative (FunctorRec rs)) => Applicative (FunctorRec (r ': rs)) where+ pure a = FunctorRec (Flap (pure a) :& getFunctorRec (pure a))+ FunctorRec (Flap f :& fs) <*> FunctorRec (Flap a :& as) =+ FunctorRec (Flap (f <*> a) :& getFunctorRec (FunctorRec fs <*> FunctorRec as))++instance Foldable (FunctorRec '[]) where+ foldr _ z (FunctorRec RNil) = z+instance (Foldable r, Foldable (FunctorRec rs)) => Foldable (FunctorRec (r ': rs)) where+ -- TODO foldr is harder lol+ foldMap f (FunctorRec (Flap r :& rs)) = foldMap f r <> foldMap f (FunctorRec rs)++-- I am shit at this LOL+instance Traversable (FunctorRec '[]) where+ traverse _ (FunctorRec RNil) = pure (FunctorRec RNil)+instance (Traversable r, Traversable (FunctorRec rs)) => Traversable (FunctorRec (r ': rs)) where+ traverse+ :: forall f a b. Applicative f+ => (a -> f b)+ -> (FunctorRec (r ': rs)) a+ -> f (FunctorRec (r ': rs) b)+ traverse f (FunctorRec (Flap (r :: r a) :& rs)) =+ FunctorRec <$> rBoth+ where+ rBoth :: f (Rec (Flap b) (r ': rs))+ rBoth = liftA2 (:&) rHead rTail+ rHead :: f (Flap b r)+ rHead = Flap <$> traverse f r+ rTail :: f (Rec (Flap b) rs)+ rTail = getFunctorRec <$> traverse f (FunctorRec rs)++-- | What a patch seek value means.+data SeekKind+ = FwdSeek -- ^ seeks only move cursor forward+ | RelSeek -- ^ seeks are relative e.g. to a universal base, or a stream cursor+ | AbsSeek -- ^ seeks specify an exact offset in stream+ deriving (Eq, Show, Generic)++-- | Get the representation for a 'SeekKind'. Allows us a bit more safety.+type family SeekRep (s :: SeekKind) where+ SeekRep 'FwdSeek = Natural+ SeekRep 'RelSeek = Integer+ SeekRep 'AbsSeek = Natural
+ src/StreamPatch/Patch/Align.hs view
@@ -0,0 +1,59 @@+module StreamPatch.Patch.Align where++import StreamPatch.Patch++import GHC.Generics ( Generic )+import Numeric.Natural+import GHC.Natural ( naturalFromInteger )+import Data.Vinyl+import Data.Vinyl.TypeLevel+import Data.Functor.Const++data Meta s = Meta+ { mExpected :: Maybe (SeekRep s)+ -- ^ Absolute stream offset for edit. Used for checking against actual offset.+ } deriving (Generic)++deriving instance (Eq (SeekRep s)) => Eq (Meta s)+deriving instance (Show (SeekRep s)) => Show (Meta s)++data Error s+ = ErrorSeekBelow0 (SeekRep 'RelSeek)+ | ErrorDoesntMatchExpected (SeekRep s) (SeekRep s) -- expected, then actual+ deriving (Generic)++deriving instance (Eq (SeekRep s)) => Eq (Error s)+deriving instance (Show (SeekRep s)) => Show (Error s)++-- | Attempt to align the given patch to 0 using the given base.+align+ :: forall s a ss rs is i r+ . ( SeekRep s ~ Natural+ , r ~ Const (Meta s)+ , rs ~ RDelete r ss+ , RElem r ss i+ , RSubset rs ss is )+ => SeekRep 'RelSeek+ -> Patch 'RelSeek ss a+ -> Either (Error s) (Patch s rs a)+align sBase (Patch a s ms) =+ case tryIntegerToNatural sAligned of+ Nothing -> Left $ ErrorSeekBelow0 sAligned+ Just sAlignedNat ->+ case mExpected m of+ Nothing -> reform sAlignedNat+ Just sExpected ->+ if sExpected == sAlignedNat+ then reform sAlignedNat+ else Left $ ErrorDoesntMatchExpected sExpected sAlignedNat+ where+ sAligned = sBase + s+ -- TODO require a visible type application here, unsure why. even without+ -- the SeekRep indirection+ m = getConst @(Meta s) $ getFlap $ rget $ getFunctorRec ms+ ms' = FunctorRec $ rcast @rs $ getFunctorRec ms+ reform s' = Right $ Patch a s' ms'++tryIntegerToNatural :: Integer -> Maybe Natural+tryIntegerToNatural n | n < 0 = Nothing+ | otherwise = Just $ naturalFromInteger n
+ src/StreamPatch/Patch/Binary.hs view
@@ -0,0 +1,181 @@+{-# LANGUAGE RecordWildCards #-}++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++type Bytes = BS.ByteString++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 Bytes+ | ErrorDidNotMatchExpected Bytes Bytes+ | ErrorBinRepTooLong Bytes 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 Bytes)+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+ toBinRep' x = mapLeft (\e -> ErrorBadBinRep x e) $ toBinRep x+ m = getConst @Meta $ getFlap $ rget $ getFunctorRec ms++{-++--checkBinRep :: BinRep => a -> Either String Bytes++binRep :: BinRep a => a -> Maybe Natural -> Either (Error a) Bytes+binRep a mN =+ case toBinRep a of+ Left err -> Left $ ErrorBadBinRep a err+ Right bs ->+ case mN of+ Nothing -> Right bs+ Just n ->+ if fromIntegral (BS.length bs) > n+ then Left $ ErrorBinRepTooLong bs n+ else Right bs++check :: BinRep a => Cfg -> Bytes -> MetaStream a -> Either (Error a) ()+check cfg bs meta = do+ case msExpected meta of+ Nothing -> Right ()+ Just aExpected -> do+ bsExpected <- binRep 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 cfg bs' bsExpected of+ True -> Right ()+ False -> Left $ ErrorDidNotMatchExpected bs' bsExpected++checkExpected :: Cfg -> Bytes -> Bytes -> Bool+checkExpected cfg bs bsExpected =+ case cfgAllowPartialExpected cfg of+ True -> BS.isPrefixOf bs bsExpected+ False -> bs == bsExpected++-}++-- | 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 Bytes++toBinRep' :: BinRep a => a -> Either (Error a) Bytes+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 -> Bytes -> 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
+ src/StreamPatch/Patch/Binary/PascalText.hs view
@@ -0,0 +1,48 @@+{-| Newtype for manipulating length-prefixed strings.++This type is for UTF-8 'Text' that you intend to write out to a length-prefixed+bytestring. The size of the length field is static. You essentially have to+decide the maximum bytesize of the string on creation.+-}++module StreamPatch.Patch.Binary.PascalText where++import StreamPatch.Patch.Binary ( BinRep(..) )++import qualified Data.Text.Encoding as Text+import Data.Text ( Text )+import qualified Data.ByteString as BS+import GHC.TypeNats+import Data.Proxy+import Data.Bits++newtype PascalText (n :: Nat) = PascalText { unPascalText :: Text }++instance KnownNat n => BinRep (PascalText n) where+ toBinRep t =+ case encodePascalText t of+ Nothing -> Left "UTF-8 encoded text too long for length prefix field"+ Just bs -> Right bs++encodePascalText :: forall n. KnownNat n => PascalText n -> Maybe BS.ByteString+encodePascalText t = do+ lenBs <- encodeToSizedBE (fromIntegral (natVal (Proxy @n))) (BS.length bs)+ return $ lenBs <> bs+ where+ bs = Text.encodeUtf8 . unPascalText $ t++encodeToSizedBE :: (Integral a, Bits a) => Int -> a -> Maybe BS.ByteString+encodeToSizedBE byteSize x =+ let bs = i2be x+ nulls = byteSize - BS.length bs+ in if nulls < 0+ then Nothing+ else Just $ BS.replicate nulls 0x00 <> bs++-- | Re-encode an 'Integer' to a little-endian integer stored as a 'ByteString'+-- using the fewest bytes needed to represent it.+--+-- adapated from crypto-api 0.13.3, Crypto.Util.i2bs_unsized+i2be :: (Integral a, Bits a) => a -> BS.ByteString+i2be 0 = BS.singleton 0+i2be i = BS.reverse $ BS.unfoldr (\i' -> if i' <= 0 then Nothing else Just (fromIntegral i', (i' `shiftR` 8))) i
+ src/StreamPatch/Patch/Linearize.hs view
@@ -0,0 +1,58 @@+module StreamPatch.Patch.Linearize where++import StreamPatch.Patch++import GHC.Generics ( Generic )+import Numeric.Natural+import GHC.Natural ( minusNaturalMaybe )+import Data.Vinyl++import Control.Monad.State+import qualified Data.List as List+import qualified Data.ByteString as BS+import qualified Data.Text as Text+import Data.Text ( Text )+import Util ( traverseM )++class HasLength a where+ getLength :: a -> Natural++instance HasLength BS.ByteString where+ getLength = fromIntegral . BS.length+instance HasLength Text where+ getLength = fromIntegral . Text.length+instance HasLength String where+ getLength = fromIntegral . List.length++data Error fs a+ = ErrorOverlap -- ^ Two edits wrote to the same offset.+ (SeekRep 'AbsSeek) -- ^ absolute position in stream+ (Patch 'AbsSeek fs a) -- ^ overlapping patch+ (Patch 'AbsSeek fs a) -- ^ previous patch+ deriving (Generic)++deriving instance (Eq a, Eq (Patch 'AbsSeek fs a)) => Eq (Error fs a)+deriving instance (Show a, ReifyConstraint Show (Flap a) fs, RMap fs, RecordToList fs) => Show (Error fs a)+deriving instance (Functor (Patch 'AbsSeek fs)) => Functor (Error fs)+deriving instance (Foldable (Patch 'AbsSeek fs)) => Foldable (Error fs)+deriving instance (Traversable (Patch 'AbsSeek fs)) => Traversable (Error fs)++linearize+ :: HasLength a+ => [Patch 'AbsSeek fs a]+ -> Either (Error fs a) [Patch 'FwdSeek fs a]+linearize ps = evalState (traverseM go (List.sortBy comparePatchSeeks ps)) (0, undefined)+ where+ go p@(Patch a s _) = do+ (cursor, pPrev) <- get+ case s `minusNaturalMaybe` cursor of+ -- next absolute seek is before cursor: current patch overlaps prev+ Nothing -> return $ Left $ ErrorOverlap cursor p pPrev+ Just skip -> do+ let cursor' = cursor + skip + getLength a+ p' = p { patchSeek = skip }+ put (cursor', p)+ return $ Right p'++comparePatchSeeks :: Ord (SeekRep s) => Patch s fs a -> Patch s fs a -> Ordering+comparePatchSeeks p1 p2 = compare (patchSeek p1) (patchSeek p2)
+ src/StreamPatch/Stream.hs view
@@ -0,0 +1,81 @@+module StreamPatch.Stream where++import StreamPatch.Patch++import Util++import Data.Vinyl+import GHC.Natural+import Data.Kind+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Lazy as BL+import Control.Monad.State+import Control.Monad.Reader+import System.IO ( Handle, SeekMode(..), hSeek )++import qualified Data.List as List++-- TODO also require reporting cursor position (for error reporting)+class Monad m => MonadFwdStream m where+ type Chunk m :: Type++ -- | Read a number of bytes without advancing the cursor.+ readahead :: Natural -> m (Chunk m)++ -- | Advance cursor without reading.+ advance :: Natural -> m ()++ -- | Insert bytes into the stream at the cursor position, overwriting+ -- existing bytes.+ overwrite :: Chunk m -> m ()++-- TODO Need MonoTraversable to define for Text, ByteString etc easily. Bleh. I+-- think Snoyman's advice is to reimplement. Also bleh.+instance Monad m => MonadFwdStream (StateT ([a], [a]) m) where+ type Chunk (StateT ([a], [a]) m) = [a]+ readahead n = List.take (fromIntegral n) <$> gets fst+ advance n = do+ (src, out) <- get+ let (bs, src') = List.splitAt (fromIntegral n) src+ put (src', out <> bs)+ overwrite bs = do+ (src, out) <- get+ let (_, src') = List.splitAt (List.length bs) src+ put (src', out <> bs)++instance MonadIO m => MonadFwdStream (ReaderT Handle m) where+ type Chunk (ReaderT Handle m) = BS.ByteString+ readahead n = do+ hdl <- ask+ bs <- liftIO $ BS.hGet hdl (fromIntegral n)+ liftIO $ hSeek hdl RelativeSeek (- fromIntegral n)+ return bs+ advance n = do+ hdl <- ask+ liftIO $ hSeek hdl RelativeSeek (fromIntegral n)+ overwrite bs = do+ hdl <- ask+ liftIO $ BS.hPut hdl bs++instance Monad m => MonadFwdStream (StateT (BS.ByteString, BB.Builder) m) where+ type Chunk (StateT (BS.ByteString, BB.Builder) m) = BS.ByteString+ readahead n = BS.take (fromIntegral n) <$> gets fst+ advance n = do+ (src, out) <- get+ let (bs, src') = BS.splitAt (fromIntegral n) src+ put (src', out <> BB.byteString bs)+ overwrite bs = do+ (src, out) <- get+ let (_, src') = BS.splitAt (BS.length bs) src+ put (src', out <> BB.byteString bs)++-- | A forward stream, but backward too.+class MonadFwdStream m => MonadCursorStream m where+ -- | Move cursor.+ move :: Integer -> m ()++instance MonadIO m => MonadCursorStream (ReaderT Handle m) where+ move n = do+ hdl <- ask+ liftIO $ hSeek hdl RelativeSeek (fromIntegral n)
+ src/Util.hs view
@@ -0,0 +1,19 @@+module Util where++import Data.Foldable ( sequenceA_ )++-- lol. ty hw-kafka-client+traverseM+ :: (Traversable t, Applicative f, Monad m)+ => (v -> m (f v'))+ -> t v+ -> m (f (t v'))+traverseM f xs = sequenceA <$> traverse f xs++-- wonder if this is the correct type? oh well it works+traverseM_+ :: (Traversable t, Applicative f, Monad m)+ => (v -> m (f ()))+ -> t v+ -> m (f ())+traverseM_ f xs = sequenceA_ <$> traverse f xs
+ test/BytePatch/HexByteStringSpec.hs view
@@ -0,0 +1,23 @@+module BytePatch.HexByteStringSpec ( spec ) where++import BytePatch.HexByteString+import Test.Hspec+import Util++import qualified Data.ByteString as BS++spec :: Spec+spec = do+ let parse = parseFromCharStream parseHexByteString+ bs = BS.pack+ it "parses valid hex bytestrings" $ do+ parse "00" `shouldBe` Just (bs [0x00])+ parse "FF" `shouldBe` Just (bs [0xFF])+ parse "1234" `shouldBe` Just (bs [0x12, 0x34])+ parse "01 9A FE" `shouldBe` Just (bs [0x01, 0x9A, 0xFE])+ parse "FFFFFFFF" `shouldBe` Just (BS.replicate 4 0xFF)+ parse "12 34 AB CD" `shouldBe` Just (bs [0x12, 0x34, 0xAB, 0xCD])+ it "fails to parse invalid hex bytestrings" $ do+ parse "-00" `shouldBe` Nothing+ parse "FG" `shouldBe` Nothing+ parse "0x1234" `shouldBe` Nothing
− test/BytePatch/Pretty/HexByteStringSpec.hs
@@ -1,23 +0,0 @@-module BytePatch.Pretty.HexByteStringSpec ( spec ) where--import BytePatch.Pretty.HexByteString-import Test.Hspec-import Util--import qualified Data.ByteString as BS--spec :: Spec-spec = do- let parse = parseFromCharStream parseHexByteString- bs = BS.pack- it "parses valid hex bytestrings" $ do- parse "00" `shouldBe` Just (bs [0x00])- parse "FF" `shouldBe` Just (bs [0xFF])- parse "1234" `shouldBe` Just (bs [0x12, 0x34])- parse "01 9A FE" `shouldBe` Just (bs [0x01, 0x9A, 0xFE])- parse "FFFFFFFF" `shouldBe` Just (BS.replicate 4 0xFF)- parse "12 34 AB CD" `shouldBe` Just (bs [0x12, 0x34, 0xAB, 0xCD])- it "fails to parse invalid hex bytestrings" $ do- parse "-00" `shouldBe` Nothing- parse "FG" `shouldBe` Nothing- parse "0x1234" `shouldBe` Nothing
+ test/StreamPatch/ApplySpec.hs view
@@ -0,0 +1,48 @@+module StreamPatch.ApplySpec ( spec ) where++import StreamPatch.Apply+import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck+import Util++import StreamPatch.Patch+import Data.Functor.Const+import Data.Vinyl+import GHC.Natural++spec :: Spec+spec = do+ let patch' ps = runPureSimpleFwdList $ makePatchscript ps+ it "applies valid simple forward in-place patches" $ do+ patch' [] "1234567890" `shouldBe` "1234567890"+ patch' [(0, "XXX")] "1234567890" `shouldBe` "XXX4567890"+ patch' [(1, "XXX")] "1234567890" `shouldBe` "1XXX567890"+ patch' [(7, "XXX")] "1234567890" `shouldBe` "1234567XXX"+ prop "applies valid generated simple forward in-place patches" $ do+ \n -> forAll (genPatchList @Char n) $+ \ps -> forAll (genBoundList @Char n n) $+ \a -> pendingWith "cba to reimplement patcher"+ -- patchListPure ps a `shouldBe` manualPatch ps a++-- | "Verified" implementation of a simple patch algorithm.+--+-- nah I cba lol just trust me+--manualPatch :: [Patch 'FwdSeek '[] [a]] -> [a] -> [a]++genPatchList :: Arbitrary a => Natural -> Gen [Patch 'FwdSeek '[] [a]]+genPatchList = skip []+ where+ skip ps n = do+ -- why no Random Natural...+ nSkip <- choose (0, naturalToInteger n)+ gen ps (naturalFromInteger nSkip) $ n - (fromIntegral nSkip)+ gen ps _ 0 = return ps+ gen ps nSkip n = do+ a <- genBoundList n 1+ let n' = n - (fromIntegral (length a))+ skip (makePatch a nSkip : ps) n'++genBoundList :: Arbitrary a => Natural -> Natural -> Gen [a]+genBoundList x y =+ arbitrary `suchThat` \l -> fromIntegral (length l) <= x && fromIntegral (length l) >= y
+ test/StreamPatch/Patch/AlignSpec.hs view
@@ -0,0 +1,28 @@+module StreamPatch.Patch.AlignSpec ( spec ) where++import StreamPatch.Patch.Align+import Test.Hspec+import Util++import StreamPatch.Patch+import Data.Vinyl+import Data.Functor.Const+import GHC.Natural++makeUnalignedPatch+ :: [(Integer, a)]+ -> [Patch 'RelSeek '[Const (Meta 'FwdSeek)] a]+makeUnalignedPatch = map go+ where go (n, a) = Patch a n $ FunctorRec $ Flap (Const (Meta Nothing)) :& RNil++align'+ :: Integer -> [(Integer, String)]+ -> Either (Error 'FwdSeek) [Patch 'FwdSeek '[] String]+align' sBase = traverse (align sBase) . makeUnalignedPatch++spec :: Spec+spec = do+ it "aligns valid simple patches" $ do+ align' 0 [] `shouldBe` Right (makePatchscript [])+ align' 0 [(0, "XXX")] `shouldBe` Right (makePatchscript [(0, "XXX")])+ align' 5 [(0, "XXX"), ((-5), "XXX")] `shouldBe` Right (makePatchscript [(5, "XXX"), (0, "XXX")])
+ test/StreamPatch/Patch/LinearizeSpec.hs view
@@ -0,0 +1,19 @@+module StreamPatch.Patch.LinearizeSpec ( spec ) where++import StreamPatch.Patch.Linearize+import Test.Hspec+import Util++import StreamPatch.Patch+import Data.Functor.Const+import Data.Either.Combinators+import GHC.Natural+import Optics+import Data.Generics.Product.Any++spec :: Spec+spec = do+ let p = makePatch' @'AbsSeek+ p' = makePatch' @'FwdSeek+ it "linearizes a simple valid patch" $ do+ linearize [p "1" 1, p "5" 5, p "2" 2] `shouldBe` Right [p' "1" 1, p' "2" 0, p' "5" 2]
test/Util.hs view
@@ -1,10 +1,29 @@-{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-orphans #-} module Util where +import StreamPatch.Patch+import Data.Vinyl import Text.Megaparsec import Data.Void+import GHC.Natural+import Test.QuickCheck --parseFromCharStream :: (MonadParsec e s m, Token s ~ Char) => m a -> s -> Maybe a parseFromCharStream :: (Stream s, Token s ~ Char) => Parsec Void s a -> s -> Maybe a parseFromCharStream parser text = parseMaybe parser text++-- TODO this is in quickcheck-instances+instance Arbitrary Natural where+ arbitrary = arbitrarySizedNatural+ shrink = shrinkIntegral++makePatch :: a -> Natural -> Patch 'FwdSeek '[] a+makePatch a n = Patch a n $ FunctorRec RNil++makePatch' :: forall s a. a -> SeekRep s -> Patch s '[] a+makePatch' a n = Patch a n $ FunctorRec RNil++makePatchscript :: [(Natural, a)] -> [Patch 'FwdSeek '[] a]+makePatchscript = map go+ where go (n, a) = makePatch a n