wireform-thrift-0.1.0.0: src/Thrift/Message.hs
{-# LANGUAGE BangPatterns #-}
{- | Thrift RPC message header encoding\/decoding for Binary and Compact protocols.
A Thrift RPC message wraps a struct payload with a header containing the
method name, message type (call\/reply\/exception\/oneway), and a sequence ID.
-}
module Thrift.Message (
ThriftMessageType (..),
ThriftMessage (..),
encodeMessageBinary,
decodeMessageBinary,
encodeMessageCompact,
decodeMessageCompact,
) where
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.ByteString.Unsafe qualified as BSU
import Data.Int (Int32)
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Word (Word32, Word64, Word8)
import Thrift.Decode (decodeBinary, decodeCompact)
import Thrift.Encode (encodeBinary, encodeCompact)
import Thrift.Value qualified as TV
import Wireform.Builder qualified as B
-- | Thrift RPC message types.
data ThriftMessageType
= TMsgCall
| TMsgReply
| TMsgException
| TMsgOneway
deriving stock (Show, Eq, Ord, Enum, Bounded)
-- | A complete Thrift RPC message: header + payload.
data ThriftMessage = ThriftMessage
{ tmsgName :: !Text
, tmsgType :: !ThriftMessageType
, tmsgSeqId :: {-# UNPACK #-} !Int32
, tmsgPayload :: !TV.Value
}
deriving stock (Show, Eq)
msgTypeToWord8 :: ThriftMessageType -> Word8
msgTypeToWord8 TMsgCall = 1
msgTypeToWord8 TMsgReply = 2
msgTypeToWord8 TMsgException = 3
msgTypeToWord8 TMsgOneway = 4
{-# INLINE msgTypeToWord8 #-}
msgTypeFromWord8 :: Word8 -> Maybe ThriftMessageType
msgTypeFromWord8 1 = Just TMsgCall
msgTypeFromWord8 2 = Just TMsgReply
msgTypeFromWord8 3 = Just TMsgException
msgTypeFromWord8 4 = Just TMsgOneway
msgTypeFromWord8 _ = Nothing
{-# INLINE msgTypeFromWord8 #-}
--------------------------------------------------------------------------------
-- Binary Protocol
--------------------------------------------------------------------------------
binaryVersion :: Word32
binaryVersion = 0x80010000
encodeMessageBinary :: ThriftMessage -> ByteString
encodeMessageBinary (ThriftMessage name mtype seqid payload) =
BL.toStrict $
B.toLazyByteString $
let !nameBytes = TE.encodeUtf8 name
!version = binaryVersion .|. fromIntegral (msgTypeToWord8 mtype)
in putBE32 version
<> putBE32 (fromIntegral (BS.length nameBytes))
<> B.byteString nameBytes
<> putBE32i (fromIntegral seqid)
<> B.byteString (encodeBinary payload)
decodeMessageBinary :: ByteString -> Either String ThriftMessage
decodeMessageBinary !bs
| BS.length bs < 4 = Left "decodeMessageBinary: insufficient data"
| otherwise =
let !w = getBE32 bs 0
in if w .&. 0x80000000 /= 0
then decodeStrictBinary bs w
else decodeOldBinary bs
decodeStrictBinary :: ByteString -> Word32 -> Either String ThriftMessage
decodeStrictBinary !bs !versionWord =
let !mtypeByte = fromIntegral (versionWord .&. 0xFF) :: Word8
in case msgTypeFromWord8 mtypeByte of
Nothing -> Left $ "decodeMessageBinary: invalid message type: " ++ show mtypeByte
Just !mtype -> do
(nameBytes, off1) <- getStr bs 4
case TE.decodeUtf8' nameBytes of
Left _ -> Left "decodeMessageBinary: invalid UTF-8 in method name"
Right name -> do
seqid <- getI32At bs off1
let !payloadBs = BS.drop (off1 + 4) bs
payload <- decodeBinary payloadBs
Right (ThriftMessage name mtype seqid payload)
decodeOldBinary :: ByteString -> Either String ThriftMessage
decodeOldBinary !bs = do
(nameBytes, off1) <- getStr bs 0
case TE.decodeUtf8' nameBytes of
Left _ -> Left "decodeMessageBinary: invalid UTF-8 in method name"
Right name -> do
if off1 >= BS.length bs
then Left "decodeMessageBinary: missing message type byte"
else do
let !mtypeByte = BSU.unsafeIndex bs off1
case msgTypeFromWord8 mtypeByte of
Nothing -> Left $ "decodeMessageBinary: invalid message type: " ++ show mtypeByte
Just !mtype -> do
seqid <- getI32At bs (off1 + 1)
let !payloadBs = BS.drop (off1 + 5) bs
payload <- decodeBinary payloadBs
Right (ThriftMessage name mtype seqid payload)
--------------------------------------------------------------------------------
-- Compact Protocol
--------------------------------------------------------------------------------
compactProtocolId :: Word8
compactProtocolId = 0x82
compactVersion :: Word8
compactVersion = 1
encodeMessageCompact :: ThriftMessage -> ByteString
encodeMessageCompact (ThriftMessage name mtype seqid payload) =
BL.toStrict $
B.toLazyByteString $
let !nameBytes = TE.encodeUtf8 name
!vtByte = (compactVersion .&. 0x1F) .|. (msgTypeToWord8 mtype `shiftL` 5)
in B.word8 compactProtocolId
<> B.word8 vtByte
<> putVarint (fromIntegral (fromIntegral seqid :: Word32))
<> putVarint (fromIntegral (BS.length nameBytes))
<> B.byteString nameBytes
<> B.byteString (encodeCompact payload)
decodeMessageCompact :: ByteString -> Either String ThriftMessage
decodeMessageCompact !bs
| BS.length bs < 2 = Left "decodeMessageCompact: insufficient data"
| BSU.unsafeIndex bs 0 /= compactProtocolId =
Left $ "decodeMessageCompact: invalid protocol ID: " ++ show (BSU.unsafeIndex bs 0)
| otherwise =
let !vtByte = BSU.unsafeIndex bs 1
!ver = vtByte .&. 0x1F
!mtypeBits = (vtByte `shiftR` 5) .&. 0x07
in if ver /= compactVersion
then Left $ "decodeMessageCompact: unsupported version: " ++ show ver
else case msgTypeFromWord8 mtypeBits of
Nothing -> Left $ "decodeMessageCompact: invalid message type: " ++ show mtypeBits
Just !mtype -> do
(seqidW, off1) <-
maybeToEither
"decodeMessageCompact: failed to read seqid varint"
(getVarint bs 2)
let !seqid = fromIntegral seqidW :: Int32
(nameLen, off2) <-
maybeToEither
"decodeMessageCompact: failed to read name length varint"
(getVarint bs off1)
let !nLen = fromIntegral nameLen :: Int
if off2 + nLen > BS.length bs
then Left "decodeMessageCompact: name extends past end of data"
else do
let !nameBytes = BS.take nLen (BS.drop off2 bs)
case TE.decodeUtf8' nameBytes of
Left _ -> Left "decodeMessageCompact: invalid UTF-8 in method name"
Right name -> do
let !payloadBs = BS.drop (off2 + nLen) bs
payload <- decodeCompact payloadBs
Right (ThriftMessage name mtype seqid payload)
--------------------------------------------------------------------------------
-- Internal helpers
--------------------------------------------------------------------------------
maybeToEither :: String -> Maybe a -> Either String a
maybeToEither msg Nothing = Left msg
maybeToEither _ (Just a) = Right a
{-# INLINE maybeToEither #-}
putBE32 :: Word32 -> B.Builder
putBE32 !w =
B.word8 (fromIntegral (w `shiftR` 24))
<> B.word8 (fromIntegral ((w `shiftR` 16) .&. 0xFF))
<> B.word8 (fromIntegral ((w `shiftR` 8) .&. 0xFF))
<> B.word8 (fromIntegral (w .&. 0xFF))
{-# INLINE putBE32 #-}
putBE32i :: Word32 -> B.Builder
putBE32i = putBE32
{-# INLINE putBE32i #-}
getBE32 :: ByteString -> Int -> Word32
getBE32 !bs !off =
let !b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word32
!b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word32
!b2 = fromIntegral (BSU.unsafeIndex bs (off + 2)) :: Word32
!b3 = fromIntegral (BSU.unsafeIndex bs (off + 3)) :: Word32
in (b0 `shiftL` 24) .|. (b1 `shiftL` 16) .|. (b2 `shiftL` 8) .|. b3
{-# INLINE getBE32 #-}
getStr :: ByteString -> Int -> Either String (ByteString, Int)
getStr !bs !off
| off + 4 > BS.length bs = Left "unexpected end of data reading string length"
| otherwise =
let !len = fromIntegral (getBE32 bs off) :: Int
!start = off + 4
in if len < 0 || start + len > BS.length bs
then Left "string length exceeds available data"
else Right (BS.take len (BS.drop start bs), start + len)
getI32At :: ByteString -> Int -> Either String Int32
getI32At !bs !off
| off + 4 > BS.length bs = Left "unexpected end of data reading i32"
| otherwise = Right (fromIntegral (getBE32 bs off))
putVarint :: Word64 -> B.Builder
putVarint !n
| n < 0x80 = B.word8 (fromIntegral n)
| otherwise = B.word8 (fromIntegral (n .&. 0x7F) .|. 0x80) <> putVarint (n `shiftR` 7)
{-# INLINE putVarint #-}
getVarint :: ByteString -> Int -> Maybe (Word64, Int)
getVarint !bs !off = go off 0 0
where
go !i !acc !shift
| i >= BS.length bs = Nothing
| shift >= 64 = Nothing
| otherwise =
let !b = fromIntegral (BSU.unsafeIndex bs i) :: Word64
!acc' = acc .|. ((b .&. 0x7F) `shiftL` shift)
in if b < 0x80
then Just (acc', i + 1)
else go (i + 1) acc' (shift + 7)
{-# INLINE getVarint #-}