packages feed

mfmts-1.0.0.0: varint/MultiFormats/VarInt.hs

-- | Module      : MultiFormats.VarInt
--   Description : Implements multiformat's varints
--   Copyright   : Zoey McBride (c) 2026
--   License     : AGPL-3.0-or-later
--   Maintainer  : zoeymcbride@mailbox.org
--   Stability   : experimental
--
-- Implements serialization and extraction for multiformat's unsigned binary
-- varint spec. See: https://github.com/multiformats/unsigned-varint
module MultiFormats.VarInt
  ( VarInt,
    extractVarInt,
    serializeVarInt,
    maxVarInt,
  )
where

import Data.Bits ((.&.), (.<<.), (.>>.), (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as Bytes
import Data.Word (Word64, Word8)
import MultiFormats.VarInt.Errors (Error (..), Okay)

-- | VarInt spec uses unsigned 64 bit integers.
type VarInt = Word64

-- | Byte with MSB=1.
msb1 :: Word8
msb1 = 1 .<<. 7

-- | Byte with LSB=1.
lsb1 :: Word8
lsb1 = 1

-- | The value w/ MSB=1 and LSB=1.
msb1lsb1 :: Word8
msb1lsb1 = msb1 .|. lsb1

-- | The value that extracts all bits but the MSB.
masklower :: Word8
masklower = msb1 - 1

-- | The maximum value the spec allows (2^63-1).
maxVarInt :: VarInt
maxVarInt = (1 .<<. 63) - 1

-- | The maximum # of bytes a VarInt can occupy when encoded.
bytesCapacity :: Int
bytesCapacity = 9

-- | Splits ByteString into it's first occurring VarInt and the remaining
-- ByteString after.
spanVarInt :: ByteString -> Maybe (ByteString, ByteString)
spanVarInt bytes =
  -- Collect the bytes with MSB=1.
  case Bytes.span isMSB1 bytes of
    -- The varint is the leading bytes with the MSB=1 and a final terminating
    -- byte with MSB=0.
    (msb1s, msb0s)
      -- Verify if a final byte exists.
      | Bytes.null msb0s -> Nothing
      -- The loop invariant ensures msb + 1 has MSB=0. Add the last byte with
      -- splitAt since it is (a lot) more efficent than append.
      | otherwise -> Just (Bytes.splitAt (Bytes.length msb1s + 1) bytes)
  where
    isMSB1 byte = (byte .&. msb1) /= 0

-- | Concats the lower 7 bits from bytes into single int value.
buildVarInt :: ByteString -> VarInt
buildVarInt = Bytes.foldr (\byte int -> advance int .|. lower byte) 0
  where
    advance int = int .<<. 7
    lower byte = fromIntegral $ byte .&. masklower

-- | Checks edge case where input is invalid from not being a minimal encoding
-- of varints, (ie, we can fit 2 bytes of 7 bits into 1 byte of 7 bits). The
-- only case of this is when a final 0x8100 should be a 0x79... I think!
nonMinimal :: ByteString -> Bool
nonMinimal bytes = Bytes.unpack (Bytes.takeEnd 2 bytes) == [msb1lsb1, 0]

-- | Checks if a VarInt encoded ByteString overflows a Word64.
aboveCapacity :: ByteString -> Bool
aboveCapacity varbytes =
  let wholelen = Bytes.length varbytes
   in not (1 <= wholelen && wholelen <= bytesCapacity)

-- | Extracts the first minimally encoded varint from a ByteString.
extractVarInt :: ByteString -> Okay (VarInt, ByteString)
extractVarInt bytes
  | Bytes.null bytes = Left EmptyBytes
  | otherwise =
      case spanVarInt bytes of
        Nothing -> Left MissingTermination
        Just (varbytes, rest)
          | aboveCapacity varbytes -> Left BytesOverflow
          | nonMinimal varbytes -> Left NotMinimal
          | otherwise -> Right (buildVarInt varbytes, rest)

-- | Puts a parsed VarInt (Word64, without MSB indicators) into a ByteString
-- with MSB indicators, and a max size of 9 bytes.
serializeVarInt :: VarInt -> Okay ByteString
serializeVarInt varint
  | varint > maxVarInt = Left AboveMaxValue
  | otherwise =
      Right
        . zeroReplaceNull
        . Bytes.unfoldr setMSBs
        . Bytes.dropWhileEnd (== 0)
        $ Bytes.pack [varintGroup i varint | i <- [1 .. bytesCapacity]]
  where
    -- Extracts Nth group of 7 bits from VarInt.
    varintGroup n var = fromIntegral (var .>>. (7 * (n - 1))) .&. masklower
    -- Sets MSB=1 for leading bytes in unfoldr.
    setMSBs bytes =
      Bytes.uncons bytes >>= \(byte, rest) ->
        if Bytes.null rest
          then Just (byte, mempty)
          else Just (byte .|. msb1, rest)
    -- Replaces a null ByteString with a single zero.
    zeroReplaceNull bytes =
      if Bytes.null bytes
        then Bytes.singleton 0
        else bytes