packages feed

spire-protobuf-0.1.0.0: src/Spire/Protobuf/Decode.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Protobuf decoding: single-pass wire format parser.
--
-- Provides 'ProtoDecode' for individual value types and 'DecodeError'
-- for structured error reporting. The decoder works directly on strict
-- 'ByteString' with zero-copy slicing for bytes/string fields.
module Spire.Protobuf.Decode
  ( -- * ProtoDecode class
    ProtoDecode (..)
    -- * Raw field parsing
  , RawField (..)
  , parseFields
    -- * Packed repeated field decoding
  , decodePacked
    -- * Packed/unpacked repeated entry decoding (Generics support)
  , DecodeRepeatedEntry (..)
    -- * Errors
  , DecodeError (..)
  ) where

import Data.Bits ((.&.))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Int (Int32, Int64)
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Data.Word (Word32, Word64)
import GHC.Float (castWord32ToFloat, castWord64ToDouble)

import Spire.Protobuf.Wire
import Spire.Protobuf.Field (SInt32(..), SInt64(..))


-- ===================================================================
-- Errors
-- ===================================================================

-- | Errors that can occur during protobuf decoding.
data DecodeError
  = UnexpectedWireType !Int !WireType !WireType
    -- ^ field number, expected wire type, actual wire type
  | TruncatedInput
    -- ^ input ended before a complete value could be read
  | InvalidVarint
    -- ^ varint encoding was malformed or overflowed
  | InvalidUtf8
    -- ^ a string field contained invalid UTF-8
  | InvalidWireType !Int
    -- ^ wire type id was not one of 0, 1, 2, 5
  deriving (Show, Eq)


-- ===================================================================
-- Raw field representation
-- ===================================================================

-- | A raw (unparsed) field value as it appears on the wire.
-- Used as an intermediate representation during decoding: parse all
-- tag-value pairs first, then interpret each field.
data RawField
  = RawVarint   !Word64
  | RawFixed64  !Word64
  | RawFixed32  !Word32
  | RawBytes    !ByteString   -- ^ zero-copy slice of the input
  deriving (Show, Eq)


-- | Parse all tag-value pairs from a protobuf message.
-- Returns a list of (field_number, raw_value) pairs in wire order.
-- Unknown wire types cause a 'Left' error; otherwise parsing is
-- lenient (unknown field numbers are preserved).
parseFields :: ByteString -> Either DecodeError [(Int, RawField)]
parseFields = go []
  where
    go !acc bs
      | BS.null bs = Right (reverse acc)
      | otherwise = case decodeTag bs of
          Nothing ->
            -- Distinguish truncated input from invalid wire type
            case decodeVarint bs of
              Nothing -> Left TruncatedInput
              Just (val, _) ->
                let wireId = fromIntegral (val .&. 0x07) :: Int
                in if wireId `elem` [0, 1, 2, 5]
                   then Left TruncatedInput
                   else Left (InvalidWireType wireId)
          Just (fieldNum, wt, rest) -> case parseRawField wt rest of
            Nothing  -> Left TruncatedInput
            Just (raw, rest') -> go ((fieldNum, raw) : acc) rest'

-- | Parse a raw field value given its wire type.
parseRawField :: WireType -> ByteString -> Maybe (RawField, ByteString)
parseRawField WireVarint bs = do
  (val, rest) <- decodeVarint bs
  Just (RawVarint val, rest)
parseRawField WireFixed64 bs = do
  (val, rest) <- decodeFixed64 bs
  Just (RawFixed64 val, rest)
parseRawField WireFixed32 bs = do
  (val, rest) <- decodeFixed32 bs
  Just (RawFixed32 val, rest)
parseRawField WireLengthDelimited bs = do
  (val, rest) <- decodeLengthDelimited bs
  Just (RawBytes val, rest)


-- ===================================================================
-- ProtoDecode class
-- ===================================================================

-- | Class for types that can be decoded from a raw protobuf field.
class ProtoDecode a where
  -- | Decode a value from a 'RawField'.
  protoDecodeValue :: RawField -> Either DecodeError a

-- Int32: varint
instance ProtoDecode Int32 where
  protoDecodeValue (RawVarint w) = Right (fromIntegral w :: Int32)
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- Int64: varint
instance ProtoDecode Int64 where
  protoDecodeValue (RawVarint w) = Right (fromIntegral w :: Int64)
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- Word32: varint
instance ProtoDecode Word32 where
  protoDecodeValue (RawVarint w) = Right (fromIntegral w :: Word32)
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- Word64: varint
instance ProtoDecode Word64 where
  protoDecodeValue (RawVarint w) = Right w
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- Bool: varint, 0 = False, nonzero = True
instance ProtoDecode Bool where
  protoDecodeValue (RawVarint w) = Right (w /= 0)
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- Float: fixed32
instance ProtoDecode Float where
  protoDecodeValue (RawFixed32 w) =
    Right (castWord32ToFloat w)
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireFixed32 WireVarint)
  {-# INLINE protoDecodeValue #-}

-- Double: fixed64
instance ProtoDecode Double where
  protoDecodeValue (RawFixed64 w) =
    Right (castWord64ToDouble w)
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireFixed64 WireVarint)
  {-# INLINE protoDecodeValue #-}

-- Text: length-delimited, UTF-8 decoded
instance ProtoDecode Text where
  protoDecodeValue (RawBytes bs) =
    case TE.decodeUtf8' bs of
      Left _  -> Left InvalidUtf8
      Right t -> Right t
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)
  {-# INLINE protoDecodeValue #-}

-- ByteString: length-delimited, zero-copy slice
instance ProtoDecode ByteString where
  protoDecodeValue (RawBytes bs) = Right bs
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)
  {-# INLINE protoDecodeValue #-}

-- SInt32: varint with zigzag decoding
instance ProtoDecode SInt32 where
  protoDecodeValue (RawVarint w) =
    Right (SInt32 (fromIntegral (zigzagDecode w)))
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}

-- SInt64: varint with zigzag decoding
instance ProtoDecode SInt64 where
  protoDecodeValue (RawVarint w) =
    Right (SInt64 (zigzagDecode w))
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireVarint WireLengthDelimited)
  {-# INLINE protoDecodeValue #-}


-- ===================================================================
-- Packed repeated field decoding
-- ===================================================================

-- | Decode packed repeated scalar values from a 'RawBytes' blob.
-- In proto3, repeated scalar fields may be encoded as a single
-- length-delimited blob containing all values concatenated.
-- Returns 'Left' if the blob cannot be fully consumed.
decodePacked :: ProtoDecode a => (ByteString -> Maybe (a, ByteString)) -> RawField -> Either DecodeError [a]
decodePacked parser (RawBytes bs) = go bs []
  where
    go remaining acc
      | BS.null remaining = Right (reverse acc)
      | otherwise = case parser remaining of
          Just (val, rest) -> go rest (val : acc)
          Nothing -> Left TruncatedInput
decodePacked _ _ = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)


-- ===================================================================
-- DecodeRepeatedEntry: handles both packed and unpacked entries
-- ===================================================================

-- | Decode a single raw field entry as one or more values of type @a@.
--
-- For packable scalar types (Int32, Int64, Word32, Word64, Bool, Float,
-- Double, SInt32, SInt64), a 'RawBytes' entry is treated as a packed
-- blob containing multiple concatenated values. For non-packable types
-- (Text, ByteString, submessages), 'RawBytes' is decoded as a single
-- value.
--
-- This class is used by the Generics decoder for repeated fields to
-- transparently handle both packed and unpacked wire encodings.
class DecodeRepeatedEntry a where
  decodeRepeatedEntry :: RawField -> Either DecodeError [a]

-- | Default: non-packable types. Decode as a single value.
instance {-# OVERLAPPABLE #-} ProtoDecode a => DecodeRepeatedEntry a where
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

-- Varint-based packable scalars: Int32, Int64, Word32, Word64, Bool

instance DecodeRepeatedEntry Int32 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

instance DecodeRepeatedEntry Int64 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

instance DecodeRepeatedEntry Word32 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

instance DecodeRepeatedEntry Word64 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

instance DecodeRepeatedEntry Bool where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

-- Fixed32-based: Float
instance DecodeRepeatedEntry Float where
  decodeRepeatedEntry (RawBytes bs) = unpackFixed32s bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

-- Fixed64-based: Double
instance DecodeRepeatedEntry Double where
  decodeRepeatedEntry (RawBytes bs) = unpackFixed64s bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

-- Varint-based with zigzag: SInt32, SInt64
instance DecodeRepeatedEntry SInt32 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}

instance DecodeRepeatedEntry SInt64 where
  decodeRepeatedEntry (RawBytes bs) = unpackVarints bs
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}


-- ===================================================================
-- Unpacking helpers
-- ===================================================================

-- | Unpack varints from a packed blob, decoding each via 'ProtoDecode'.
unpackVarints :: ProtoDecode a => ByteString -> Either DecodeError [a]
unpackVarints = go []
  where
    go !acc bs
      | BS.null bs = Right (reverse acc)
      | otherwise = case decodeVarint bs of
          Just (w, rest) -> case protoDecodeValue (RawVarint w) of
            Right val -> go (val : acc) rest
            Left err  -> Left err
          Nothing -> Left TruncatedInput
{-# INLINE unpackVarints #-}

-- | Unpack fixed32 values from a packed blob.
unpackFixed32s :: ProtoDecode a => ByteString -> Either DecodeError [a]
unpackFixed32s = go []
  where
    go !acc bs
      | BS.null bs = Right (reverse acc)
      | otherwise = case decodeFixed32 bs of
          Just (w, rest) -> case protoDecodeValue (RawFixed32 w) of
            Right val -> go (val : acc) rest
            Left err  -> Left err
          Nothing -> Left TruncatedInput
{-# INLINE unpackFixed32s #-}

-- | Unpack fixed64 values from a packed blob.
unpackFixed64s :: ProtoDecode a => ByteString -> Either DecodeError [a]
unpackFixed64s = go []
  where
    go !acc bs
      | BS.null bs = Right (reverse acc)
      | otherwise = case decodeFixed64 bs of
          Just (w, rest) -> case protoDecodeValue (RawFixed64 w) of
            Right val -> go (val : acc) rest
            Left err  -> Left err
          Nothing -> Left TruncatedInput
{-# INLINE unpackFixed64s #-}