valiant-0.1.0.0: src/Valiant/Binary/Range.hs
-- | Binary encode/decode for PostgreSQL range types.
--
-- PG binary format for ranges:
-- 1 byte: flags
-- 0x01 = empty
-- 0x02 = has lower bound
-- 0x04 = has upper bound
-- 0x08 = lower bound inclusive
-- 0x10 = upper bound inclusive
-- if has lower bound:
-- 4 bytes: lower bound data length
-- N bytes: lower bound data
-- if has upper bound:
-- 4 bytes: upper bound data length
-- N bytes: upper bound data
module Valiant.Binary.Range
( PgRange (..)
, RangeBound (..)
, pgEncodeRange
, pgDecodeRange
) where
import Data.Bits (shiftL, testBit, (.|.))
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 (Word8)
-- | A bound of a range.
data RangeBound a
= Inclusive a
| Exclusive a
deriving stock (Show, Eq, Ord)
-- | A PostgreSQL range value.
data PgRange a
= EmptyRange
| PgRange (Maybe (RangeBound a)) (Maybe (RangeBound a))
-- ^ Lower bound, upper bound. Nothing means unbounded.
deriving stock (Show, Eq)
-- Flags
flagEmpty, flagHasLower, flagHasUpper, flagLowerInc, flagUpperInc :: Word8
flagEmpty = 0x01
flagHasLower = 0x02
flagHasUpper = 0x04
flagLowerInc = 0x08
flagUpperInc = 0x10
-- | Encode a range value given an element encoder.
pgEncodeRange :: (a -> ByteString) -> PgRange a -> ByteString
pgEncodeRange _ EmptyRange =
BS.singleton flagEmpty
pgEncodeRange enc (PgRange mLower mUpper) =
LBS.toStrict . B.toLazyByteString $
B.word8 flags
<> encodeBound enc mLower
<> encodeBound enc mUpper
where
flags =
(if hasBound mLower then flagHasLower else 0)
.|. (if hasBound mUpper then flagHasUpper else 0)
.|. (if isInclusive mLower then flagLowerInc else 0)
.|. (if isInclusive mUpper then flagUpperInc else 0)
hasBound Nothing = False
hasBound (Just _) = True
isInclusive (Just (Inclusive _)) = True
isInclusive _ = False
encodeBound :: (a -> ByteString) -> Maybe (RangeBound a) -> B.Builder
encodeBound _ Nothing = mempty
encodeBound enc (Just bound) =
let bs = enc (boundValue bound)
in B.int32BE (fromIntegral (BS.length bs)) <> B.byteString bs
boundValue :: RangeBound a -> a
boundValue (Inclusive a) = a
boundValue (Exclusive a) = a
-- | Decode a range value given an element decoder.
pgDecodeRange :: (ByteString -> Either String a) -> ByteString -> Either String (PgRange a)
pgDecodeRange _ bs
| BS.null bs = Left "range: empty input"
pgDecodeRange dec bs = do
let flags = BS.index bs 0
if testBit flags 0
then Right EmptyRange
else do
let hasLower = testBit flags 1
hasUpper = testBit flags 2
lowerInc = testBit flags 3
upperInc = testBit flags 4
(mLower, off1) <-
if hasLower
then do
(val, off) <- decodeBoundAt dec bs 1
let bound = if lowerInc then Inclusive val else Exclusive val
Right (Just bound, off)
else Right (Nothing, 1)
(mUpper, _) <-
if hasUpper
then do
(val, off) <- decodeBoundAt dec bs off1
let bound = if upperInc then Inclusive val else Exclusive val
Right (Just bound, off)
else Right (Nothing, off1)
Right (PgRange mLower mUpper)
decodeBoundAt :: (ByteString -> Either String a) -> ByteString -> Int -> Either String (a, Int)
decodeBoundAt dec bs off
| off + 4 > BS.length bs = Left "range: unexpected end reading bound length"
| otherwise = do
len <- decodeInt32At bs off
let dataOff = off + 4
dataEnd = dataOff + fromIntegral len
if dataEnd > BS.length bs
then Left "range: unexpected end reading bound data"
else do
let val = BS.take (fromIntegral len) (BS.drop dataOff bs)
a <- dec val
Right (a, dataEnd)
decodeInt32At :: ByteString -> Int -> Either String Int32
decodeInt32At bs off
| off + 4 > BS.length bs = Left "range: 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)