wireform-proto-0.2.0.0: src/Proto/Extension.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
{- | Proto2 extensions runtime support.
Proto2 @extend Foo { optional int32 bar = 123; }@ declarations
expose typed accessors for fields whose numbers live in a message's
declared extension ranges. At the Haskell level we preserve those
fields through the existing unknown-field machinery in
"Proto.Decode" — each extension becomes a typed @'Extension' msg a@
descriptor that knows how to read the corresponding entry out of a
message's unknown-fields list and how to write it back in.
Typical generated shape:
@
-- In @Foo.hs@:
data Foo = Foo { fooUnknownFields :: ![UnknownField], ... }
-- In the module that contains the @extend Foo@ block:
barExt :: Extension Foo Int32
barExt = Extension
{ extNumber = 123
, extType = ExtInt32
}
@
Callers then use 'getExtension' / 'setExtension' / 'clearExtension'
to read, write, and remove the value.
-}
module Proto.Extension (
-- * Extension descriptors
Extension (..),
ExtensionType (..),
-- * Accessing extension values on a message
HasExtensions (..),
hasExtension,
getExtension,
setExtension,
clearExtension,
-- * Repeated extensions
RepeatedExtension (..),
getRepeatedExtension,
setRepeatedExtension,
appendRepeatedExtension,
clearRepeatedExtension,
-- * Internal helpers
{- | Reused by "Proto.Internal.JSON.Extension" to bridge the
bracket-quoted JSON @[FQN]@ extension key syntax through
the same encoder \/ decoder used for typed extension
accessors.
-}
encodeExtensionValue,
decodeExtensionValue,
unknownFieldNumber,
) where
import Data.Bits (shiftL, shiftR, xor, (.&.), (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BS8
import Data.Int (Int32, Int64)
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Word (Word32, Word64)
import GHC.Float (
castDoubleToWord64,
castFloatToWord32,
castWord32ToFloat,
castWord64ToDouble,
)
import Proto.Decode (UnknownField (..))
-- ============================================================
-- Types
-- ============================================================
{- | A typed extension descriptor. Carries the wire-level field
number plus the type information needed to decode/encode the
value and tie it to the right 'UnknownField' constructor.
-}
data Extension msg a = Extension
{ extNumber :: !Int
-- ^ The field number declared in the @extend@ block.
, extType :: !(ExtensionType a)
-- ^ Payload type information — drives the decoder + encoder.
}
deriving stock instance Show (ExtensionType a) => Show (Extension msg a)
{- | Payload types supported by extensions today. Covers every
singular scalar proto2 supports plus an embedded message escape
hatch that round-trips raw bytes. Repeated / packed extensions
aren't covered here; the generated 'Extension' for those would
need a small additional wrapper.
-}
data ExtensionType a where
ExtInt32 :: ExtensionType Int32
ExtInt64 :: ExtensionType Int64
ExtUInt32 :: ExtensionType Word32
ExtUInt64 :: ExtensionType Word64
ExtSInt32 :: ExtensionType Int32
ExtSInt64 :: ExtensionType Int64
ExtFixed32 :: ExtensionType Word32
ExtFixed64 :: ExtensionType Word64
ExtSFixed32 :: ExtensionType Int32
ExtSFixed64 :: ExtensionType Int64
ExtFloat :: ExtensionType Float
ExtDouble :: ExtensionType Double
ExtBool :: ExtensionType Bool
ExtString :: ExtensionType Text
ExtBytes :: ExtensionType ByteString
ExtMessage :: ExtensionType ByteString
{- ^ Sub-message stored as its raw length-delimited payload;
callers use 'Proto.Decode.decodeMessage' to re-project.
-}
deriving stock instance Show (ExtensionType a)
-- ============================================================
-- Accessors
-- ============================================================
{- | Lens-like access to the unknown-fields list on a message.
Generated message types that carry extensions provide an instance
of this class; the three combinators below are written once in
terms of the instance.
-}
class HasExtensions msg where
messageUnknownFields :: msg -> [UnknownField]
setMessageUnknownFields :: [UnknownField] -> msg -> msg
-- | 'True' when the message carries a value for this extension.
hasExtension :: HasExtensions msg => Extension msg a -> msg -> Bool
hasExtension ext msg =
any
(\uf -> unknownFieldNumber uf == extNumber ext)
(messageUnknownFields msg)
{- | Retrieve an extension value. Returns 'Nothing' when the field
is absent or the stored bytes don't fit the declared payload
type (corruption or aliasing — real deployments treat the
missing case as "use the extension's proto default", which
callers can layer on top).
-}
getExtension :: HasExtensions msg => Extension msg a -> msg -> Maybe a
getExtension ext msg = do
uf <- lookupField (extNumber ext) (messageUnknownFields msg)
decodeExtensionValue (extType ext) uf
{- | Attach (or overwrite) an extension value. The underlying
unknown-fields list has any prior entries for the same field
number removed before the new one is appended, matching
protobuf's "last one wins" semantics for singular fields.
-}
setExtension
:: HasExtensions msg => Extension msg a -> a -> msg -> msg
setExtension ext value msg =
let !fresh = encodeExtensionValue (extNumber ext) (extType ext) value
!rest =
filter
(\uf -> unknownFieldNumber uf /= extNumber ext)
(messageUnknownFields msg)
in setMessageUnknownFields (rest ++ [fresh]) msg
{- | Remove an extension value. Leaves the message unchanged when
the extension wasn't set.
-}
clearExtension
:: HasExtensions msg => Extension msg a -> msg -> msg
clearExtension ext msg =
setMessageUnknownFields
( filter
(\uf -> unknownFieldNumber uf /= extNumber ext)
(messageUnknownFields msg)
)
msg
-- ============================================================
-- Internal
-- ============================================================
-- | Extract the field number from an 'UnknownField'.
unknownFieldNumber :: UnknownField -> Int
unknownFieldNumber = \case
UnknownVarint n _ -> n
UnknownFixed64 n _ -> n
UnknownLenDelim n _ -> n
UnknownFixed32 n _ -> n
-- The proto spec says later occurrences of a singular field
-- override earlier ones, so we walk from the tail.
lookupField :: Int -> [UnknownField] -> Maybe UnknownField
lookupField fn = go . reverse
where
go [] = Nothing
go (uf : rest)
| unknownFieldNumber uf == fn = Just uf
| otherwise = go rest
-- | Decode a typed value from an 'UnknownField' using the given 'ExtensionType'.
decodeExtensionValue :: ExtensionType a -> UnknownField -> Maybe a
decodeExtensionValue ty uf = case (ty, uf) of
(ExtInt32, UnknownVarint _ v) -> Just (fromIntegral v)
(ExtInt64, UnknownVarint _ v) -> Just (fromIntegral v)
(ExtUInt32, UnknownVarint _ v) -> Just (fromIntegral v)
(ExtUInt64, UnknownVarint _ v) -> Just v
(ExtSInt32, UnknownVarint _ v) -> Just (zigzagDecode32 v)
(ExtSInt64, UnknownVarint _ v) -> Just (zigzagDecode64 v)
(ExtBool, UnknownVarint _ v) -> Just (v /= 0)
(ExtFixed32, UnknownFixed32 _ v) -> Just v
(ExtSFixed32, UnknownFixed32 _ v) -> Just (fromIntegral v)
(ExtFloat, UnknownFixed32 _ v) -> Just (castWord32ToFloat v)
(ExtFixed64, UnknownFixed64 _ v) -> Just v
(ExtSFixed64, UnknownFixed64 _ v) -> Just (fromIntegral v)
(ExtDouble, UnknownFixed64 _ v) -> Just (castWord64ToDouble v)
(ExtString, UnknownLenDelim _ b) -> case TE.decodeUtf8' b of
Right t -> Just t
Left _ -> Nothing
(ExtBytes, UnknownLenDelim _ b) -> Just b
(ExtMessage, UnknownLenDelim _ b) -> Just b
_ -> Nothing
-- | Encode a typed value into an 'UnknownField' for the given field number and type.
encodeExtensionValue :: Int -> ExtensionType a -> a -> UnknownField
encodeExtensionValue fn ty value = case ty of
ExtInt32 -> UnknownVarint fn (fromIntegral value)
ExtInt64 -> UnknownVarint fn (fromIntegral value)
ExtUInt32 -> UnknownVarint fn (fromIntegral value)
ExtUInt64 -> UnknownVarint fn value
ExtSInt32 -> UnknownVarint fn (zigzagEncode32 value)
ExtSInt64 -> UnknownVarint fn (zigzagEncode64 value)
ExtBool -> UnknownVarint fn (if value then 1 else 0)
ExtFixed32 -> UnknownFixed32 fn value
ExtSFixed32 -> UnknownFixed32 fn (fromIntegral value)
ExtFloat -> UnknownFixed32 fn (castFloatToWord32 value)
ExtFixed64 -> UnknownFixed64 fn value
ExtSFixed64 -> UnknownFixed64 fn (fromIntegral value)
ExtDouble -> UnknownFixed64 fn (castDoubleToWord64 value)
ExtString -> UnknownLenDelim fn (TE.encodeUtf8 value)
ExtBytes -> UnknownLenDelim fn value
ExtMessage -> UnknownLenDelim fn value
-- ============================================================
-- Repeated extensions
-- ============================================================
{- | A typed repeated-extension descriptor. The 'reIsPacked' flag
selects between protobuf's two repeated-on-the-wire encodings:
* 'False' (the proto2 default): one wire entry per element,
all sharing the same field number.
* 'True' (the proto3 default for fixed-width scalars; opt-in
in proto2 via @[packed = true]@): a single
length-delimited entry whose payload is the concatenation
of every element. Only valid for fixed-width scalar types
(varint integers, fixed32/64, float/double, bool); strings,
bytes, and submessages always use the unpacked encoding.
-}
data RepeatedExtension msg a = RepeatedExtension
{ reNumber :: !Int
, reType :: !(ExtensionType a)
, reIsPacked :: !Bool
}
deriving stock instance Show (ExtensionType a) => Show (RepeatedExtension msg a)
{- | Read every value associated with a repeated extension, in
wire order (which matches the order the user wrote them). Both
packed and unpacked encodings are accepted regardless of
'reIsPacked'; protobuf parsers must honour either form on read.
-}
getRepeatedExtension
:: HasExtensions msg => RepeatedExtension msg a -> msg -> [a]
getRepeatedExtension ext msg =
concatMap
decodeOne
[ uf
| uf <- messageUnknownFields msg
, unknownFieldNumber uf == reNumber ext
]
where
decodeOne uf = case (reType ext, uf) of
-- Packed scalars can show up as a single UnknownLenDelim.
(ty, UnknownLenDelim _ payload)
| not (isLenDelimNative ty) -> decodePacked ty payload
-- Otherwise, decode as a single unpacked entry.
(ty, _) ->
case decodeExtensionValue ty uf of
Just v -> [v]
Nothing -> []
{- | Replace every occurrence of the extension with the given list,
in order. Uses packed or unpacked encoding per 'reIsPacked' (the
former requires fixed-width scalar types).
-}
setRepeatedExtension
:: HasExtensions msg => RepeatedExtension msg a -> [a] -> msg -> msg
setRepeatedExtension ext values msg =
let !rest =
filter
(\uf -> unknownFieldNumber uf /= reNumber ext)
(messageUnknownFields msg)
!fresh =
if reIsPacked ext && isPackable (reType ext)
then [packRepeated (reNumber ext) (reType ext) values]
else map (encodeExtensionValue (reNumber ext) (reType ext)) values
in setMessageUnknownFields (rest ++ fresh) msg
{- | Append one element to a repeated extension. Always produces an
unpacked entry; combine with 'setRepeatedExtension' to repack.
-}
appendRepeatedExtension
:: HasExtensions msg => RepeatedExtension msg a -> a -> msg -> msg
appendRepeatedExtension ext value msg =
let !uf = encodeExtensionValue (reNumber ext) (reType ext) value
in setMessageUnknownFields
(messageUnknownFields msg ++ [uf])
msg
-- | Drop every entry for this repeated extension.
clearRepeatedExtension
:: HasExtensions msg => RepeatedExtension msg a -> msg -> msg
clearRepeatedExtension ext msg =
setMessageUnknownFields
( filter
(\uf -> unknownFieldNumber uf /= reNumber ext)
(messageUnknownFields msg)
)
msg
{- | Whether the given type's wire form is itself
length-delimited. Only @string@, @bytes@, and @message@ are.
-}
isLenDelimNative :: ExtensionType a -> Bool
isLenDelimNative = \case
ExtString -> True
ExtBytes -> True
ExtMessage -> True
_ -> False
{- | Whether a packed encoding is permitted for this type. Per the
protobuf spec only fixed-width scalars and varint integers are
packable.
-}
isPackable :: ExtensionType a -> Bool
isPackable ty = not (isLenDelimNative ty)
{- | Decode the body of a packed-format @UnknownLenDelim@ entry
into a list of values.
-}
decodePacked :: ExtensionType a -> ByteString -> [a]
decodePacked ty bs = case ty of
ExtInt32 -> map fromIntegral (varintList bs)
ExtInt64 -> map fromIntegral (varintList bs)
ExtUInt32 -> map fromIntegral (varintList bs)
ExtUInt64 -> varintList bs
ExtSInt32 -> map zigzagDecode32 (varintList bs)
ExtSInt64 -> map zigzagDecode64 (varintList bs)
ExtBool -> map (/= 0) (varintList bs)
ExtFixed32 -> chunked 4 readU32 bs
ExtSFixed32 -> map fromIntegral (chunked 4 readU32 bs)
ExtFloat -> map castWord32ToFloat (chunked 4 readU32 bs)
ExtFixed64 -> chunked 8 readU64 bs
ExtSFixed64 -> map fromIntegral (chunked 8 readU64 bs)
ExtDouble -> map castWord64ToDouble (chunked 8 readU64 bs)
-- LEN-delimited types can't be packed; treat the whole payload
-- as one unpacked element.
ExtString -> case TE.decodeUtf8' bs of
Right t -> [t]
Left _ -> []
ExtBytes -> [bs]
ExtMessage -> [bs]
-- | Pack a list of values into a single 'UnknownLenDelim' entry.
packRepeated :: Int -> ExtensionType a -> [a] -> UnknownField
packRepeated fn ty values =
let !payload = packedPayload ty values
in UnknownLenDelim fn payload
packedPayload :: ExtensionType a -> [a] -> ByteString
packedPayload ty values =
BS8.concat $ map encodeOne values
where
encodeOne v = case ty of
ExtInt32 -> encodeVarint (fromIntegral v)
ExtInt64 -> encodeVarint (fromIntegral v)
ExtUInt32 -> encodeVarint (fromIntegral v)
ExtUInt64 -> encodeVarint v
ExtSInt32 -> encodeVarint (zigzagEncode32 v)
ExtSInt64 -> encodeVarint (zigzagEncode64 v)
ExtBool -> encodeVarint (if v then 1 else 0)
ExtFixed32 -> writeU32 v
ExtSFixed32 -> writeU32 (fromIntegral v)
ExtFloat -> writeU32 (castFloatToWord32 v)
ExtFixed64 -> writeU64 v
ExtSFixed64 -> writeU64 (fromIntegral v)
ExtDouble -> writeU64 (castDoubleToWord64 v)
_ -> BS.empty -- shouldn't be reachable: isPackable guards
varintList :: ByteString -> [Word64]
varintList = go
where
go bs
| BS.null bs = []
| otherwise = case readVarint bs of
Nothing -> []
Just (v, rest) -> v : go rest
readVarint :: ByteString -> Maybe (Word64, ByteString)
readVarint = step 0 0
where
step !acc !shift bs
| BS.null bs = Nothing
| otherwise =
let b = BS.head bs
acc' = acc + (fromIntegral (b .&. 0x7F) `shiftL` shift)
in if b < 0x80
then Just (acc', BS.tail bs)
else step acc' (shift + 7) (BS.tail bs)
encodeVarint :: Word64 -> ByteString
encodeVarint n0 = BS.pack (go n0)
where
go n
| n < 0x80 = [fromIntegral n]
| otherwise = (fromIntegral (n .&. 0x7F) .|. 0x80) : go (n `shiftR` 7)
writeU32 :: Word32 -> ByteString
writeU32 w =
BS.pack
[ fromIntegral w
, fromIntegral (w `shiftR` 8)
, fromIntegral (w `shiftR` 16)
, fromIntegral (w `shiftR` 24)
]
writeU64 :: Word64 -> ByteString
writeU64 w =
BS.pack
[ fromIntegral w
, fromIntegral (w `shiftR` 8)
, fromIntegral (w `shiftR` 16)
, fromIntegral (w `shiftR` 24)
, fromIntegral (w `shiftR` 32)
, fromIntegral (w `shiftR` 40)
, fromIntegral (w `shiftR` 48)
, fromIntegral (w `shiftR` 56)
]
readU32 :: ByteString -> Word32
readU32 bs =
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 b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24)
readU64 :: ByteString -> Word64
readU64 bs =
let b0 = fromIntegral (BS.index bs 0) :: Word64
b1 = fromIntegral (BS.index bs 1) :: Word64
b2 = fromIntegral (BS.index bs 2) :: Word64
b3 = fromIntegral (BS.index bs 3) :: Word64
b4 = fromIntegral (BS.index bs 4) :: Word64
b5 = fromIntegral (BS.index bs 5) :: Word64
b6 = fromIntegral (BS.index bs 6) :: Word64
b7 = fromIntegral (BS.index bs 7) :: Word64
in b0
.|. (b1 `shiftL` 8)
.|. (b2 `shiftL` 16)
.|. (b3 `shiftL` 24)
.|. (b4 `shiftL` 32)
.|. (b5 `shiftL` 40)
.|. (b6 `shiftL` 48)
.|. (b7 `shiftL` 56)
chunked :: Int -> (ByteString -> a) -> ByteString -> [a]
chunked n decode = go
where
go bs
| BS.length bs < n = []
| otherwise =
decode (BS.take n bs) : go (BS.drop n bs)
-- Zig-zag encodings per the protobuf spec.
zigzagEncode32 :: Int32 -> Word64
zigzagEncode32 n =
let !w = fromIntegral n :: Word32
in fromIntegral ((w `shiftL` 1) `xor` fromIntegral (n `shiftR` 31))
zigzagEncode64 :: Int64 -> Word64
zigzagEncode64 n =
let !w = fromIntegral n :: Word64
in (w `shiftL` 1) `xor` fromIntegral (n `shiftR` 63)
zigzagDecode32 :: Word64 -> Int32
zigzagDecode32 v =
let !w = fromIntegral v :: Word32
in fromIntegral ((w `shiftR` 1) `xor` negate (w .&. 1))
zigzagDecode64 :: Word64 -> Int64
zigzagDecode64 v =
fromIntegral ((v `shiftR` 1) `xor` negate (v .&. 1))