packages feed

postgresql-types-0.1.1: src/library/PostgresqlTypes/Bytea.hs

module PostgresqlTypes.Bytea
  ( Bytea (..),

    -- * Accessors
    toByteString,

    -- * Constructors
    fromByteString,
  )
where

import qualified Data.Attoparsec.Text as Attoparsec
import qualified Data.ByteString as ByteString
import qualified Data.Text as Text
import PostgresqlTypes.Algebra
import PostgresqlTypes.Prelude
import PostgresqlTypes.Via
import qualified PtrPeeker
import qualified PtrPoker.Write as Write
import qualified TextBuilder

-- | PostgreSQL @bytea@ type. Binary data ("byte array").
--
-- [PostgreSQL docs](https://www.postgresql.org/docs/18/datatype-binary.html).
newtype Bytea = Bytea ByteString
  deriving newtype (Eq, Ord, Hashable)
  deriving (Show, Read, IsString) via (ViaIsScalar Bytea)

instance Arbitrary Bytea where
  arbitrary = Bytea . ByteString.pack <$> arbitrary
  shrink (Bytea bytes) = Bytea . ByteString.pack <$> shrink (ByteString.unpack bytes)

instance IsScalar Bytea where
  schemaName = Tagged Nothing
  typeName = Tagged "bytea"
  baseOid = Tagged (Just 17)
  arrayOid = Tagged (Just 1001)
  typeParams = Tagged []
  binaryEncoder (Bytea bs) =
    Write.byteString bs
  binaryDecoder =
    Right . Bytea <$> PtrPeeker.remainderAsByteString
  textualEncoder (Bytea bs) =
    "\\x" <> foldMap TextBuilder.hexadecimal (ByteString.unpack bs)
  textualDecoder = do
    _ <- Attoparsec.string "\\x"
    hexText <- Attoparsec.takeText
    case parseHexBytes hexText of
      Left err -> fail err
      Right bytes -> pure (Bytea bytes)
    where
      parseHexBytes :: Text -> Either String ByteString
      parseHexBytes t = ByteString.pack <$> parseHexPairs (Text.unpack t)
      parseHexPairs :: [Char] -> Either String [Word8]
      parseHexPairs [] = Right []
      parseHexPairs [_] = Left "Odd number of hex digits"
      parseHexPairs (a : b : rest) = do
        byte <- hexPairToByte a b
        (byte :) <$> parseHexPairs rest
      hexPairToByte :: Char -> Char -> Either String Word8
      hexPairToByte a b = do
        high <- hexDigitToWord8 a
        low <- hexDigitToWord8 b
        pure (high * 16 + low)
      hexDigitToWord8 :: Char -> Either String Word8
      hexDigitToWord8 c
        | c >= '0' && c <= '9' = Right (fromIntegral (ord c - ord '0'))
        | c >= 'a' && c <= 'f' = Right (fromIntegral (ord c - ord 'a' + 10))
        | c >= 'A' && c <= 'F' = Right (fromIntegral (ord c - ord 'A' + 10))
        | otherwise = Left ("Invalid hex digit: " ++ [c])

-- * Accessors

-- | Extract the underlying 'ByteString' value.
toByteString :: Bytea -> ByteString
toByteString (Bytea bs) = bs

-- * Constructors

-- | Construct a PostgreSQL 'Bytea' from a 'ByteString' value.
fromByteString :: ByteString -> Bytea
fromByteString = Bytea