packages feed

bytepatch-0.2.0: src/BytePatch/Pretty/PascalText.hs

{-| 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)