packages feed

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