solana-haskell-sdk-1.2.0.0: src/Network/Solana/Core/Compact.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Network.Solana.Core.Compact
-- Description : compact-u16 (ShortU16) encoding and compact arrays.
--
-- Solana serializes lengths as /compact-u16/: a 'Word16' packed into 1–3
-- bytes, little-endian, 7 data bits per byte, with the high bit of each
-- byte flagging a continuation byte. The third byte may only carry 2 data
-- bits, and aliased (non-canonical) encodings are rejected when decoding.
-- A 'CompactArray' is a sequence serialized as its compact-u16 length
-- followed by the serialized items.
module Network.Solana.Core.Compact
( getCompactU16,
putCompactU16,
encodeCompactU16,
decodeCompactU16,
CompactArray (),
mkCompact,
unCompact,
getCompactArrayLength,
)
where
import Control.Monad (replicateM)
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.Bits
import Data.ByteString.Lazy qualified as BL
import GHC.Generics
------------------------------------------------------------------------------------------------
-- * CompactArray
------------------------------------------------------------------------------------------------
-- | A list paired with its length, serialized ('Binary' 'put') as a
-- compact-u16 length prefix followed by each item. Build with 'mkCompact'.
data CompactArray a = CompactArray
{ -- | The recorded number of items.
getCompactArrayLength :: Word16,
-- | The items of the array.
unCompact :: [a]
}
deriving (Eq, Ord, Show, Generic)
-- instance (ToJSON a) => ToJSON (CompactArray a) where
-- toJSON :: CompactArray a -> Value
-- toJSON (CompactArray _ xs) = toJSON xs
-- instance (FromJSON a) => FromJSON (CompactArray a) where
-- parseJSON :: (FromJSON a) => Value -> Parser (CompactArray a)
-- parseJSON v = mkCompact <$> parseJSON v
instance (Binary a) => Binary (CompactArray a) where
put :: (Binary a) => CompactArray a -> Put
put (CompactArray i xs) = do
putCompactU16 i
mapM_ put xs -- not default putList
get :: (Binary a) => Get (CompactArray a)
get = do
n <- getCompactU16
xs <- replicateM (fromIntegral n) get
pure (CompactArray n xs)
-- | Wrap a list into a 'CompactArray', recording its length. The length is
-- truncated to 'Word16'.
mkCompact :: [a] -> CompactArray a
mkCompact xs = CompactArray (fromIntegral $ length xs) xs
------------------------------------------------------------------------------------------------
-- * CompactU16
------------------------------------------------------------------------------------------------
-- | Decode a compact-u16 value. Fails on encodings longer than 3 bytes, on
-- aliased (non-canonical) encodings, on an invalid third byte, and on
-- values exceeding 'Word16'.
getCompactU16 :: Get Word16
getCompactU16 = go 0 0
where
go :: Word32 -> Int -> Get Word16
go acc byteIndex
| byteIndex >= 3 = fail "Too many bytes in compact-u16"
| otherwise = do
byte <- getWord8
let value = fromIntegral (byte .&. 0x7F) --
acc' = acc .|. (value `shiftL` (7 * byteIndex))
continue = (byte .&. 0x80) /= 0
if continue
then go acc' (byteIndex + 1)
else
if byteIndex > 0 && byte == 0
then fail "Aliased (non-canonical) compact-u16 encoding"
else
if byteIndex == 2 && (byte .&. 0xFC) /= 0
then fail "Invalid 3rd byte in compact-u16 (only 2 bits allowed)"
else case fromIntegral acc' of
w | w <= 0xFFFF -> return w
_ -> fail "Decoded value exceeds u16 range"
-- | Encode a 'Word16' as compact-u16 (1–3 bytes).
putCompactU16 :: Word16 -> Put
putCompactU16 val = go (fromIntegral val :: Word32)
where
go :: Word32 -> Put
go v
| v < 0x80 = putWord8 (fromIntegral v)
| otherwise = do
putWord8 (fromIntegral (v .&. 0x7F) .|. 0x80)
go (v `shiftR` 7)
-- | Run 'putCompactU16' to a lazy byte string.
encodeCompactU16 :: Word16 -> BL.ByteString
encodeCompactU16 = runPut . putCompactU16
-- | Run 'getCompactU16' over a lazy byte string, returning the decoder's
-- error message on failure.
decodeCompactU16 :: BL.ByteString -> Either String Word16
decodeCompactU16 bs =
case runGetOrFail getCompactU16 bs of
Left (_, _, err) -> Left err
Right (_, _, val) -> Right val