packages feed

ipldm-1.0.0.0: dagcbor/IPLD/DagCBOR/Decoder/Supplemental.hs

-- | Module      : IPLD.DagCBOR.Decoder.Supplemental
--   Description : Parsing additional data from argument types.
--   Copyright   : Zoey McBride (c) 2026
--   License     : AGPL-3.0-or-later
--   Maintainer  : zoeymcbride@mailbox.org
--   Stability   : experimental
module IPLD.DagCBOR.Decoder.Supplemental
  ( -- * Type-class for implementing Supplements for Arguments.
    Supplemental (decodeSup),

    -- * Implements CBOR simple values (Bools, Nulls, FloatData)
    SimpleData (..),

    -- * Types for working with CBOR binary objects
    ListObj,
    MapObj,

    -- * DecoderMethods for supplementals
    supplementIntData,
  )
where

import Data.Bifunctor (Bifunctor (bimap, first))
import Data.Bits ((.<<.), (.|.))
import Data.ByteString qualified as LBS
import Data.ByteString.Builder qualified as Builder
import Data.Text (Text)
import Data.Text.Encoding qualified as Text
import Data.XCodec.BinaryTranscoder (BinaryTranscoder)
import Data.XCodec.BinaryTranscoder qualified as BXC
import IPLD.DM qualified as DM
import IPLD.DagCBOR.Decoder.Arguments
import IPLD.DagCBOR.Decoder.Errors
import IPLD.DagCBOR.Decoder.MajorTypes
import MultiFormats.CID (CID)
import MultiFormats.CID.Constants qualified as CID
import MultiFormats.CID.Extractor qualified as CID
import Unsafe.Coerce (unsafeCoerce)

-- | Defines arguments which have additional data after the parameter.
class (Argument a, BinaryTranscoder b) => Supplemental a t b where
  -- | Provides function for reading argument data from argument type.
  decodeSup :: a -> Okay (DecoderMethod t b)

-- | Gives back the bytes converted into `a` when minimally encoded.
supplementIntData :: (BinaryTranscoder bxc) => Int -> DecoderMethod IntData bxc
supplementIntData sizeof bytes = do
  -- Load the #bytes for the integer
  Result intdata rest <- extractBytes sizeof bytes
  -- Build the data from big-endian encoded CBOR
  let value = fromIntegral $ BXC.unpackValueBE intdata
      result = Result value rest
  -- Match the size with its minimal encoding check
  case sizeof of
    1 -- Check 8-bit encodings
      | not (minimal8 value) -> Left NotMinimal
      | otherwise -> Right result
    2 -- Check 16-bit encodings
      | not (minimal16 value) -> Left NotMinimal
      | otherwise -> Right result
    4 -- Check 32-bit encodings
      | not (minimal32 value) -> Left NotMinimal
      | otherwise -> Right result
    8 -- Check 64-bit minimal encoding
      | not (minimal64 value) -> Left NotMinimal
      | otherwise -> Right result
    _ -> error "supplementIntData: sizeof invariant failed"
  where
    -- Valid minimal-encoding ranges for different byte-sized integers
    minimal8 r = 0 <= r && r <= 0xFF
    minimal16 r = 0xFF < r && r <= 0xFFFF
    minimal32 r = 0xFFFF < r && r <= 0xFFFFFFFF
    minimal64 r = 0xFFFFFFFF < r && r <= 0xFFFFFFFFFFFFFFFF

-- | Implements reading a Number after an Ints argument. All numbers get stored
-- in a 64-bit integer for performance.
instance (BinaryTranscoder bxc) => Supplemental Ints IntData bxc where
  decodeSup arg =
    case arg of
      Ints5Bits value -> Right $ paramIntData value
      Ints8Bits -> Right $ supplementIntData 1
      Ints16Bits -> Right $ supplementIntData 2
      Ints32Bits -> Right $ supplementIntData 4
      Ints64Bits -> Right $ supplementIntData 8
    where
      -- Creates a Result from a 5-bit int parameter.
      paramIntData value bytes =
        Right $ Result (fromIntegral value :: IntData) bytes

-- | Implements reading a Supplemental collection of binary transcoder data.
instance (BinaryTranscoder b) => Supplemental Collection b b where
  decodeSup arg =
    case arg of
      CollectionsIndefinite -> Left Indefinite
      CollectionsLength ints ->
        Right $ \bytes -> do
          -- Get the function for reading the int
          intdata <- decodeSup ints :: Okay (DecoderMethod IntData b)
          -- Read the #bytes from the IntData
          Result count rest <- intdata bytes
          extractBytes (fromIntegral count) rest

-- | Implements reading a Supplemental CBOR bytestring data.
instance (BinaryTranscoder b) => Supplemental Collection DM.BytesData b where
  decodeSup arg =
    Right $ \bytes -> do
      -- Read the bytes from the transcoder data
      readbytes <- decodeSup arg :: Okay (DecoderMethod b b)
      Result bytesdata rest <- readbytes bytes
      -- Serialize the binary value into Builder
      let builder = BXC.unpackSerial bytesdata
          len = BXC.totalOctets bytesdata
      Right $ Result (DM.BytesData builder len) rest

-- | Implements reading a Supplemental collection of UTF-8 text.
instance (BinaryTranscoder bxc) => Supplemental Collection Text bxc where
  decodeSup arg =
    Right $ \bytes -> do
      -- Get the method for reading ByteStrings
      readbytes <- decodeSup arg :: Okay (DecoderMethod DM.BytesData bxc)
      -- Read the decoder result
      Result bytesdata rest <- readbytes bytes
      -- Decode the result
      Right $ Result (decodeUtf8 bytesdata) rest
    where
      -- Instansiates the Builder into a strict ByteString for Text.decodeUtf8
      decodeUtf8 (DM.BytesData builder _) =
        Text.decodeUtf8 $ LBS.toStrict (Builder.toLazyByteString builder)

-- | Iterates some monadic action `f` on the pure `a` value at most `n` times.
iterateTakeM :: (Monad m) => Int -> (a -> m a) -> a -> m [a]
iterateTakeM n f = sequence . take n . drop 1 . iterate (f =<<) . pure

-- | Stores length information for a parsed CBOR object.
data Obj bxc = Obj IntData bxc

-- | Removes the CBOR object from the bytes.
nextObj :: (BinaryTranscoder bxc) => bxc -> Okay (Obj bxc, bxc)
nextObj bytes = do
  Result size _ <- majorTypeSize bytes
  isize <- downcastIntData size
  let (hi, lo) = BXC.splitOffset isize bytes
  Right (Obj size hi, lo)

-- | Folds the resulting list of CBOR objects from the iteration list.
foldObjs :: bxc -> [(Obj a, bxc)] -> Result (ListObj a) bxc
foldObjs start = foldr buildResult (Result (0, []) start)
  where
    -- Returns true if the result is the initial accumulator to foldr
    isInitial (Result (size, _) _) = size == 0
    -- Builds the Result from the (Obj a, bxc) list fold.
    buildResult (Obj len bxc, afterobjs) res
      -- If this is the first item, this is the bytestring with all the objects
      -- removed, so update the pointer in the second item in Result.
      | isInitial res = bimap (bimap (len +) (bxc :)) (const afterobjs) res
      -- Otherwise, only update the length and list of BXCs
      | otherwise = first (bimap (len +) (bxc :)) res

-- | Stores a list of CBOR binary objects + length.
type ListObj bxc = (IntData, [bxc])

-- | Chunks DAG-CBOR Lists into list of encoded binary CBOR objects.
instance (BinaryTranscoder b) => Supplemental Collection (ListObj b) b where
  decodeSup arg =
    case arg of
      CollectionsIndefinite -> Left Indefinite
      CollectionsLength ints ->
        Right $ \bytes -> do
          -- Read the list length data
          Result elemcount start <- extractIntData ints bytes
          ielems <- downcastIntData elemcount
          -- Iterate through CBOR list members for the #elements
          foldObjs start <$> iterateTakeM ielems nextElem (Obj 0 mempty, start)
    where
      -- Extracts the next element from the list with iterate splitAt
      nextElem (Obj total _, bytes) = do
        (Obj len obj, rest) <- nextObj bytes
        return (Obj (len + total) obj, rest)

-- | Stores key-value CBOR binary object pairs + length.
type MapObj bxc = ListObj (bxc, bxc)

-- | Chunks DAG-CBOR Map into list of encoded binary CBOR key-value pairs.
instance (BinaryTranscoder b) => Supplemental Collection (MapObj b) b where
  decodeSup arg =
    case arg of
      CollectionsIndefinite -> Left Indefinite
      CollectionsLength ints ->
        Right $ \bytes -> do
          -- Read the number of pairs for the map data
          Result len start <- extractIntData ints bytes
          ipairs <- downcastIntData len
          -- Iterate through CBOR list members for the #pairs
          foldObjs start <$> iterateTakeM ipairs nextPair (mkIterObj start)
    where
      -- Initializes the iterator for collecting key value pairs
      mkIterObj start = (Obj 0 (mempty, mempty), start)
      -- Extracts the next pair from the map with iterate splitAt
      nextPair (Obj total _, bytes) = do
        (Obj l1 key, onval) <- nextObj bytes
        (Obj l2 val, rest) <- nextObj onval
        return (Obj (total + l1 + l2) (key, val), rest)

-- | Implements reading a Supplemental CID from the matching tag, see:
-- https://github.com/ipld/cid-cbor/
-- https://ipld.io/specs/codecs/dag-cbor/spec/#links
instance (BinaryTranscoder bxc) => Supplemental Tag CID bxc where
  decodeSup Tag =
    Right $ \bytes -> do
      -- Remove the CID tag identifier
      Result _ inner1 <- removeOctet CID.ipldTagCBOR bytes
      -- Decode the arg from the first byte and get the decoder rule
      Result arg inner2 <- extractByte inner1
      -- Get the rule for reading in the CID data bytestring.
      bytesrule <- decodeArg arg :: Okay Collection
      bytesread <- decodeSup bytesrule :: Okay (DecoderMethod bxc bxc)
      -- Remove and verify the multibase 0 prefix and extract the CID result.
      Result cidtop final <- bytesread inner2
      Result _ ciddata <- removeOctet 0 cidtop
      case CID.extractor ciddata of
        Left err -> Left $ CIDError err
        Right (cid, fragmented)
          | BXC.totalOctets fragmented > 0 -> Left Fragmented
          | otherwise -> Right $ Result cid final

-- | Reads simple values. For floating points, it does:
-- 1. Takes n number bytes from the ByteString
-- 2. Validates bytes taken to matches the FloatData type size.
-- 3. Copy those bytes to a corresponding sized Word type
-- 4. Check if they are a valid IPLD Float (Not -∞, ∞, or NaN)
-- 5. Updates the bytes to remove the floating point bytes.
-- Otherwise, it packages the simple value and leaves the bytes unmodified.
instance (BinaryTranscoder bxc) => Supplemental Simple SimpleData bxc where
  decodeSup arg =
    case arg of
      -- Floating point values
      FloatHalf -> Right $ float DM.HalfPrecision 2
      FloatSingle -> Right $ float DM.SinglePrecision 4
      FloatDouble -> Right $ float DM.DoublePrecision 8
      -- Simple supported values
      SimpleNull -> Right $ simple NullData
      SimpleBool bool -> Right $ simple (BoolData bool)
      -- Simple unsupported values
      SimpleUndefined -> Left NotDefined
      StopCollection -> Left Indefinite
    where
      -- Returns the simple value without changing the bytes.
      simple value bytes = Right $ Result value bytes
      -- Copys and constructs a floating point number from n bytes.
      -- TODO: I want to be able to relax the validation with an ifdef
      float mk n bytes = do
        -- Take the first n bytes from the ByteString
        Result taken rest <- extractBytes n bytes
        -- Convert the value from network order to CPU's format.
        value <- copyBEtoCPU taken
        -- Validate its IEEE754 representation. Use `unsafeCoerce` to change
        -- the bit interpretation from Word to Float/Double.
        case (unsafeCoerce value, unsafeCoerce value) of
          (as32, as64)
            -- NOTE: 16-bit floating point numbers aren't part of Haskell.
            | n == 2 -> Left $ NotImplemented "decodeSup: 16-bit float"
            -- Validate 32-bit single floats
            | n == 4 && invalidSingle as32 -> Left InvalidIEEE754
            -- Validate 64-bit double floats
            | n == 8 && invalidDouble as64 -> Left InvalidIEEE754
            -- Success: return the constructed value
            | otherwise -> Right $ Result (FloatData $ mk value) rest
        where
          -- Copys from BE format to CPU format
          -- WARN: only supports little-endian CPUs.
          -- TODO: there should be a way to convert endianness in XCodec
          copyBEtoCPU = copyW8s 0 0 . BXC.unpackOctets . BXC.swapOrder
          -- Bitwise-or each byte from the number into the Word type. Fail when
          -- iteration exceeds bytes or doesn't read the correct #bytes.
          copyW8s !i !acc (!w8 : ws)
            | i < n = copyW8s (i + 1) (acc .<<. 8 .|. fromIntegral w8) ws
            | otherwise = Left UnexpectedStop
          copyW8s !i !acc []
            | i == n = Right acc
            | otherwise = Left UnexpectedStop
          -- True if invalid 32-bit value
          invalidSingle (single :: Float) = isNaN single || isInfinite single
          -- True if invalid 64-bit value
          invalidDouble (double :: Double) = isNaN double || isInfinite double