packages feed

valiant-0.1.0.0: src/Valiant/Binary/Composite.hs

-- | Binary encode/decode for PostgreSQL composite (row) types.
--
-- PG binary format for composite values:
--   4 bytes: number of fields (Int32)
--   per field:
--     4 bytes: field type OID (Word32)
--     4 bytes: field data length (-1 for NULL)
--     N bytes: field data
module Valiant.Binary.Composite
  ( pgEncodeComposite
  , pgDecodeComposite
  , CompositeField (..)
  ) where

import Data.Bits (shiftL, (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as B
import Data.ByteString.Lazy qualified as LBS
import Data.Int (Int32)
import Data.Word (Word32)

-- | A single field in a composite value.
data CompositeField = CompositeField
  { cfOid :: Word32
  , cfValue :: Maybe ByteString
  -- ^ Nothing for NULL
  }
  deriving stock (Show, Eq)

-- | Encode a list of composite fields to PG binary format.
pgEncodeComposite :: [CompositeField] -> ByteString
pgEncodeComposite fields =
  LBS.toStrict . B.toLazyByteString $
    B.int32BE (fromIntegral (length fields))
      <> foldMap encodeField fields
  where
    encodeField (CompositeField oid Nothing) =
      B.word32BE oid <> B.int32BE (-1)
    encodeField (CompositeField oid (Just bs)) =
      B.word32BE oid
        <> B.int32BE (fromIntegral (BS.length bs))
        <> B.byteString bs

-- | Decode PG binary composite format to a list of fields.
pgDecodeComposite :: ByteString -> Either String [CompositeField]
pgDecodeComposite bs
  | BS.length bs < 4 = Left "composite: header too short"
  | otherwise = do
      nfields <- decodeInt32At bs 0
      decodeFields bs 4 (fromIntegral nfields)

decodeFields :: ByteString -> Int -> Int -> Either String [CompositeField]
decodeFields _ _ 0 = Right []
decodeFields bs off n
  | off + 8 > BS.length bs = Left "composite: unexpected end reading field header"
  | otherwise = do
      oid <- decodeWord32At bs off
      len <- decodeInt32At bs (off + 4)
      if len == -1
        then do
          rest <- decodeFields bs (off + 8) (n - 1)
          Right (CompositeField oid Nothing : rest)
        else do
          let dataOff = off + 8
              dataEnd = dataOff + fromIntegral len
          when (dataEnd > BS.length bs) $
            Left "composite: unexpected end reading field data"
          let val = BS.take (fromIntegral len) (BS.drop dataOff bs)
          rest <- decodeFields bs dataEnd (n - 1)
          Right (CompositeField oid (Just val) : rest)

-- Helpers -------------------------------------------------------------------

decodeInt32At :: ByteString -> Int -> Either String Int32
decodeInt32At bs off
  | off + 4 > BS.length bs = Left "composite: int32 out of bounds"
  | otherwise =
      let b0 = fromIntegral (BS.index bs off) :: Int32
          b1 = fromIntegral (BS.index bs (off + 1)) :: Int32
          b2 = fromIntegral (BS.index bs (off + 2)) :: Int32
          b3 = fromIntegral (BS.index bs (off + 3)) :: Int32
       in Right (b0 `shiftL` 24 .|. b1 `shiftL` 16 .|. b2 `shiftL` 8 .|. b3)

decodeWord32At :: ByteString -> Int -> Either String Word32
decodeWord32At bs off = fromIntegral <$> decodeInt32At bs off

when :: Bool -> Either String () -> Either String ()
when True e = e
when False _ = Right ()