hcobs-0.1.0.1: src/Data/Stuffed.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
-- | This module tries to be as efficient as possible, type safe and easy to use. If you have a "sink" like
--
-- > sink :: Stuffed 0 -> IO ()
-- > sink = undefined
--
-- You'd then simply be able to encode a Bytestring with
--
-- > sink $ stuff bytes
--
-- You can try this out in ghci with:
--
-- >>> :set -XOverloadedStrings
-- >>> :set -XDataKinds
-- >>> import Data.Stuffed
-- >>> let stuffedBytes = stuff "a\0b\0c" :: Stuffed 0
-- >>> unpack $ unwrap stuffedBytes -- directly access the underlying bytestring
-- [2,97,2,98,2,99]
-- >>> unpack $ unstuff stuffedBytes
-- [97,0,98,0,99]
module Data.Stuffed
( Stuffed
, stuff
, unstuff
, unwrap
)
where
import qualified Data.ByteString as B
import Data.ByteString.Builder (Builder, byteString, toLazyByteString)
import Data.ByteString.Lazy (ByteString, fromChunks, splitAt,
toStrict)
import Data.Int (Int64)
import Data.Proxy (Proxy (..))
import Data.Reflection (reflect)
import Data.Semigroup (Semigroup)
import Data.Word (Word8)
import GHC.Generics (Generic)
import GHC.Types (Nat)
import Prelude hiding (concat, length, null, splitAt, last)
import Data.Stuffed.Internal (IsByte)
-- | Wrapper for Lazy Bytestrings, parametrized on the Byte (Word8, represented as a type-level 'Nat') to be encoded away.
newtype Stuffed (a :: Nat) = Stuffed ByteString
deriving (Eq, Ord, Show, Semigroup, Monoid, Generic)
splitEvery :: Int64 -> ByteString -> [B.ByteString]
splitEvery _ "" = []
splitEvery n bs = toStrict start : splitEvery n rest
where
(start, rest) = splitAt n bs
stuff :: forall a. IsByte a => ByteString -> Stuffed a
stuff bs = Stuffed $ fromChunks chunks
where
excluded = fromIntegral (reflect (Proxy :: Proxy a) :: Integer)
chunks = map (buildStuffed excluded) $ splitEvery 254 bs
buildStuffed :: Word8 -> B.ByteString -> B.ByteString
buildStuffed excluded bs = B.cons last stuffed
where
excludedOffset = excluded + 1
swapExcluded n current | current == excluded = (excludedOffset, n)
| otherwise = (n + 1, current)
(last, stuffed) = B.mapAccumR swapExcluded excludedOffset bs
unstuff :: forall a. IsByte a => Stuffed a -> ByteString
unstuff (Stuffed bs) = toLazyByteString $ mconcat chunks
where
excluded = fromIntegral (reflect (Proxy :: Proxy a) :: Integer)
rebuild bytes = rebuildChunk excluded (B.uncons bytes) mempty
chunks = map rebuild $ splitEvery 255 bs
rebuildChunk :: Word8 -> Maybe (Word8, B.ByteString) -> Builder -> Builder
rebuildChunk _ Nothing b = b
rebuildChunk excluded (Just (starting_offset, rest)) _ = byteString $ snd $ B.mapAccumL swapExcluded (starting_offset - 1) rest
where
swapExcluded n current | n == excluded = (current - 1, excluded)
| otherwise = (n - 1, current)
-- | Extract the encoded bytestring from a Stuffed
unwrap :: Stuffed a -> ByteString
unwrap (Stuffed bs) = bs