mfmts-1.2.0.0: cid/MultiFormats/CID.hs
-- | Module : MultiFormats.CID
-- Description : Implements multiformats CID
-- Copyright : Zoey McBride (c) 2026
-- License : AGPL-3.0-or-later
-- Maintainer : zoeymcbride@mailbox.org
-- Stability : experimental
--
-- This module provides a way for extracting, parsing and serializing CID data.
-- See: https://github.com/multiformats/cid
module MultiFormats.CID
( CID (CID),
cidVersion,
cidDataCodec,
cidHashCodec,
cidHashBuilder,
cidHashSize,
cidSizeOf,
)
where
import Control.DeepSeq (NFData (..), force, ($!!))
import Data.BaseSystems.Lazy (base16lower, encoder)
import Data.Bifunctor (first)
import Data.Bits ((.>>.))
import Data.ByteString.Builder (Builder)
import Data.ByteString.Builder qualified as Builder
import Data.ByteString.Short (ShortByteString)
import Data.List (intercalate)
import Data.Maybe (fromJust)
import Data.Text qualified as Text
import Data.XCodec.BinaryTranscoder (BinaryTranscoder)
import Data.XCodec.BinaryTranscoder qualified as BXC
import MultiFormats.CID.Component.Codec qualified as CID
import MultiFormats.CID.Component.Decoder qualified as CID
import MultiFormats.CID.Component.Hash qualified as CID
import MultiFormats.CID.Component.Version (Version (CIDv0, CIDv1))
import MultiFormats.CID.Component.Version qualified as CID
import MultiFormats.CID.Constants qualified as CID
import MultiFormats.CID.Errors
import MultiFormats.CID.Extractor
import MultiFormats.CID.Parser
import MultiFormats.CID.Serializer
import MultiFormats.MultiCodec (MultiCodec (..), resolveMultiCodec)
import MultiFormats.MultiHash (MultiHash (..), resolveMultiHash)
import MultiFormats.VarInt (VarInt)
-- | Defines the data-type for CID data.
data CID = CID
{ -- | Specifies the version (CIDv0 or CIDv1) of this CID.
cidVersion :: CID.Version,
-- | Describes the content type of the hashed data.
cidDataCodec :: MultiCodec,
-- | Contains the hash function used.
cidHashCodec :: MultiHash,
-- | Stores the # of bytes used to store the HashDigest
cidHashSize :: VarInt,
-- | Stores the hash function's output.
cidHashBuilder :: Builder
}
-- | Returns the size of a CID in bytes.
{-# INLINE cidSizeOf #-}
cidSizeOf :: CID -> VarInt
cidSizeOf (CID CIDv0 _ _ _ _) = fromIntegral CID.v0TotalBytes
cidSizeOf (CID CIDv1 datacodec hashcodec hashsize _) =
let datavar = multiCodecValue datacodec
hashvar = multiCodecValue $ multiHashCodec hashcodec
in hashsize
+ varSizeOf 0 CID.v1FirstVarInt
+ varSizeOf 0 datavar
+ varSizeOf 0 hashsize
+ varSizeOf 0 hashvar
where
-- Gets the #bytes for a VarInt, including the extra byte for 8-byte values.
varSizeOf !total !var
-- Shift the varint by 1 varint group (7 bits) until it reaches zero
| var /= 0 = varSizeOf (total + 1) (var .>>. 7)
-- Adjust the total for when the byte value is zero.
| otherwise = max 1 total
-- | Checks equality of the CID values, version, and hash digest.
instance Eq CID where
(CID v1 c1 h1 s1 d1) == (CID v2 c2 h2 s2 d2) =
-- Check that none of the comparable values mismatch
not (v1 /= v2 || c1 /= c2 || h1 /= h2 || s1 /= s2)
-- And then check equality on the executed builder
&& Builder.toLazyByteString d1 == Builder.toLazyByteString d2
-- | Implements Show for CID.
instance Show CID where
show (CID version datacodec hashcodec hashsize hashbuilder) =
let hashdigest = Builder.toLazyByteString hashbuilder
hashstring = "0x" ++ Text.unpack (encoder showbase hashdigest)
in show version
++ "("
++ intercalate
", "
[ "DataCodec=" ++ show datacodec,
"HashCodec=" ++ show hashcodec,
"HashDigest[" ++ show hashsize ++ "]=" ++ hashstring
]
++ ")"
where
-- Sets the base implementation to show the hash digest with.
showbase = base16lower
-- | Evaluates CID struct into normal form without executing the hash builder.
instance NFData CID where
rnf (CID version datacodec hashcodec hashsize _) =
rnf version `seq` rnf datacodec `seq` rnf hashcodec `seq` rnf hashsize
-- | Parses a CIDv1 from a MultiBase encoded string OR a CIDv0 from a base16lower
-- encoded string. Fails when there is extra bytes past the CID.
instance Parser CID where
parser cidstr = do
-- Decode to bytes based on the String's encoding. Use SBS as BXC because
-- the bytestring is discarded after this function.
CID.Decoder cidbytes <- parser cidstr :: Okay (CID.Decoder ShortByteString)
-- Extracts the CID's data from bytes.
(cid, trailing) <- extractor cidbytes
-- Fail if there is any leftovers. Otherwise fully evaluate the CID.
if BXC.totalOctets trailing > 0
then Left (InvalidData TrailingBytes)
else Right $!! cid
-- | Extracts a CID from a ByteString. Allows trailing bytes.
instance (BinaryTranscoder t) => Extractor CID t where
extractor bytes = do
-- First, extract the version from bytes
(version, rest) <- extractor bytes
-- Apply the constructor if the CID was successfully extracted from bytes,
-- and then strictly evaluate the CID into memory.
first force <$> case version of
CIDv0 -> extractCIDv0 rest
CIDv1 -> extractCIDv1 rest
-- | Applies the extraction after version# for CIDv0 was extracted.
extractCIDv0 :: (BinaryTranscoder t) => Extraction CID t
extractCIDv0 ondata
-- Fail if there isn't enough bytes
| totalbytes /= CID.v0HashBytes = Left (InvalidHash IncorrectLength)
-- Create the CIDv0 from data
| otherwise = Right (mkCIDv0 ciddata, rest)
where
totalbytes = BXC.totalOctets ondata
(ciddata, rest) = BXC.splitOffset CID.v0HashBytes ondata
-- | Applies the extraction after version# for CIDv1 was extracted.
extractCIDv1 :: (BinaryTranscoder t) => Extraction CID t
extractCIDv1 oncodec = do
-- Extract the 1st MultiCodec from the bytes.
(CID.Codec multicodec, onhash) <- extractor oncodec
-- Extract the 2nd MultiCodec and get CIDHash
(CID.Hash multihash hashsize hashdigest, stop) <- extractor onhash
-- Get the length of the hash data after the hash codec
-- TODO: verifiy hash length against crypton library
return (CID CIDv1 multicodec multihash hashsize hashdigest, stop)
-- | Serializes a CID from representation.
instance Serializer CID where
serializer ciddata =
case cidVersion ciddata of
CIDv0 -> serializeCIDv0 ciddata
CIDv1 -> serializeCIDv1 ciddata
-- | Serializes CIDv0 by adding the Codec Byte to its digest
serializeCIDv0 :: Serialization CID
serializeCIDv0 (CID CIDv1 _ _ _ _) = error "serializeCIDv0: input has CIDv1!"
serializeCIDv0 (CID CIDv0 _ _ _ cidhash) =
-- Concat the Builders for the bytes data into a single Builder
let cid1st = Builder.word8 CID.v0FirstByte
cid2nd = Builder.word8 CID.v0SecondByte
in return $ mconcat [cid1st, cid2nd, cidhash]
-- | Serializes CIDv1 into VarInts of its components.
serializeCIDv1 :: Serialization CID
serializeCIDv1 (CID CIDv0 _ _ _ _) = error "serializeCIDv1: input has CIDv0!"
serializeCIDv1 (CID CIDv1 datacodec hashcodec hashlength hashbuilder) = do
-- Get the Builder from each serializer sequenced from Just values
builders <-
sequence
[ serializer CID.v1FirstVarInt,
serializer datacodec,
serializer hashcodec,
serializer hashlength,
pure hashbuilder
]
-- Concat the list into one large Builder
return $ mconcat builders
-- | Provides the contructor for CIDv0s, see:
-- https://github.com/multiformats/cid?tab=readme-ov-file#cidv0
mkCIDv0 :: (BinaryTranscoder t) => t -> CID
mkCIDv0 digest =
CID
{ cidVersion = CIDv0,
cidDataCodec = codec "dag-pb",
cidHashCodec = hash "sha2-256",
cidHashSize = varlen digest,
cidHashBuilder = BXC.unpackSerial digest
}
where
codec str = fromJust $!! resolveMultiCodec str
hash str = fromJust $!! resolveMultiHash str
varlen bxc = fromIntegral $! BXC.totalOctets bxc