packages feed

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

-- | Module      : IPLD.DagCBOR.Decoder.Objects
--   Description : Decodes CBOR objects from binary into the IPLD data model
--   Copyright   : Zoey McBride (c) 2026
--   License     : AGPL-3.0-or-later
--   Maintainer  : zoeymcbride@mailbox.org
--   Stability   : experimental
module IPLD.DagCBOR.Decoder.Objects
  ( -- * Instances of major-type data decoded from DAG-CBOR
    Object (..),
    decodeMajorType,
    decodeSupplemental,
    decodeObject,

    -- * Recursively defines object collections for CBOR
    MapData,
    ListData,
  )
where

import Control.DeepSeq (NFData (..), force)
import Control.Parallel.Strategies (parBuffer, rdeepseq, using)
import Data.Array.IArray qualified as Array
import Data.Bifunctor (Bifunctor (first))
import Data.Text (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 IPLD.DagCBOR.Decoder.Supplemental
import MultiFormats.CID (CID)

-- | Stores the data gotten from each major-type
data Object
  = -- | Instance of a CBOR positive integer.
    ObjectPositive IntData
  | -- | Instance of a CBOR negative integer.
    ObjectNegative IntData
  | -- | Instance of a CBOR bytestring.
    ObjectBytes DM.BytesData
  | -- | Instance of a CBOR UTF-8 string.
    ObjectUTF8 Text
  | -- | Instance of a valid IPLD CBOR list object.
    ObjectList ListData
  | -- | Instance of a valid IPLD CBOR map object.
    ObjectMap MapData
  | -- | Instance of a CID following its CBOR tag.
    ObjectTag CID
  | -- | Instance of simple types (null, bool, float).
    ObjectSimple SimpleData
  deriving (Show, Eq)

-- | Resolves the supplemental data from a major type.
instance (BinaryTranscoder bxc) => Supplemental MajorType Object bxc where
  decodeSup arg =
    case arg of
      TypePositive intrule -> mk ObjectPositive intrule
      TypeNegative intrule -> mk ObjectNegative intrule
      TypeBytes bytesrule -> mk ObjectBytes bytesrule
      TypeUTF8 utf8rule -> mk ObjectUTF8 utf8rule
      TypeList listrule -> mk ObjectList listrule
      TypeMap maprule -> mk ObjectMap maprule
      TypeTag tagrule -> mk ObjectTag tagrule
      TypeSimple simplerule -> mk ObjectSimple simplerule
    where
      -- Creates the DecoderMethod that reads the object using the given rule
      {-# INLINE mk #-}
      mk constructor rule =
        pure $ \bytes -> do
          sup <- decodeSup rule
          Result supdata rest <- sup bytes
          pure $ Result (constructor supdata) rest

-- | Translates the CBOR MajorObject into the IPLD data model.
{-# INLINE objectNode #-}
objectNode :: Object -> DM.Node
objectNode obj =
  -- Strictly evaluate the entire converted object
  force convObject
  where
    -- Points to the conversion of the object into a DM.Node
    {-# NOINLINE convObject #-}
    convObject =
      case obj of
        ObjectSimple NullData -> DM.NullKind
        ObjectSimple (BoolData bool) -> DM.BoolKind bool
        ObjectSimple (FloatData float) -> DM.FloatKind float
        ObjectBytes bytes -> DM.BytesKind bytes
        ObjectUTF8 text -> DM.StringKind text
        ObjectTag cid -> DM.ResourceKind cid
        ObjectList (ListData listdata) -> DM.ListKind listdata
        ObjectMap (MapData mapdata) -> DM.MapKind mapdata
        -- Convert the raw intdata into Haskell Integer (including zero)
        ObjectPositive intdata -> DM.IntKind (fromIntegral intdata)
        -- Adjust negative values for extra digit from not having zero
        ObjectNegative intdata -> DM.IntKind (-(fromIntegral intdata) - 1)

-- | Decodes the CBOR major-type for the first transcoder byte and keeps it in
-- place.
decodeMajorType :: (BinaryTranscoder bxc) => DecoderMethod MajorType bxc
decodeMajorType bytes = do
  Result top _ <- extractByte bytes
  mt <- decodeArg top
  Right $ Result mt bytes

-- | Decodes a major-type's supplemental data from the CBOR object binary and
-- removes it.
decodeSupplemental ::
  (BinaryTranscoder bxc) =>
  MajorType ->
  DecoderMethod Object bxc
decodeSupplemental mt bytes = do
  sup <- decodeSup mt
  sup bytes

-- | Decodes the MajorType and it's Supplemental data from the transcoder and
-- removes both.
decodeObject :: (BinaryTranscoder bxc) => DecoderMethod DM.Node bxc
decodeObject bytes = do
  Result mt _ <- decodeMajorType bytes
  Result _ inner <- extractByte bytes
  first objectNode <$> decodeSupplemental mt inner

-- | Decodes list/map subobjects in parallel using the given decoder function.
decodeSubObjects ::
  (Monoid x, NFData y) =>
  IntData ->
  Int ->
  (x -> Okay y) ->
  [x] ->
  Okay [y]
decodeSubObjects size count decoder bxcs
  -- Run the subdecoders in parallel, and join the objects in sequence on
  -- success, and fully evaluates the subobject w/ deepseq.
  | mtenabled = sequence (mapdecoder `using` parBuffer numsparks rdeepseq)
  -- Otherwise, use single-threading.
  | otherwise =  sequence mapdecoder
  where
    -- Minimum #items and #bytes to qualify for requesting spark threads
    mtenabled = count > 1 && (count > minsparks || size > 4096)
    -- Sets the #sparks, which becomes an enqueued thread to run or become GC'd
    numsparks = max minsparks (min maxsparks count)
    -- Maps the decoder onto the list of binary CBOR objects
    mapdecoder = map decoder bxcs
    -- Maximum #sparks to request from the RTS
    maxsparks = 128
    -- Minimum #sparks to request from the RTS
    minsparks = 16

-- | Provides ListData from DM for DAG-CBOR list object.
newtype ListData = ListData (DM.ListData DM.Node) deriving (Show, Eq)

-- | Implements reading list objects from CBOR.
instance (BinaryTranscoder bxc) => Supplemental Collection ListData bxc where
  decodeSup arg = do
    case arg of
      CollectionsIndefinite -> Left Indefinite
      CollectionsLength ints ->
        Right $ \bytes -> do
          -- Read the list of binary CBOR objects and decode them
          sup <- decodeSup arg :: Okay (DecoderMethod (ListObj bxc) bxc)
          Result (size, chunks) following <- sup bytes
          -- Read the #elements in the list from the top of the bytes
          Result elems _ <- extractIntData ints bytes
          ielems <- downcastIntData elems
          -- Decode the subobjects of the list
          objs <- decodeSubObjects size ielems decodeElem chunks
          let listarray = Array.listArray (0, ielems - 1) objs
          Right $ Result (ListData listarray) following
    where
      -- Decodes single CBOR list element from binary
      {-# NOINLINE decodeElem #-}
      decodeElem bin = do
        Result obj after <- decodeObject bin
        if not (BXC.null after)
          then Left Fragmented
          else return obj

-- | Provides ListData from DM for DAG-CBOR list object.
newtype MapData = MapData (DM.MapData DM.Node DM.Node) deriving (Show, Eq)

-- | Implements reading map objects from CBOR.
instance (BinaryTranscoder bxc) => Supplemental Collection MapData bxc where
  decodeSup arg = do
    case arg of
      CollectionsIndefinite -> Left Indefinite
      CollectionsLength ints ->
        Right $ \bytes -> do
          -- Read the map of binary CBOR objects pairs and decode them
          sup <- decodeSup arg :: Okay (DecoderMethod (MapObj bxc) bxc)
          Result (size, chunks) following <- sup bytes
          -- Read the #elements in the list from the top of the bytes
          Result pairs _ <- extractIntData ints bytes
          ipairs <- downcastIntData pairs
          -- Decode the subobjects of the map
          objs <- decodeSubObjects size ipairs decodePair chunks
          let maparray = Array.listArray (0, ipairs - 1) objs
          Right $ Result (MapData maparray) following
    where
      -- Decodes single CBOR map pair from binary tuple
      {-# NOINLINE decodePair #-}
      decodePair (bin1, bin2) = do
        -- Read the key and value binary objects
        Result key afterkey <- decodeObject bin1
        Result val afterval <- decodeObject bin2
        -- Verify parsing the map succeeded
        case (key, val) of
          ypair
            | not (BXC.null afterkey && BXC.null afterval) -> Left Fragmented
            | not (isKeyValidDAGCBOR key) -> Left NotStringKey
            | otherwise -> Right ypair
      -- Gives true if the key object for the map is a string type
      {-# INLINE isKeyValidDAGCBOR #-}
      isKeyValidDAGCBOR (DM.StringKind _) = True
      isKeyValidDAGCBOR _OtherKinds = False