mfmts-1.0.0.0: cid/MultiFormats/CID/Internal/CIDVersion.hs
-- | Module : MultiFormats.CID.Internal.CIDVersion
-- 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.Internal.CIDVersion
( VersionEnum (CIDv0, CIDv1),
CIDVersion (CIDVersion),
-- * Extractions
extractVersion0,
extractVersion1,
-- * Helper functions
isCIDv0Str,
-- * Constants
cidv0Bytes,
cidv0HashLength,
cidv0Byte1st,
cidv1VarInt1st,
)
where
import Data.Bifunctor (Bifunctor (first))
import Data.ByteString qualified as Bytes
import Data.Word (Word8)
import MultiFormats.CID.Errors
import MultiFormats.CID.Extractor (Extraction, Extractor (extractor))
import MultiFormats.VarInt (VarInt)
-- | Implements available CID versions.
data VersionEnum = CIDv0 | CIDv1 deriving (Show, Eq, Enum)
-- | Wraps Version type to implement Extractor.
newtype CIDVersion = CIDVersion VersionEnum deriving (Eq)
-- | Extracts and validates the CID versioning information from bytes.
instance Extractor CIDVersion where
extractor cidbytes =
case Bytes.uncons cidbytes of
Nothing -> Left (InvalidData EmptyCID)
-- 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.
Just (0x12, _) -> first CIDVersion <$> 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).
Just _VarInt -> first CIDVersion <$> extractVersion1 cidbytes
-- | Performs extraction for CIDv1.
extractVersion1 :: Extraction VersionEnum
extractVersion1 bytes =
extractor bytes >>= \(varint1, rest) ->
if invalidCIDv1 varint1
then Left (InvalidEncoding ExpectedCIDv1)
else Right (CIDv1, rest)
where
invalidCIDv1 v1 = v1 /= cidv1VarInt1st
-- | Performs extraction for CIDv0.
extractVersion0 :: Extraction VersionEnum
extractVersion0 bytes =
let (lead, rest) = Bytes.splitAt 2 bytes
in case Bytes.unpack lead of
[b1, b2]
| invalidCIDv0 b1 b2 -> Left (InvalidEncoding MalformedCIDv0)
| otherwise -> Right (CIDv0, rest)
_Not2 -> Left (InvalidEncoding MalformedCIDv0)
where
invalidCIDv0 b1 b2 =
b1 /= cidv0Byte1st
|| b2 /= cidv0Byte2nd
|| Bytes.length bytes /= cidv0Bytes
-- | This prefixes a CIDv0 instead of a multibase prefix.
cidv0StringPrefix :: String
cidv0StringPrefix = "Qm"
-- | The length of a CIDv0 string, base58 encoded.
cidv0StringLength :: Int
cidv0StringLength = 46
-- | Required length in bytes of all CIDv0 encodings.
cidv0Bytes :: Int
cidv0Bytes = 34
-- | Length of hash digest in CIDv0, ie the length without its versioning
-- information, since those are the only items in CIDv0.
cidv0HashLength :: Int
cidv0HashLength = cidv0Bytes - 2
-- | Required first byte of all CIDv0 encodings.
cidv0Byte1st :: Word8
cidv0Byte1st = 0x12
-- | Required second byte of all CIDv0 encodings.
cidv0Byte2nd :: Word8
cidv0Byte2nd = 0x20
-- | Required first VarInt of all CIDv1 encodings.
cidv1VarInt1st :: VarInt
cidv1VarInt1st = 1
-- | Checks if a string meets CIDv0 criteria.
isCIDv0Str :: String -> Bool
isCIDv0Str str =
take 2 str == cidv0StringPrefix && length str == cidv0StringLength