packages feed

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