packages feed

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