wireform-proto-0.2.0.0: src/Proto/Internal/Wire.hs
{- | Wire format types and tag encoding/decoding.
The protobuf wire format uses a tag-length-value scheme where each field
is preceded by a tag encoding the field number and wire type.
-}
module Proto.Internal.Wire (
-- * Wire types
WireType (..),
wireTypeFromTag,
wireTypeToWord,
-- * Tags
Tag (..),
makeTag,
encodeTag,
decodeTag,
-- * Field key
fieldTag,
) where
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.Word (Word32, Word64)
-- | Protobuf wire types.
data WireType
= WireVarint -- 0: int32, int64, uint32, uint64, sint32, sint64, bool, enum
| Wire64Bit -- 1: fixed64, sfixed64, double
| WireLengthDelimited -- 2: string, bytes, embedded messages, packed repeated fields
| WireStartGroup -- 3: deprecated
| WireEndGroup -- 4: deprecated
| Wire32Bit -- 5: fixed32, sfixed32, float
deriving stock (Show, Eq, Ord, Enum, Bounded)
-- | Extract the wire type from the low 3 bits of a tag value.
wireTypeFromTag :: Word32 -> Maybe WireType
wireTypeFromTag n = case n .&. 0x07 of
0 -> Just WireVarint
1 -> Just Wire64Bit
2 -> Just WireLengthDelimited
3 -> Just WireStartGroup
4 -> Just WireEndGroup
5 -> Just Wire32Bit
_ -> Nothing
-- | Convert a 'WireType' to its numeric encoding (0-5).
wireTypeToWord :: WireType -> Word32
wireTypeToWord = \case
WireVarint -> 0
Wire64Bit -> 1
WireLengthDelimited -> 2
WireStartGroup -> 3
WireEndGroup -> 4
Wire32Bit -> 5
-- | A decoded tag: field number + wire type.
data Tag = Tag
{ tagFieldNumber :: {-# UNPACK #-} !Int
, tagWireType :: !WireType
}
deriving stock (Show, Eq)
-- | Construct a tag from a field number and wire type.
makeTag :: Int -> WireType -> Tag
makeTag = Tag
-- | Encode a tag as a single Word64 value (for varint encoding).
encodeTag :: Tag -> Word64
encodeTag (Tag fn wt) =
fromIntegral fn `shiftL` 3 .|. fromIntegral (wireTypeToWord wt)
{- | Decode a tag from a varint value. Per the proto wire format
spec, field number 0 is reserved and any tag whose field
number resolves to 0 (or whose wire type is one of the
deprecated group types) must be rejected; otherwise a
conformance test like @IllegalZeroFieldNum@ would be silently
accepted as an unknown field instead of an error.
-}
decodeTag :: Word64 -> Maybe Tag
decodeTag w = do
wt <- wireTypeFromTag (fromIntegral (w .&. 0x07))
let fn = fromIntegral (w `shiftR` 3)
if fn == 0 then Nothing else Just (Tag fn wt)
-- | Convenience: make the wire tag value for a given field number and wire type.
fieldTag :: Int -> WireType -> Word64
fieldTag fn wt = encodeTag (makeTag fn wt)