bytestring-typenats-1.0.0: src/Data/ByteString/TypeNats.hs
{-# LANGUAGE DataKinds, KindSignatures, TypeOperators, OverloadedStrings,
LambdaCase, DeriveDataTypeable #-}
-- |
-- Module : Data.ByteString.TypeNats
-- Copyright: Jeremy Groven
-- License: BSD3
--
-- A tiny tagged 'ByteString' wrapper that carries around the size of the
-- wrapped ByteString. This allows for very simple
-- serialization/deserialization.
module Data.ByteString.TypeNats
( ByteString(stripSize)
, wrap
, wrap'
, random
, length
, append
, sizeHelper
, sizeHelper'
#ifdef UseArbitrary
, fastRandBs
, slowRandBs
#endif
) where
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as BSC
-- import qualified Data.ByteString.Lazy as LazyByteString
import Control.DeepSeq ( NFData(..) )
-- import Crypto.Hash.Skein512 ( hash )
import Data.Functor.Identity ( Identity(..) )
import Data.Monoid ( (<>) )
import Data.Proxy ( Proxy(..) )
import Data.String ( IsString(..) )
import Data.Typeable ( Typeable )
import System.Entropy ( getEntropy )
import GHC.TypeLits
import Prelude hiding ( length )
#ifdef UseCereal
import qualified Data.Serialize.Put as SP
import qualified Data.Serialize.Get as SG
import Data.Serialize ( Serialize(..) )
#endif
#ifdef UseBinary
import qualified Data.Binary.Put as BP
import qualified Data.Binary.Get as BG
import Data.Binary ( Binary(..) )
import Data.Coerce ( coerce )
#endif
#ifdef UseArbitrary
import Crypto.Hash.BLAKE2.BLAKE2bp ( hash )
import Test.QuickCheck ( Arbitrary(..), Gen, choose, vectorOf )
#endif
-- | A sized-tagged wrapper around 'ByteString's. Useful when doing a lot of
-- serialization and deserialization of bytestrings that have fixed lengths.
newtype ByteString (sz :: Nat ) = ByteString
{ stripSize :: ByteString.ByteString }
deriving ( Eq, Ord, Typeable )
-- |Wrap a 'ByteString' with a size tag. Returns 'Just' if the given ByteString
-- has the desired length, 'Nothing' if it does not.
wrap :: KnownNat sz => ByteString.ByteString -> Either String (ByteString sz)
wrap bs = runIdentity $ sizeHelper (const $ return bs)
-- | Wrap a 'ByteString' with a size tag. Errors out if the given ByteString is
-- the wrong size, does the wrapping if the size is right.
wrap' :: KnownNat sz => ByteString.ByteString -> ByteString sz
wrap' bs = case wrap bs of
Right wrapped -> wrapped
Left err -> error err
-- | Generate a random bytestring using the system's entropy source
random :: KnownNat sz => IO (ByteString sz)
random = sizeHelper' getEntropy
-- | Determine the length of a size tagged ByteString. I think this is probably
-- constant-folded at compile time, since it only uses types.
length :: KnownNat sz => ByteString sz -> Int
length = fromIntegral . natVal . proxy
-- | Append two tagged 'ByteString's.
append :: (KnownNat s1, KnownNat s2)
=> ByteString s1 -> ByteString s2 -> ByteString (s1+s2)
append (ByteString a) (ByteString b) =
-- I really want to use wrap', but I can't make the type machinery work.
-- Something about having to be 10% smarter than the tools we use... :(
ByteString (a <> b)
proxy :: ByteString sz -> Proxy sz
proxy _ = Proxy
fst' :: (a, a) -> a
fst' = fst
-- | Generate a ByteString of the desired length. The given callback will be
-- called with the number of bytes that are indicated by the output's type, and
-- is expected to return a (standard) ByteString of that length.
sizeHelper :: (Monad m, KnownNat sz)
=> (Int -> m ByteString.ByteString)
-> m (Either String (ByteString sz))
sizeHelper fn = do
let dummy = undefined
r <- helper' fn dummy
return $ fst' (r, dummy)
where
helper' :: (Monad m, KnownNat sz)
=> (Int -> m ByteString.ByteString)
-> ByteString sz
-> m (Either String (ByteString sz))
helper' fn' dummy = do
let needLen = fromIntegral $ natVal $ proxy dummy
bs <- fn' needLen
let gotLen = ByteString.length bs
if gotLen == needLen
then return $ Right $ ByteString bs
else return $ Left $ "Expected " ++ show needLen ++
" byte result from callback, but got " ++ show gotLen
-- | Same as sizeHelper, but this does the monad's fail action instead of
-- returning Left/Right values
sizeHelper' :: (Monad m, KnownNat sz)
=> (Int -> m ByteString.ByteString)
-> m (ByteString sz)
sizeHelper' fn =
sizeHelper fn >>= \case
Right bs -> return bs
Left err -> fail err
instance Show (ByteString sz) where
showsPrec x b = showsPrec x (stripSize b)
instance KnownNat sz => IsString (ByteString sz) where
fromString = wrap' . BSC.pack
instance NFData (ByteString sz) where
rnf (ByteString bs) = rnf bs
#ifdef UseCereal
instance KnownNat sz => Serialize (ByteString sz) where
put (ByteString bs) = SP.putByteString bs
get = sizeHelper' SG.getByteString
#endif
#ifdef UseBinary
instance KnownNat sz => Binary (ByteString sz) where
put (ByteString bs) = BP.putByteString bs
putList sbss =
let bss = coerce sbss
bs = ByteString.concat bss
in BP.putByteString bs
get = sizeHelper' BG.getByteString
#endif
#ifdef UseArbitrary
-- | Quickly generate large amounts of pseudo-random data using a few bytes
-- from the quicktest generator, and then a bunch more bytes from Blake2.
fastRandBs :: Int -> Gen ByteString.ByteString
fastRandBs 0 = return ""
fastRandBs numBytes | numBytes <= 16 = slowRandBs numBytes
fastRandBs numBytes = hash numBytes "" <$> slowRandBs 16
-- | Use choose to generate some "random" Word8 values, and then pack them
-- together with ByteString.pack
slowRandBs :: Int -> Gen ByteString.ByteString
slowRandBs numBytes = ByteString.pack `fmap` vectorOf numBytes (choose (0, 255))
instance KnownNat sz => Arbitrary (ByteString sz) where
arbitrary = sizeHelper' fastRandBs
#endif