mfmts-1.1.0.0: cid/MultiFormats/CID/Component/Version.hs
-- | Module : MultiFormats.CID.Component.Version
-- Description : Implements extraction versioning from CID data.
-- Copyright : Zoey McBride (c) 2026
-- License : AGPL-3.0-or-later
-- Maintainer : zoeymcbride@mailbox.org
-- Stability : experimental
module MultiFormats.CID.Component.Version
( Version (CIDv0, CIDv1),
-- * Extractions
extractVersion0,
extractVersion1,
-- * Helper functions
isTextCIDv0,
isStringCIDv0,
-- * Constants
v0EncodingPrefix,
v0EncodingLength,
v0TotalBytes,
v0HashBytes,
v0FirstByte,
v0SecondByte,
v1FirstVarInt,
)
where
import Data.BaseSystem.BinaryTranscoder (BinaryTranscoder)
import Data.BaseSystem.BinaryTranscoder qualified as BXC
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Word (Word8)
import MultiFormats.CID.Errors
import MultiFormats.CID.Extractor (Extraction, Extractor (extractor))
import MultiFormats.VarInt (VarInt)
-- | Implements available CID versions.
data Version = CIDv0 | CIDv1 deriving (Show, Eq, Enum)
-- | Extracts and validates the CID versioning information from bytes.
instance (BinaryTranscoder t) => Extractor Version t where
extractor cidbytes =
case BXC.uncons cidbytes of
Nothing -> Left (InvalidData EmptyCID)
Just (magic, _)
-- Only CIDv0 will start with 0x12, and it must be followed w/ a
-- special byte; if it doesn't have both, the encoding is invalid.
| magic == v0FirstByte -> extractVersion0 cidbytes
-- Without the magic bytes, CIDvX > CIDv0 is done in VarInts, so we get
-- the Version information from the first varint (0x1 for CIDv1).
| otherwise -> extractVersion1 cidbytes
-- | Performs extraction for CIDv1.
extractVersion1 :: (BinaryTranscoder t) => Extraction Version t
extractVersion1 bytes =
extractor bytes >>= \(varint1, rest) ->
if invalidCIDv1 varint1
then Left (InvalidEncoding ExpectedCIDv1)
else Right (CIDv1, rest)
where
invalidCIDv1 v1 = v1 /= v1FirstVarInt
-- | Performs extraction for CIDv0.
extractVersion0 :: (BinaryTranscoder t) => Extraction Version t
extractVersion0 ciddata =
let (lead, rest) = BXC.splitAtByte 2 ciddata
total = BXC.lengthBytes ciddata
in case BXC.unpackBytes lead of
[b1, b2]
| invalidCIDv0 b1 b2 total -> Left (InvalidEncoding MalformedCIDv0)
| otherwise -> Right (CIDv0, rest)
_Not2 -> Left (InvalidEncoding MalformedCIDv0)
where
-- Checks the conditions where a CIDv0 is invalidated.
{-# INLINE invalidCIDv0 #-}
invalidCIDv0 b1 b2 total =
b1 /= v0FirstByte || b2 /= v0SecondByte || total /= v0TotalBytes
-- | This prefixes a CIDv0 instead of a multibase prefix.
v0EncodingPrefix :: String
v0EncodingPrefix = "Qm"
-- | The length of a CIDv0 string, base58 encoded.
v0EncodingLength :: Int
v0EncodingLength = 46
-- | Required length in bytes of all CIDv0 encodings.
v0TotalBytes :: Int
v0TotalBytes = 34
-- | Length of hash digest in CIDv0, ie the length without its versioning
-- information, since those are the only items in CIDv0.
v0HashBytes :: Int
v0HashBytes = v0TotalBytes - 2
-- | Required first byte of all CIDv0 encodings.
v0FirstByte :: Word8
v0FirstByte = 0x12
-- | Required second byte of all CIDv0 encodings.
v0SecondByte :: Word8
v0SecondByte = 0x20
-- | Required first VarInt of all CIDv1 encodings.
v1FirstVarInt :: VarInt
v1FirstVarInt = 1
-- | /O(1)/ Checks if Text qualifes for CIDv0 decoding.
isTextCIDv0 :: Text -> Bool
isTextCIDv0 text =
let start = Text.unpack $ Text.take 2 text
len = Text.length text
in start == v0EncodingPrefix && len == v0EncodingLength
-- | /O(n)/ Checks if a String qualifes for CIDv0 decoding.
isStringCIDv0 :: String -> Bool
isStringCIDv0 str =
let start = take 2 str
len = length str
in start == v0EncodingPrefix && len == v0EncodingLength