spire-protobuf-0.1.0.0: src/Spire/Protobuf/Wire.hs
-- | Protobuf wire format primitives.
--
-- The proto3 wire format uses 5 wire types. Each field on the wire is
-- a tag (field_number << 3 | wire_type) followed by the value.
--
-- This module provides low-level encoding and decoding of varints,
-- fixed-width values, and length-delimited fields.
module Spire.Protobuf.Wire
( -- * Wire types
WireType (..)
-- * Tags
, encodeTag
, decodeTag
-- * Varint encoding/decoding
, encodeVarint
, decodeVarint
-- * Zigzag encoding (for sint32/sint64)
, zigzagEncode
, zigzagDecode
-- * Fixed-width encoding
, encodeFixed32
, encodeFixed64
, decodeFixed32
, decodeFixed64
-- * Float/Double encoding
, encodeProtoFloat
, encodeProtoDouble
, decodeProtoFloat
, decodeProtoDouble
-- * Length-delimited
, encodeLengthDelimited
, decodeLengthDelimited
-- * Field skipping
, skipField
) where
import Prelude hiding (encodeFloat, decodeFloat)
import Data.Bits
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as B
import Data.Int (Int64)
import Data.Word (Word8, Word32, Word64)
import GHC.Float (castWord32ToFloat, castFloatToWord32,
castWord64ToDouble, castDoubleToWord64)
-- | Proto3 wire types.
data WireType
= WireVarint -- ^ 0: int32, int64, uint32, uint64, sint32, sint64, bool, enum
| WireFixed64 -- ^ 1: fixed64, sfixed64, double
| WireLengthDelimited -- ^ 2: string, bytes, embedded messages, packed repeated
| WireFixed32 -- ^ 5: fixed32, sfixed32, float
deriving (Show, Eq)
wireTypeId :: WireType -> Word8
wireTypeId WireVarint = 0
wireTypeId WireFixed64 = 1
wireTypeId WireLengthDelimited = 2
wireTypeId WireFixed32 = 5
wireTypeFromId :: Word8 -> Maybe WireType
wireTypeFromId 0 = Just WireVarint
wireTypeFromId 1 = Just WireFixed64
wireTypeFromId 2 = Just WireLengthDelimited
wireTypeFromId 5 = Just WireFixed32
wireTypeFromId _ = Nothing
-- ===================================================================
-- Tags
-- ===================================================================
-- | Encode a field tag (field_number << 3 | wire_type).
encodeTag :: Int -> WireType -> B.Builder
encodeTag fieldNum wt =
encodeVarint (fromIntegral fieldNum `shiftL` 3 .|. fromIntegral (wireTypeId wt))
{-# INLINE encodeTag #-}
-- | Decode a field tag. Returns (field_number, wire_type, remaining bytes).
decodeTag :: ByteString -> Maybe (Int, WireType, ByteString)
decodeTag bs = do
(val, rest) <- decodeVarint bs
let wireId = fromIntegral (val .&. 0x07) :: Word8
fieldNum = fromIntegral (val `shiftR` 3)
wt <- wireTypeFromId wireId
Just (fieldNum, wt, rest)
{-# INLINE decodeTag #-}
-- ===================================================================
-- Varint encoding/decoding
-- ===================================================================
-- | Encode a value as a varint (7 bits per byte, MSB continuation).
encodeVarint :: Word64 -> B.Builder
encodeVarint n
| n < 0x80 = B.word8 (fromIntegral n)
| otherwise = B.word8 (fromIntegral (n .&. 0x7F) .|. 0x80)
<> encodeVarint (n `shiftR` 7)
{-# INLINE encodeVarint #-}
-- | Decode a varint. Returns (value, remaining bytes).
-- Rejects overlong encodings (more than 10 bytes or invalid high bits
-- in the 10th byte).
decodeVarint :: ByteString -> Maybe (Word64, ByteString)
decodeVarint = go 0 0
where
go !acc !shft !bs = case BS.uncons bs of
Nothing -> Nothing
Just (b, rest) ->
let val = acc .|. (fromIntegral (b .&. 0x7F) `shiftL` shft)
in if b .&. 0x80 == 0
then if shft == 63 && (b .&. 0x7E) /= 0
then Nothing -- reject overlong encoding at byte 10
else Just (val, rest)
else if shft >= 63
then Nothing -- overflow protection
else go val (shft + 7) rest
{-# INLINE decodeVarint #-}
-- ===================================================================
-- Zigzag encoding (for sint32/sint64)
-- ===================================================================
-- | Zigzag-encode a signed integer: maps negatives to odds, positives to evens.
-- This makes small negative numbers small on the wire.
zigzagEncode :: Int64 -> Word64
zigzagEncode n = fromIntegral ((n `shiftL` 1) `xor` (n `shiftR` 63))
{-# INLINE zigzagEncode #-}
-- | Zigzag-decode.
zigzagDecode :: Word64 -> Int64
zigzagDecode n = fromIntegral ((n `shiftR` 1) `xor` negate (n .&. 1))
{-# INLINE zigzagDecode #-}
-- ===================================================================
-- Fixed-width encoding
-- ===================================================================
-- | Encode a 32-bit value in little-endian.
encodeFixed32 :: Word32 -> B.Builder
encodeFixed32 = B.word32LE
{-# INLINE encodeFixed32 #-}
-- | Encode a 64-bit value in little-endian.
encodeFixed64 :: Word64 -> B.Builder
encodeFixed64 = B.word64LE
{-# INLINE encodeFixed64 #-}
-- | Decode a 32-bit little-endian value.
decodeFixed32 :: ByteString -> Maybe (Word32, ByteString)
decodeFixed32 bs
| BS.length bs < 4 = Nothing
| otherwise =
let b0 = fromIntegral (BS.index bs 0) :: Word32
b1 = fromIntegral (BS.index bs 1) :: Word32
b2 = fromIntegral (BS.index bs 2) :: Word32
b3 = fromIntegral (BS.index bs 3) :: Word32
in Just (b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24),
BS.drop 4 bs)
{-# INLINE decodeFixed32 #-}
-- | Decode a 64-bit little-endian value.
decodeFixed64 :: ByteString -> Maybe (Word64, ByteString)
decodeFixed64 bs
| BS.length bs < 8 = Nothing
| otherwise =
let readByte i = fromIntegral (BS.index bs i) :: Word64
in Just ( readByte 0 .|. (readByte 1 `shiftL` 8) .|. (readByte 2 `shiftL` 16)
.|. (readByte 3 `shiftL` 24) .|. (readByte 4 `shiftL` 32)
.|. (readByte 5 `shiftL` 40) .|. (readByte 6 `shiftL` 48)
.|. (readByte 7 `shiftL` 56),
BS.drop 8 bs)
{-# INLINE decodeFixed64 #-}
-- ===================================================================
-- Float/Double
-- ===================================================================
-- | Encode a Float (as fixed32, little-endian IEEE 754).
encodeProtoFloat :: Float -> B.Builder
encodeProtoFloat = encodeFixed32 . castFloatToWord32
{-# INLINE encodeProtoFloat #-}
-- | Encode a Double (as fixed64, little-endian IEEE 754).
encodeProtoDouble :: Double -> B.Builder
encodeProtoDouble = encodeFixed64 . castDoubleToWord64
{-# INLINE encodeProtoDouble #-}
-- | Decode a Float.
decodeProtoFloat :: ByteString -> Maybe (Float, ByteString)
decodeProtoFloat bs = do
(w, rest) <- decodeFixed32 bs
Just (castWord32ToFloat w, rest)
{-# INLINE decodeProtoFloat #-}
-- | Decode a Double.
decodeProtoDouble :: ByteString -> Maybe (Double, ByteString)
decodeProtoDouble bs = do
(w, rest) <- decodeFixed64 bs
Just (castWord64ToDouble w, rest)
{-# INLINE decodeProtoDouble #-}
-- ===================================================================
-- Length-delimited
-- ===================================================================
-- | Encode a length-delimited value: varint length + bytes.
encodeLengthDelimited :: ByteString -> B.Builder
encodeLengthDelimited bs =
encodeVarint (fromIntegral (BS.length bs)) <> B.byteString bs
{-# INLINE encodeLengthDelimited #-}
-- | Decode a length-delimited value. Returns (bytes, remaining).
-- Zero-copy: returns a slice of the input ByteString.
decodeLengthDelimited :: ByteString -> Maybe (ByteString, ByteString)
decodeLengthDelimited bs = do
(len, rest) <- decodeVarint bs
let n = fromIntegral len
if BS.length rest < n
then Nothing
else Just (BS.take n rest, BS.drop n rest)
{-# INLINE decodeLengthDelimited #-}
-- ===================================================================
-- Field skipping (for unknown fields)
-- ===================================================================
-- | Skip a field value based on its wire type. Returns remaining bytes.
skipField :: WireType -> ByteString -> Maybe ByteString
skipField WireVarint bs = do
(_, rest) <- decodeVarint bs
Just rest
skipField WireFixed64 bs
| BS.length bs >= 8 = Just (BS.drop 8 bs)
| otherwise = Nothing
skipField WireLengthDelimited bs = do
(_, rest) <- decodeLengthDelimited bs
Just rest
skipField WireFixed32 bs
| BS.length bs >= 4 = Just (BS.drop 4 bs)
| otherwise = Nothing
{-# INLINE skipField #-}