use crate::Error;
use core::fmt;
use core::hash::Hash;
use digest::{Digest, ExtendableOutput, Update};
use multi_base::Base;
use multi_codec::Codec;
use multi_trait::{EncodeInto, Null, TryDecodeFrom};
use multi_util::{BaseEncoded, CodecInfo, DetectedEncoder, EncodingInfo, Varbytes};
use subtle::ConstantTimeEq;
use typenum::consts::{U28, U32, U48, U64};
pub const MAX_HASH_LENGTH: usize = 16 * 1024 * 1024;
pub const HASH_CODECS: [Codec; 25] = [
Codec::Blake2B224,
Codec::Blake2B256,
Codec::Blake2B384,
Codec::Blake2B512,
Codec::Blake2S224,
Codec::Blake2S256,
Codec::Blake3,
Codec::Md5,
Codec::Ripemd128,
Codec::Ripemd160,
Codec::Ripemd256,
Codec::Ripemd320,
Codec::Sha1,
Codec::Sha2224,
Codec::Sha2256,
Codec::Sha2384,
Codec::Sha2512,
Codec::Sha2512224,
Codec::Sha2512256,
Codec::Sha3224,
Codec::Sha3256,
Codec::Sha3384,
Codec::Sha3512,
Codec::Shake128,
Codec::Shake256,
];
pub const SAFE_HASH_CODECS: [Codec; 10] = [
Codec::Blake2B256,
Codec::Blake2B384,
Codec::Blake2B512,
Codec::Blake2S256,
Codec::Blake3,
Codec::Sha3256,
Codec::Sha3384,
Codec::Sha3512,
Codec::Shake128,
Codec::Shake256,
];
#[cfg(feature = "fips")]
pub const FIPS_CODECS: [Codec; 13] = [
Codec::Sha1,
Codec::Sha2224,
Codec::Sha2256,
Codec::Sha2384,
Codec::Sha2512,
Codec::Sha2512224,
Codec::Sha2512256,
Codec::Sha3224,
Codec::Sha3256,
Codec::Sha3384,
Codec::Sha3512,
Codec::Shake128,
Codec::Shake256,
];
#[cfg(feature = "fips")]
pub const SAFE_FIPS_CODECS: [Codec; 8] = [
Codec::Sha2256,
Codec::Sha2384,
Codec::Sha2512,
Codec::Sha3256,
Codec::Sha3384,
Codec::Sha3512,
Codec::Shake128,
Codec::Shake256,
];
pub const SIGIL: Codec = Codec::Multihash;
pub type EncodedMultihash = BaseEncoded<Multihash, DetectedEncoder>;
#[derive(Clone, Copy)]
enum OutputPolicy {
Fixed(usize),
Xof,
}
const OUTPUT_POLICIES: [OutputPolicy; 25] = [
OutputPolicy::Fixed(28), OutputPolicy::Fixed(32), OutputPolicy::Fixed(48), OutputPolicy::Fixed(64), OutputPolicy::Fixed(28), OutputPolicy::Fixed(32), OutputPolicy::Xof, OutputPolicy::Fixed(16), OutputPolicy::Fixed(16), OutputPolicy::Fixed(20), OutputPolicy::Fixed(32), OutputPolicy::Fixed(40), OutputPolicy::Fixed(20), OutputPolicy::Fixed(28), OutputPolicy::Fixed(32), OutputPolicy::Fixed(48), OutputPolicy::Fixed(64), OutputPolicy::Fixed(28), OutputPolicy::Fixed(32), OutputPolicy::Fixed(28), OutputPolicy::Fixed(32), OutputPolicy::Fixed(48), OutputPolicy::Fixed(64), OutputPolicy::Xof, OutputPolicy::Xof, ];
const _: () = assert!(OUTPUT_POLICIES.len() == HASH_CODECS.len());
fn output_policy(codec: Codec) -> Option<OutputPolicy> {
let index = HASH_CODECS
.iter()
.position(|&candidate| candidate == codec)?;
OUTPUT_POLICIES.get(index).copied()
}
fn validate_digest_length(codec: Codec, hash: &[u8]) -> Result<(), Error> {
match output_policy(codec) {
Some(OutputPolicy::Fixed(expected)) => {
let actual = hash.len();
if actual == expected {
Ok(())
} else {
Err(Error::invalid_digest_length(codec, expected, actual))
}
}
Some(OutputPolicy::Xof) => {
let actual = hash.len();
if actual == 0 || actual > MAX_HASH_LENGTH {
Err(Error::output_len_invalid(codec, actual, MAX_HASH_LENGTH))
} else {
Ok(())
}
}
None => Ok(()),
}
}
const fn validate_output_len(codec: Codec, output_len: Option<usize>) -> Result<usize, Error> {
match output_len {
None => Err(Error::output_len_required(codec)),
Some(len) if len == 0 || len > MAX_HASH_LENGTH => {
Err(Error::output_len_invalid(codec, len, MAX_HASH_LENGTH))
}
Some(len) => Ok(len),
}
}
#[derive(Clone)]
enum Hasher {
Blake2B224(blake2::Blake2b<U28>),
Blake2B256(blake2::Blake2b<U32>),
Blake2B384(blake2::Blake2b<U48>),
Blake2B512(blake2::Blake2b<U64>),
Blake2S224(blake2::Blake2s<U28>),
Blake2S256(blake2::Blake2s<U32>),
Blake3(Box<blake3::Hasher>),
Md5(md5::Md5),
Ripemd128(ripemd::Ripemd128),
Ripemd160(ripemd::Ripemd160),
Ripemd256(ripemd::Ripemd256),
Ripemd320(ripemd::Ripemd320),
Sha1(sha1::Sha1),
Sha2224(sha2::Sha224),
Sha2256(sha2::Sha256),
Sha2384(sha2::Sha384),
Sha2512(sha2::Sha512),
Sha2512224(sha2::Sha512_224),
Sha2512256(sha2::Sha512_256),
Sha3224(sha3::Sha3_224),
Sha3256(sha3::Sha3_256),
Sha3384(sha3::Sha3_384),
Sha3512(sha3::Sha3_512),
Shake128(shake::Shake128),
Shake256(shake::Shake256),
}
impl Hasher {
fn new(codec: Codec) -> Option<Self> {
Some(match codec {
Codec::Blake2B224 => Self::Blake2B224(blake2::Blake2b::<U28>::new()),
Codec::Blake2B256 => Self::Blake2B256(blake2::Blake2b::<U32>::new()),
Codec::Blake2B384 => Self::Blake2B384(blake2::Blake2b::<U48>::new()),
Codec::Blake2B512 => Self::Blake2B512(blake2::Blake2b::<U64>::new()),
Codec::Blake2S224 => Self::Blake2S224(blake2::Blake2s::<U28>::new()),
Codec::Blake2S256 => Self::Blake2S256(blake2::Blake2s::<U32>::new()),
Codec::Blake3 => Self::Blake3(Box::new(blake3::Hasher::new())),
Codec::Md5 => Self::Md5(md5::Md5::new()),
Codec::Ripemd128 => Self::Ripemd128(ripemd::Ripemd128::new()),
Codec::Ripemd160 => Self::Ripemd160(ripemd::Ripemd160::new()),
Codec::Ripemd256 => Self::Ripemd256(ripemd::Ripemd256::new()),
Codec::Ripemd320 => Self::Ripemd320(ripemd::Ripemd320::new()),
Codec::Sha1 => Self::Sha1(sha1::Sha1::new()),
Codec::Sha2224 => Self::Sha2224(sha2::Sha224::new()),
Codec::Sha2256 => Self::Sha2256(sha2::Sha256::new()),
Codec::Sha2384 => Self::Sha2384(sha2::Sha384::new()),
Codec::Sha2512 => Self::Sha2512(sha2::Sha512::new()),
Codec::Sha2512224 => Self::Sha2512224(sha2::Sha512_224::new()),
Codec::Sha2512256 => Self::Sha2512256(sha2::Sha512_256::new()),
Codec::Sha3224 => Self::Sha3224(sha3::Sha3_224::new()),
Codec::Sha3256 => Self::Sha3256(sha3::Sha3_256::new()),
Codec::Sha3384 => Self::Sha3384(sha3::Sha3_384::new()),
Codec::Sha3512 => Self::Sha3512(sha3::Sha3_512::new()),
Codec::Shake128 => Self::Shake128(shake::Shake128::default()),
Codec::Shake256 => Self::Shake256(shake::Shake256::default()),
_ => return None,
})
}
fn update(&mut self, data: &[u8]) {
match self {
Self::Blake2B224(h) => Digest::update(h, data),
Self::Blake2B256(h) => Digest::update(h, data),
Self::Blake2B384(h) => Digest::update(h, data),
Self::Blake2B512(h) => Digest::update(h, data),
Self::Blake2S224(h) => Digest::update(h, data),
Self::Blake2S256(h) => Digest::update(h, data),
Self::Blake3(h) => {
blake3::Hasher::update(h, data);
}
Self::Md5(h) => Digest::update(h, data),
Self::Ripemd128(h) => Digest::update(h, data),
Self::Ripemd160(h) => Digest::update(h, data),
Self::Ripemd256(h) => Digest::update(h, data),
Self::Ripemd320(h) => Digest::update(h, data),
Self::Sha1(h) => Digest::update(h, data),
Self::Sha2224(h) => Digest::update(h, data),
Self::Sha2256(h) => Digest::update(h, data),
Self::Sha2384(h) => Digest::update(h, data),
Self::Sha2512(h) => Digest::update(h, data),
Self::Sha2512224(h) => Digest::update(h, data),
Self::Sha2512256(h) => Digest::update(h, data),
Self::Sha3224(h) => Digest::update(h, data),
Self::Sha3256(h) => Digest::update(h, data),
Self::Sha3384(h) => Digest::update(h, data),
Self::Sha3512(h) => Digest::update(h, data),
Self::Shake128(h) => Update::update(h, data),
Self::Shake256(h) => Update::update(h, data),
}
}
fn finalize(self, codec: Codec, output_len: Option<usize>) -> Result<Vec<u8>, Error> {
match self {
Self::Blake2B224(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake2B256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake2B384(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake2B512(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake2S224(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake2S256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Blake3(h) => {
let out_len = validate_output_len(codec, output_len)?;
let mut out = vec![0u8; out_len];
blake3::Hasher::finalize_xof(&h).fill(&mut out);
Ok(out)
}
Self::Md5(h) => Ok(Digest::finalize(h).to_vec()),
Self::Ripemd128(h) => Ok(Digest::finalize(h).to_vec()),
Self::Ripemd160(h) => Ok(Digest::finalize(h).to_vec()),
Self::Ripemd256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Ripemd320(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha1(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2224(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2384(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2512(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2512224(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha2512256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha3224(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha3256(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha3384(h) => Ok(Digest::finalize(h).to_vec()),
Self::Sha3512(h) => Ok(Digest::finalize(h).to_vec()),
Self::Shake128(h) => {
let out_len = validate_output_len(codec, output_len)?;
Ok(xof_finalize(h, out_len))
}
Self::Shake256(h) => {
let out_len = validate_output_len(codec, output_len)?;
Ok(xof_finalize(h, out_len))
}
}
}
}
fn xof_finalize<T: ExtendableOutput>(hasher: T, out_len: usize) -> Vec<u8> {
let mut out = vec![0u8; out_len];
ExtendableOutput::finalize_xof_into(hasher, &mut out);
out
}
impl fmt::Debug for Hasher {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
Self::Blake2B224(_) => "Blake2B224(..)",
Self::Blake2B256(_) => "Blake2B256(..)",
Self::Blake2B384(_) => "Blake2B384(..)",
Self::Blake2B512(_) => "Blake2B512(..)",
Self::Blake2S224(_) => "Blake2S224(..)",
Self::Blake2S256(_) => "Blake2S256(..)",
Self::Blake3(_) => "Blake3(..)",
Self::Md5(_) => "Md5(..)",
Self::Ripemd128(_) => "Ripemd128(..)",
Self::Ripemd160(_) => "Ripemd160(..)",
Self::Ripemd256(_) => "Ripemd256(..)",
Self::Ripemd320(_) => "Ripemd320(..)",
Self::Sha1(_) => "Sha1(..)",
Self::Sha2224(_) => "Sha2224(..)",
Self::Sha2256(_) => "Sha2256(..)",
Self::Sha2384(_) => "Sha2384(..)",
Self::Sha2512(_) => "Sha2512(..)",
Self::Sha2512224(_) => "Sha2512224(..)",
Self::Sha2512256(_) => "Sha2512256(..)",
Self::Sha3224(_) => "Sha3224(..)",
Self::Sha3256(_) => "Sha3256(..)",
Self::Sha3384(_) => "Sha3384(..)",
Self::Sha3512(_) => "Sha3512(..)",
Self::Shake128(_) => "Shake128(..)",
Self::Shake256(_) => "Shake256(..)",
};
f.write_str(name)
}
}
#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub struct Multihash {
pub(crate) codec: Codec,
pub(crate) hash: Vec<u8>,
}
impl CodecInfo for Multihash {
fn preferred_codec() -> Codec {
SIGIL
}
fn codec(&self) -> Codec {
self.codec
}
}
impl EncodingInfo for Multihash {
fn preferred_encoding() -> Base {
Base::Base16Lower
}
fn encoding(&self) -> Base {
Self::preferred_encoding()
}
}
impl From<Multihash> for Vec<u8> {
fn from(mh: Multihash) -> Self {
let codec_bytes: Self = mh.codec.into();
let len_bytes = mh.hash.len().encode_into();
let total = codec_bytes.len() + len_bytes.len() + mh.hash.len();
let mut v = Self::with_capacity(total);
v.extend_from_slice(&codec_bytes);
v.extend_from_slice(&len_bytes);
v.extend_from_slice(&mh.hash);
v
}
}
impl<'a> TryFrom<&'a [u8]> for Multihash {
type Error = Error;
fn try_from(s: &'a [u8]) -> Result<Self, Self::Error> {
let (mh, _) = Self::try_decode_from(s)?;
Ok(mh)
}
}
impl<'a> TryDecodeFrom<'a> for Multihash {
type Error = Error;
fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> {
let (codec, ptr) = Codec::try_decode_from(bytes)?;
let (hash, ptr) = Varbytes::try_decode_from(ptr)?;
let hash = hash.to_inner();
Ok((Self { codec, hash }, ptr))
}
}
impl AsRef<[u8]> for Multihash {
fn as_ref(&self) -> &[u8] {
self.hash.as_ref()
}
}
impl ConstantTimeEq for Multihash {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
let codec_eq = u64::from(self.codec).ct_eq(&u64::from(other.codec));
let len_eq = self.hash.len().ct_eq(&other.hash.len());
let bytes_eq = self.hash.as_slice().ct_eq(other.hash.as_slice());
codec_eq & len_eq & bytes_eq
}
}
impl Null for Multihash {
fn null() -> Self {
Self::default()
}
fn is_null(&self) -> bool {
*self == Self::default()
}
}
impl fmt::Debug for Multihash {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:?} - {:?} - {}",
SIGIL,
self.codec(),
hex::encode(&self.hash)
)
}
}
#[derive(Clone, Debug)]
pub struct Builder {
codec: Codec,
hasher: Option<Hasher>,
hash: Option<Vec<u8>>,
output_len: Option<usize>,
base_encoding: Option<Base>,
}
impl Builder {
pub fn new(codec: Codec) -> Result<Self, Error> {
if !HASH_CODECS.contains(&codec) {
return Err(Error::unsupported_hash(codec));
}
Ok(Self {
codec,
hasher: None,
hash: None,
output_len: None,
base_encoding: None,
})
}
pub fn update(&mut self, data: impl AsRef<[u8]>) {
if self.hasher.is_none() {
self.hasher = Hasher::new(self.codec);
}
debug_assert!(
self.hasher.is_some(),
"Hasher::new must cover every codec in HASH_CODECS"
);
if let Some(hasher) = &mut self.hasher {
hasher.update(data.as_ref());
}
}
pub const fn output_len(&mut self, output_len: usize) {
self.output_len = Some(output_len);
}
#[must_use]
pub fn with_hash(mut self, hash: impl Into<Vec<u8>>) -> Self {
self.hash = Some(hash.into());
self
}
#[must_use]
pub const fn with_base_encoding(mut self, base: Base) -> Self {
self.base_encoding = Some(base);
self
}
pub fn try_build_encoded(self) -> Result<EncodedMultihash, Error> {
let Self {
codec,
hasher,
hash,
output_len,
base_encoding,
} = self;
let mh = build_multihash(codec, hasher, hash, output_len)?;
Ok(BaseEncoded::new(
base_encoding.unwrap_or_else(Multihash::preferred_encoding),
mh,
))
}
pub fn try_build(self) -> Result<Multihash, Error> {
let Self {
codec,
hasher,
hash,
output_len,
..
} = self;
build_multihash(codec, hasher, hash, output_len)
}
}
fn build_multihash(
codec: Codec,
hasher: Option<Hasher>,
hash: Option<Vec<u8>>,
output_len: Option<usize>,
) -> Result<Multihash, Error> {
let hash = match hash {
Some(hash) => hash,
None => match hasher {
Some(hasher) => hasher.finalize(codec, output_len)?,
None => return Err(Error::MissingHash),
},
};
validate_digest_length(codec, &hash)?;
Ok(Multihash { codec, hash })
}
#[cfg(test)]
mod tests {
use super::*;
fn xof_output_len(codec: Codec) -> Option<usize> {
match codec {
Codec::Blake3 | Codec::Shake128 => Some(32),
Codec::Shake256 => Some(64),
_ => None,
}
}
fn streamed_multihash(codec: Codec, data: &[u8]) -> Multihash {
let mut builder = Builder::new(codec).unwrap();
builder.update(data);
if let Some(output_len) = xof_output_len(codec) {
builder.output_len(output_len);
}
builder.try_build().unwrap()
}
fn streamed_encoded(codec: Codec, base: Base, data: &[u8]) -> EncodedMultihash {
let mut builder = Builder::new(codec).unwrap();
builder.update(data);
if let Some(output_len) = xof_output_len(codec) {
builder.output_len(output_len);
}
builder
.with_base_encoding(base)
.try_build_encoded()
.unwrap()
}
#[test]
fn test_matrix() {
let hashers = vec![
Codec::Blake2B224,
Codec::Blake2B256,
Codec::Blake2B384,
Codec::Blake2B512,
Codec::Blake2S224,
Codec::Blake2S256,
Codec::Blake3,
Codec::Md5,
Codec::Ripemd128,
Codec::Ripemd160,
Codec::Ripemd256,
Codec::Ripemd320,
Codec::Sha1,
Codec::Sha2224,
Codec::Sha2256,
Codec::Sha2384,
Codec::Sha2512,
Codec::Sha2512224,
Codec::Sha2512256,
Codec::Sha3224,
Codec::Sha3256,
Codec::Sha3384,
Codec::Sha3512,
Codec::Shake128,
Codec::Shake256,
];
let bases = vec![
Base::Base2,
Base::Base8,
Base::Base10,
Base::Base16Lower,
Base::Base16Upper,
Base::Base32Lower,
Base::Base32Upper,
Base::Base32PadLower,
Base::Base32PadUpper,
Base::Base32HexLower,
Base::Base32HexUpper,
Base::Base32HexPadLower,
Base::Base32HexPadUpper,
Base::Base32Z,
Base::Base36Lower,
Base::Base36Upper,
Base::Base58Flickr,
Base::Base58Btc,
Base::Base64,
Base::Base64Pad,
Base::Base64Url,
Base::Base64UrlPad,
];
for h in &hashers {
for b in &bases {
let mh1 = streamed_encoded(*h, *b, b"for great justice, move every zig!");
let s = mh1.to_string();
assert_eq!(mh1, EncodedMultihash::try_from(s.as_str()).unwrap());
}
}
}
#[test]
fn test_binary_roundtrip() {
let mh1 = streamed_multihash(Codec::Sha3384, b"for great justice, move every zig!");
let v: Vec<u8> = mh1.clone().into();
let mh2 = Multihash::try_from(v.as_ref()).unwrap();
assert_eq!(mh1, mh2);
}
#[test]
fn test_encoded() {
let mh = streamed_encoded(
Codec::Sha3256,
Base::Base58Btc,
b"for great justice, move every zig!",
);
let s = mh.to_string();
println!("{mh:?}");
println!("{s}");
assert_eq!(mh, EncodedMultihash::try_from(s.as_str()).unwrap());
}
#[test]
fn test_matching() {
let mh1 = streamed_multihash(Codec::Sha3256, b"for great justice, move every zig!");
let mh2 = Multihash::try_from(
hex::decode("16206b761d3b2e7675e088e337a82207b55711d3957efdb877a3d261b0ca2c38e201")
.unwrap()
.as_ref(),
)
.unwrap();
assert_eq!(mh1, mh2);
}
#[test]
fn test_null() {
let mh1 = Multihash::null();
assert!(mh1.is_null());
let mh2 = Multihash::default();
assert_eq!(mh1, mh2);
assert!(mh2.is_null());
}
#[test]
fn test_multihash_sha1() {
let bases = vec![
(
Base::Base16Lower,
"f111488c2f11fb2ce392acb5b2986e640211c4690073e",
),
(Base::Base32Upper, "BCEKIRQXRD6ZM4OJKZNNSTBXGIAQRYRUQA47A"),
(Base::Base58Btc, "z5dsgvJGnvAfiR3K6HCBc4hcokSfmjj"),
(Base::Base64, "mERSIwvEfss45KstbKYbmQCEcRpAHPg"),
];
for (b, h) in bases {
let mh = streamed_encoded(Codec::Sha1, b, b"multihash");
let s = mh.to_string();
assert_eq!(h, s.as_str());
}
}
#[test]
fn test_multihash_sha2_256() {
let bases = vec![
(
Base::Base16Lower,
"f12209cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47",
),
(
Base::Base32Upper,
"BCIQJZPAHYP4ZC4SYG2R2UKSYDSRAFEMYVJBAXHMZXQHBGHM7HYWL4RY",
),
(
Base::Base58Btc,
"zQmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk",
),
(
Base::Base64,
"mEiCcvAfD+ZFyWDajqipYHKICkZiqQgudmbwOEx2fPiy+Rw",
),
];
for (b, h) in bases {
let mh = streamed_encoded(Codec::Sha2256, b, b"multihash");
let s = mh.to_string();
assert_eq!(h, s.as_str());
}
}
#[test]
fn test_multihash_in_indexmap() {
let mut map = std::collections::HashMap::new();
let mh1 = streamed_multihash(Codec::Sha2256, b"for great justice, move every zig!");
let mh2 = streamed_multihash(Codec::Sha2256, b"for great justice, move every zag!");
map.insert(mh1, "zig");
map.insert(mh2, "zag");
assert_eq!(map.len(), 2);
}
#[test]
fn test_ct_eq_equal() {
let mh1 = streamed_multihash(Codec::Sha2256, b"hello");
let mh2 = streamed_multihash(Codec::Sha2256, b"hello");
assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 1);
}
#[test]
fn test_ct_eq_unequal_hash() {
let mh1 = streamed_multihash(Codec::Sha2256, b"hello");
let mh2 = streamed_multihash(Codec::Sha2256, b"world");
assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 0);
}
#[test]
fn test_ct_eq_unequal_codec() {
let mh1 = streamed_multihash(Codec::Sha2256, b"hello");
let mh2 = streamed_multihash(Codec::Sha2256, b"hello");
let mh3 = Multihash {
codec: Codec::Sha2512,
hash: mh1.hash.clone(),
};
assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 1);
assert_eq!(mh1.ct_eq(&mh3).unwrap_u8(), 0);
}
#[test]
fn test_ct_eq_unequal_length() {
let mh1 = streamed_multihash(Codec::Sha2256, b"hello");
let mh2 = Multihash {
codec: mh1.codec,
hash: vec![0u8; 16],
};
assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 0);
}
#[test]
fn test_builder_new_unsupported_codec() {
for &codec in &[Codec::Identity, Codec::DagCbor, Codec::Multihash] {
let result = Builder::new(codec);
assert!(
matches!(result, Err(Error::UnsupportedHash { .. })),
"codec {codec:?} was accepted"
);
}
}
#[test]
fn test_builder_missing_hash_state() {
let result = Builder::new(Codec::Sha2256).unwrap().try_build();
assert!(matches!(result, Err(Error::MissingHash)));
}
#[test]
fn test_builder_update_streaming() {
let mut builder = Builder::new(Codec::Sha2256).unwrap();
builder.update(b"multi");
builder.update(b"hash");
let mh = builder.try_build().unwrap();
assert_eq!(
hex::encode(mh.as_ref()),
"9cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47"
);
}
#[test]
fn test_builder_update_chunks_match_whole() {
let data = b"for great justice, move every zig!";
let mut chunked = Builder::new(Codec::Sha2256).unwrap();
chunked.update(&data[..7]);
chunked.update(&data[7..20]);
chunked.update(&data[20..]);
let chunked = chunked.try_build().unwrap();
let whole = streamed_multihash(Codec::Sha2256, data);
assert_eq!(chunked, whole);
}
#[test]
fn test_builder_with_hash_exact_length() {
let hash = vec![7u8; 32];
let mh = Builder::new(Codec::Sha2256)
.unwrap()
.with_hash(hash.clone())
.try_build()
.unwrap();
assert_eq!(mh.codec(), Codec::Sha2256);
assert_eq!(mh.as_ref(), hash.as_slice());
}
#[test]
fn test_builder_with_hash_wrong_length() {
for &codec in &HASH_CODECS {
match output_policy(codec) {
Some(OutputPolicy::Fixed(expected)) => {
let result = Builder::new(codec)
.unwrap()
.with_hash(vec![0u8; expected + 1])
.try_build();
assert!(
matches!(
result,
Err(Error::InvalidDigestLength {
expected: e,
actual: a,
..
}) if e == expected && a == expected + 1
),
"codec {codec:?} accepted a wrong-length digest"
);
}
Some(OutputPolicy::Xof) => {
let result = Builder::new(codec)
.unwrap()
.with_hash(Vec::new())
.try_build();
assert!(
matches!(result, Err(Error::OutputLenInvalid { output_len: 0, .. })),
"codec {codec:?} accepted an empty XOF digest"
);
}
None => panic!("codec {codec:?} is in HASH_CODECS but has no policy"),
}
}
}
#[test]
fn test_with_hash_precedence() {
let mut builder = Builder::new(Codec::Sha2256).unwrap();
builder.update(b"streamed data");
let explicit = vec![9u8; 32];
let mh = builder.with_hash(explicit.clone()).try_build().unwrap();
assert_eq!(mh.as_ref(), explicit.as_slice());
}
#[test]
fn test_with_hash_precedence_validates_length() {
let mut builder = Builder::new(Codec::Sha2256).unwrap();
builder.update(b"streamed data");
let result = builder.with_hash(vec![0u8; 31]).try_build();
assert!(matches!(result, Err(Error::InvalidDigestLength { .. })));
}
#[test]
fn test_builder_stream_policy_lengths() {
for &codec in &HASH_CODECS {
match output_policy(codec) {
Some(OutputPolicy::Xof) => {
let output_len = xof_output_len(codec).unwrap();
let mut builder = Builder::new(codec).unwrap();
builder.update(b"digest policy lengths");
builder.output_len(output_len);
let mh = builder.try_build().unwrap();
assert_eq!(mh.as_ref().len(), output_len, "codec {codec:?}");
let mh = Builder::new(codec)
.unwrap()
.with_hash(vec![0u8; output_len])
.try_build()
.unwrap();
assert_eq!(mh.as_ref().len(), output_len, "codec {codec:?}");
}
Some(OutputPolicy::Fixed(expected)) => {
let mut builder = Builder::new(codec).unwrap();
builder.update(b"digest policy lengths");
let mh = builder.try_build().unwrap();
assert_eq!(mh.as_ref().len(), expected, "codec {codec:?}");
let mh = Builder::new(codec)
.unwrap()
.with_hash(vec![0u8; expected])
.try_build()
.unwrap();
assert_eq!(mh.as_ref().len(), expected, "codec {codec:?}");
}
None => panic!("codec {codec:?} is in HASH_CODECS but has no policy"),
}
}
}
#[test]
fn test_try_build_consumes_builder() {
for &codec in &[Codec::Sha2256, Codec::Blake3, Codec::Sha3384] {
let mut original = Builder::new(codec).unwrap();
original.update(b"consumed");
if let Some(output_len) = xof_output_len(codec) {
original.output_len(output_len);
}
let snapshot = original.clone();
let mh1 = original.try_build().unwrap();
let mh2 = snapshot.try_build().unwrap();
assert_eq!(mh1, mh2, "codec {codec:?}");
}
}
#[test]
fn test_try_build_encoded_consumes_builder() {
let mut builder = Builder::new(Codec::Sha3256).unwrap();
builder.update(b"encoded consume");
let mh = builder
.with_base_encoding(Base::Base58Btc)
.try_build_encoded()
.unwrap();
let s = mh.to_string();
assert_eq!(mh, EncodedMultihash::try_from(s.as_str()).unwrap());
}
#[test]
fn test_builder_send_sync_streaming() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_clone<T: Clone>() {}
fn assert_debug<T: fmt::Debug>() {}
assert_send::<Builder>();
assert_sync::<Builder>();
assert_clone::<Builder>();
assert_debug::<Builder>();
let mut builder = Builder::new(Codec::Sha3256).unwrap();
builder.update(b"sent across threads");
let handle = std::thread::spawn(move || {
builder.update(b" and more");
builder.try_build().unwrap()
});
let mh = handle.join().unwrap();
assert_eq!(mh.codec(), Codec::Sha3256);
assert_eq!(mh.as_ref().len(), 32);
}
#[test]
fn test_builder_debug() {
let mut builder = Builder::new(Codec::Sha2256).unwrap();
let codec_debug = format!("{:?}", Codec::Sha2256);
let debug_idle = format!("{builder:?}");
assert!(debug_idle.contains(&codec_debug), "idle: {debug_idle}");
assert!(debug_idle.contains("None"), "idle: {debug_idle}");
builder.update(b"debug");
let debug_live = format!("{builder:?}");
assert!(debug_live.contains(&codec_debug), "live: {debug_live}");
assert!(
debug_live.contains("Some(Sha2256(..))"),
"live: {debug_live}"
);
}
#[test]
fn test_decode_ignores_digest_policy() {
let mut bytes = vec![0x12u8, 0x10];
bytes.extend_from_slice(&[0u8; 16]);
let mh = Multihash::try_from(bytes.as_ref()).unwrap();
assert_eq!(mh.codec(), Codec::Sha2256);
assert_eq!(mh.as_ref().len(), 16);
}
#[test]
fn test_codec_constant_lengths() {
assert_eq!(HASH_CODECS.len(), 25);
assert_eq!(SAFE_HASH_CODECS.len(), 10);
assert_eq!(MAX_HASH_LENGTH, 16 * 1024 * 1024);
assert_eq!(HASH_CODECS[23], Codec::Shake128);
assert_eq!(HASH_CODECS[24], Codec::Shake256);
assert_eq!(SAFE_HASH_CODECS[8], Codec::Shake128);
assert_eq!(SAFE_HASH_CODECS[9], Codec::Shake256);
}
#[test]
fn test_xof_output_len_required() {
for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] {
let mut builder = Builder::new(codec).unwrap();
builder.update(b"missing output length");
let result = builder.try_build();
assert!(
matches!(result, Err(Error::OutputLenRequired { .. })),
"codec {codec:?} built without an output length"
);
let mut builder = Builder::new(codec).unwrap();
builder.update(b"encoded without an output length");
let result = builder.try_build_encoded();
assert!(
matches!(result, Err(Error::OutputLenRequired { .. })),
"codec {codec:?} encoded without an output length"
);
}
}
#[test]
fn test_xof_output_len_invalid() {
for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] {
for &output_len in &[0, MAX_HASH_LENGTH + 1] {
let mut builder = Builder::new(codec).unwrap();
builder.update(b"bad output length");
builder.output_len(output_len);
let result = builder.try_build();
assert!(
matches!(
result,
Err(Error::OutputLenInvalid {
output_len: requested,
max,
..
}) if requested == output_len && max == MAX_HASH_LENGTH
),
"codec {codec:?} accepted output length {output_len}"
);
}
}
}
#[test]
fn test_xof_output_len_minimum() {
for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] {
let mut builder = Builder::new(codec).unwrap();
builder.update(b"one byte output");
builder.output_len(1);
let mh = builder.try_build().unwrap();
assert_eq!(mh.codec(), codec);
assert_eq!(mh.as_ref().len(), 1);
}
}
#[test]
fn test_fixed_output_ignores_output_len() {
for &output_len in &[0, 33, MAX_HASH_LENGTH + 1] {
let mut builder = Builder::new(Codec::Sha2256).unwrap();
builder.update(b"ignored output length");
builder.output_len(output_len);
let mh = builder.try_build().unwrap();
assert_eq!(mh.codec(), Codec::Sha2256);
assert_eq!(mh.as_ref().len(), 32);
}
let result = Builder::new(Codec::Sha2256)
.unwrap()
.with_hash(vec![0u8; 32])
.try_build();
assert!(result.is_ok());
}
#[test]
fn test_xof_prefix_consistency() {
for (codec, short_len) in [
(Codec::Blake3, 32usize),
(Codec::Shake128, 32usize),
(Codec::Shake256, 64usize),
] {
let data = b"prefix consistency";
let mut builder = Builder::new(codec).unwrap();
builder.update(data);
builder.output_len(short_len);
let short = builder.try_build().unwrap();
let mut builder = Builder::new(codec).unwrap();
builder.update(data);
builder.output_len(short_len * 2);
let long = builder.try_build().unwrap();
assert_eq!(short.codec(), long.codec());
assert_eq!(short.as_ref().len(), short_len);
assert_eq!(long.as_ref().len(), short_len * 2);
assert!(
long.as_ref().starts_with(short.as_ref()),
"codec {codec:?}: short digest is not a prefix of long digest"
);
let short_bytes: Vec<u8> = short.clone().into();
let long_bytes: Vec<u8> = long.clone().into();
assert_ne!(
short_bytes, long_bytes,
"codec {codec:?}: digest encodings matched across lengths"
);
}
}
#[test]
fn test_xof_send_sync_streaming() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Builder>();
assert_sync::<Builder>();
let mut builder = Builder::new(Codec::Shake128).unwrap();
builder.update(b"sent across threads");
builder.output_len(32);
let handle = std::thread::spawn(move || {
builder.update(b" and more");
builder.try_build().unwrap()
});
let streamed = handle.join().unwrap();
let mut one_shot = Builder::new(Codec::Shake128).unwrap();
one_shot.update(b"sent across threads and more");
one_shot.output_len(32);
let one_shot = one_shot.try_build().unwrap();
assert_eq!(streamed, one_shot);
}
#[test]
fn test_xof_with_hash_policy() {
let hash = vec![3u8; 8];
let mut builder = Builder::new(Codec::Shake256).unwrap();
builder.update(b"streamed but ignored");
builder.output_len(64);
let mh = builder.with_hash(hash.clone()).try_build().unwrap();
assert_eq!(mh.codec(), Codec::Shake256);
assert_eq!(mh.as_ref(), hash.as_slice());
}
}