mfmts-1.2.0.0: multihash/MultiFormats/MultiHash.hs
-- | Module : MultiFormats.MultiHash
-- Description : Associates multicodec values with their hash functions.
-- Copyright : Zoey McBride (c) 2026
-- License : AGPL-3.0-or-later
-- Maintainer : zoeymcbride@mailbox.org
-- Stability : experimental
--
-- Provides a lookup table to verify multicodec values as multihashes and a way
-- to resolve hash functions from multicodecs or their multicodec name. See:
-- https://github.com/multiformats/multihash
module MultiFormats.MultiHash
( MultiHash (MultiHash, multiHashCodec, multiHashFunction),
resolveMultiHash,
resolveHashCodec,
)
where
import Control.DeepSeq (NFData (..))
import Control.Monad (join, (<$!>))
import Crypto.Hash (HashAlgorithm)
import Crypto.Hash qualified as Hash
import Crypto.Hash.Algorithms qualified as Algo
import Data.BaseSystems (base16lower, encoder)
import Data.ByteArray qualified as Memory
import Data.ByteString.Builder (Builder)
import Data.ByteString.Builder qualified as Builder
import Data.ByteString.Internal (ByteString (BS))
import Data.ByteString.Lazy (LazyByteString)
import Data.ByteString.Lazy qualified as LBS
import Data.Function (on)
import Data.Map (Map)
import Data.Map qualified as LazyMap
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text qualified as Text
import Data.XCodec.BinaryTranscoder (packValueBE)
import Foreign.ForeignPtr (newForeignPtr)
import Foreign.Marshal.Alloc (finalizerFree)
import Foreign.Marshal.Array (copyArray, mallocArray)
import GHC.IO (unsafePerformIO)
import MultiFormats.MultiCodec (MultiCodec (..))
import MultiFormats.MultiCodec qualified as MC
-- | Alias for the type of function signture we want from a hash function.
type HashFunction = Builder -> LazyByteString
-- | Forms the MultiHash implementaton.
data MultiHash = MultiHash
{ -- | Stores the MultiCodec associated with multiHashCodec. Used for Eq, Ord.
multiHashCodec :: MultiCodec,
-- | Lazy loads the hash function when called.
multiHashFunction :: ~HashFunction
}
-- | Evaluates MultiHash into strict normal form.
instance NFData MultiHash where
rnf (MultiHash codec hashfunc) = rnf codec `seq` rnf hashfunc
instance Show MultiHash where
show (MultiHash codec@(MultiCodec value) _) =
let name = fromMaybe "<invalid codec>" $ MC.resolveCodecName codec
hex = "0x" ++ Text.unpack (encoder base16lower $ packValueBE value)
in "MultiHash(" ++ name ++ "=" ++ hex ++ ")"
instance Eq MultiHash where
(==) = (==) `on` multiHashCodec
instance Ord MultiHash where
compare = compare `on` multiHashCodec
-- | Resolves a MultiHash from a MultiCodec.
resolveHashCodec :: MultiCodec -> Maybe MultiHash
resolveHashCodec codec =
-- Strictly contructs the MultiHash with the codec and the hash lookup
(MultiHash $! codec) <$!> join (LazyMap.lookup codec hashFunctionTable)
-- | Resolves a MultiHash from MultiCodec name.
resolveMultiHash :: String -> Maybe MultiHash
resolveMultiHash name = resolveHashCodec =<< MC.resolveMultiCodec name
-- | Runs the hash function and copies the result into a new buffer.
hashBytesCopy :: (HashAlgorithm a) => a -> HashFunction
hashBytesCopy algo builder =
-- This was done as a pure function with pack/unpack, but we're going to use
-- IO to improve the performance of the copy, which needs to happen to
-- instantiate a new bytestring type from Crypto.Hash function results.
unsafePerformIO $!
-- In order to work with the data produced by the hash via Memory, we have
-- pass an IO transformation on the pointer to the data. The data at the
-- pointer gets GC'd after this, so we can't copy by referenece and have to
-- malloc to retain the data (and not double free and crash) unfortunately.
Memory.withByteArray digest $ \srcptr -> do
-- Copy the array into newly allocated space
destptr <- mallocArray len
copyArray destptr srcptr len
-- Associate the finalizer with free for the pointer to be used in the GC
hashptr <- newForeignPtr finalizerFree destptr
-- Construct the bytestring from the pointer, then O(1) convert to lazy
return $ LBS.fromStrict (BS hashptr len)
where
-- Loads the entire input into memory. hashWith only accepts instances of
-- ByteArrayAccess, but LazyByteString doesn't implement this.
access = LBS.toStrict (Builder.toLazyByteString builder)
-- Runs the hash algorithm on the accessed data.
digest = Hash.hashWith algo access
-- Gets the length (in bytes) of the pointed to memory.
len = Memory.length digest
-- | Resolves all the String names from MultiCodec table and assigns an
-- implementation of the hash function if it exists
hashFunctionTable :: Map MultiCodec (Maybe HashFunction)
hashFunctionTable =
-- We want a lazy map so we can skip evaluting hash functions that never get
-- looked up, but we have to evaluate all the hash codecs that exist.
LazyMap.fromList . mapMaybe pairEntries $
[ ("identity", Just Builder.toLazyByteString),
("sha1", hashSingle Algo.SHA1),
("sha2-256", hashSingle Algo.SHA256),
("sha2-512", hashSingle Algo.SHA512),
("sha3-256", hashSingle Algo.SHA3_256),
("sha3-512", hashSingle Algo.SHA3_512),
("sha3-224", hashSingle Algo.SHA3_224),
("sha3-384", hashSingle Algo.SHA3_384),
("sha3-512", hashSingle Algo.SHA3_512),
("keccak-224", hashSingle Algo.Keccak_224),
("keccak-256", hashSingle Algo.Keccak_256),
("keccak-384", hashSingle Algo.Keccak_384),
("keccak-512", hashSingle Algo.Keccak_512),
("md5", hashSingle Algo.MD5),
("dbl-sha2-256", hashDouble Algo.SHA256),
-- These have HashAlgorithms in Algo, but idk how to use them
("blake3", Nothing),
("shake-128", Nothing),
("shake-256", Nothing)
]
where
-- Applies the hash algorithm once
hashSingle algo = Just $ hashBytesCopy algo
-- Applies the hash algorithm twice
hashDouble algo = do
hash <- hashSingle algo
return $ hash . Builder.lazyByteString . hash
-- Returns a map entry if the multicodec exists
pairEntries (name, hfunc) =
case MC.resolveMultiCodec name of
Just multicodec -> Just (multicodec, hfunc)
Nothing -> Nothing