diff --git a/CHANGELOG.md b/CHANGELOG.md index 78256e7..1c60f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-10-13 + +### Added + +- One feature per hash family: `blake2`, `blake3`, `md5`, `ripemd`, `sha1`, `sha2`, `sha3`, and `shake`. Each one pulls in only its hash crate (plus `digest`, and `typenum` for `blake2`). All of them are in the default feature set, so a default build behaves as 2.x did. +- With `default-features = false` the crate depends on no hash crate. It still provides the `Multihash` type, its binary and multibase encoding, `HASH_CODECS` and the other codec lists, the digest length checks, and `Builder::with_hash` for digests computed elsewhere. +- `impl From<&Multihash> for Vec` encodes without cloning. The owned `From` now delegates to it, and binary serde serialization uses it instead of cloning the multihash. + +### Changed + +- **Breaking:** a build with `default-features = false` no longer computes digests. In 2.x that setting only dropped `serde`. Streaming data with `Builder::update` into a codec whose family feature is off makes `try_build` and `try_build_encoded` return `Error::UnsupportedHash`, where 2.x returned the digest. Add the hash family features you use to keep 2.x behavior. +- `Error::UnsupportedHash` now also covers a codec whose hash family feature is disabled, and its message says so. +- The `serde` feature now enables the optional `serde` dependency through `dep:serde`. +- The benchmark declares `required-features`, so `cargo bench` skips it when a hash family it uses is off. +- Binary serde deserialization decodes straight from the input bytes instead of copying them into an intermediate buffer first. +- Removed three redundant `clone()` calls in the XOF prefix tests that failed `cargo clippy --all-targets -- -D warnings`. + +### Dependencies + +- `blake2`, `blake3`, `digest`, `md-5`, `ripemd`, `sha1`, `sha2`, `sha3`, `shake`, and `typenum` are now optional. + ## [2.0.0] - 2026-10-07 ### Added diff --git a/Cargo.toml b/Cargo.toml index e84694e..e52de9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-hash" -version = "2.0.1" +version = "3.0.0" edition = "2024" rust-version = "1.99" authors = ["Dave Grantham "] @@ -12,30 +12,51 @@ keywords = ["multihash", "multiformats", "hash", "crypto"] categories = ["cryptography", "encoding"] [features] -default = ["serde"] +default = [ + "blake2", + "blake3", + "md5", + "ripemd", + "serde", + "sha1", + "sha2", + "sha3", + "shake", +] +# each hash family is its own feature; with none enabled the crate still +# provides the Multihash type, its encoding, and Builder::with_hash +blake2 = ["dep:blake2", "dep:digest", "dep:typenum"] +blake3 = ["dep:blake3"] # expose the FIPS approved codec constants, FIPS_CODECS and SAFE_FIPS_CODECS fips = [] +md5 = ["dep:digest", "dep:md-5"] +ripemd = ["dep:digest", "dep:ripemd"] +serde = ["dep:serde"] +sha1 = ["dep:digest", "dep:sha1"] +sha2 = ["dep:digest", "dep:sha2"] +sha3 = ["dep:digest", "dep:sha3"] +shake = ["dep:digest", "dep:shake"] [dependencies] -blake2 = "0.11.0" -blake3 = { version = "1.8", features = ["traits-preview", "zeroize"] } +blake2 = { version = "0.11.0", optional = true } +blake3 = { version = "1.8", features = ["traits-preview", "zeroize"], optional = true } +digest = { version = "0.11", features = ["alloc"], optional = true } +hex = "0.4" +md-5 = { version = "0.11", optional = true } multi-base = "1.1" multi-codec = "1.6" multi-trait = "1.1" multi-util = "1.2" -digest = { version = "0.11", features = ["alloc"] } -hex = "0.4" -md-5 = "0.11" -ripemd = "0.2" +ripemd = { version = "0.2", optional = true } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } -sha1 = "0.11" -sha2 = "0.11" -sha3 = "0.12" -shake = "0.1" +sha1 = { version = "0.11", optional = true } +sha2 = { version = "0.11", optional = true } +sha3 = { version = "0.12", optional = true } +shake = { version = "0.1", optional = true } subtle = "2" thiserror = { version = "2.0" } -typenum = "1.20" +typenum = { version = "1.20", optional = true } unsigned-varint = { version = "0.8", features = ["std"] } [dev-dependencies] @@ -62,3 +83,4 @@ unsafe_code = "deny" name = "multihash_bench" harness = false path = "benches/multihash_bench.rs" +required-features = ["blake3", "md5", "sha2", "sha3", "shake"] diff --git a/README.md b/README.md index 60a0ee5..2923acc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Multihash is a self-describing format. It pairs a hash algorithm identifier (a m - [Features](#features) - [Install](#install) +- [Upgrading from 2.x](#upgrading-from-2x) - [Supported Algorithms](#supported-algorithms) - [Usage](#usage) - [Computing a Hash](#computing-a-hash) @@ -37,11 +38,11 @@ Multihash is a self-describing format. It pairs a hash algorithm identifier (a m ## Features -- 25 hash algorithms. SHA1, SHA2 family, SHA3 family, Blake2, Blake3, MD5, RIPEMD. `Blake3`, `Shake128`, and `Shake256` are extendable-output functions (XOF) with a caller-chosen digest length. +- 25 hash algorithms. SHA1, SHA2 family, SHA3 family, Blake2, Blake3, MD5, RIPEMD, each family behind its own [feature](#feature-flags). `Blake3`, `Shake128`, and `Shake256` are extendable-output functions (XOF) with a caller-chosen digest length. - Builder pattern. A fluent API to create multihashes from raw data or existing digests. - Multibase encoding. The `EncodedMultihash` smart pointer gives a base-encoded string representation via `BaseEncoded` from `multi-util`. - Serde support. JSON gives the codec name string. Binary gives the varint bytes. The `serde` feature gates it. -- Binary round-trip. `Into>` and `TryFrom<&[u8]>` for the raw wire format. +- Binary round-trip. `From<&Multihash>` and `From` for `Vec`, and `TryFrom<&[u8]>`, for the raw wire format. Encoding by reference does not clone the multihash. - Type-safe newtypes. `HashDigest` and `AlgorithmId` wrappers. - Zero unsafe code. `#![deny(unsafe_code)]` is set at the crate root. - Thread-safe. All types are `Send + Sync`. @@ -52,18 +53,28 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -multi-hash = "2.0" +multi-hash = "3.0" ``` -To disable serde support: +To pick only the hash families you need, turn off the defaults and list them (see [Feature Flags](#feature-flags)): ```toml [dependencies] -multi-hash = { version = "2.0", default-features = false } +multi-hash = { version = "3.0", default-features = false, features = ["sha2", "sha3"] } ``` MSRV: Rust 1.99 (Edition 2024). +## Upgrading from 2.x + +3.0 puts each hash family behind its own feature. The default build has every family on, so `multi-hash = "3.0"` behaves as 2.x did. + +- If you used `default-features = false` only to drop serde, add back the families you hash with, e.g. `features = ["sha2", "blake3"]`. Without them, `try_build` returns `Error::UnsupportedHash` for a streamed codec, where 2.x returned the digest. +- `Builder::with_hash` and decoding work for every codec in any build, so a crate that only wraps or parses digests can drop all hash crates. +- Encode by reference with `Vec::::from(&mh)` to avoid cloning. The owned `From` still works. + +See [CHANGELOG.md](CHANGELOG.md) for the full list. + ## Supported Algorithms ### Secure algorithms (recommended for cryptographic use) @@ -193,8 +204,8 @@ let mut builder = Builder::new(Codec::Sha2256).unwrap(); builder.update(b"data"); let mh1 = builder.try_build().unwrap(); -// Encode to binary (varint wire format) -let bytes: Vec = mh1.clone().into(); +// Encode to binary (varint wire format); by reference, so mh1 is not cloned +let bytes: Vec = (&mh1).into(); // Decode from binary let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); @@ -393,6 +404,21 @@ CI collects coverage with `cargo-llvm-cov` and uploads the result to Codecov. ## Feature Flags +Each hash family is its own feature, and all of them are on by default: + +| Feature | Codecs | Crate | +|---|---|---| +| `blake2` | `Blake2B224`, `Blake2B256`, `Blake2B384`, `Blake2B512`, `Blake2S224`, `Blake2S256` | `blake2` | +| `blake3` | `Blake3` (XOF) | `blake3` | +| `md5` | `Md5` | `md-5` | +| `ripemd` | `Ripemd128`, `Ripemd160`, `Ripemd256`, `Ripemd320` | `ripemd` | +| `sha1` | `Sha1` | `sha1` | +| `sha2` | `Sha2224`, `Sha2256`, `Sha2384`, `Sha2512`, `Sha2512224`, `Sha2512256` | `sha2` | +| `sha3` | `Sha3224`, `Sha3256`, `Sha3384`, `Sha3512` | `sha3` | +| `shake` | `Shake128`, `Shake256` (XOF) | `shake` | + +A family feature is only needed to compute a digest. With `default-features = false` and no family enabled, the crate still provides the `Multihash` type, its binary and multibase encoding, the codec lists and output-length checks, and `Builder::with_hash` for digests computed elsewhere. Streaming data with `Builder::update` into a codec whose family is off makes `try_build` return `Error::UnsupportedHash`. + - `serde` (default). Enables serde serialization and deserialization. When on, `Multihash` implements `Serialize` and `Deserialize`. Human-readable formats give the codec name and the hex digest. Binary formats give the varint bytes. - `fips` (additive, off by default). Exports `FIPS_CODECS` (13 codecs) and `SAFE_FIPS_CODECS` (8 codecs). They list the NIST FIPS approved hash algorithms. `SAFE_FIPS_CODECS` omits SHA-1 and the 224-bit algorithms that SP 800-131A restricts. `Blake2`, `Blake3`, `Md5`, and the RIPEMD codecs are not FIPS approved. SHA-1 is verification-only under NIST SP 800-131A Rev. 2. The SP 800-131A Rev. 3 draft deprecates SHA-1 and the 224-bit hashes through 2030 and disallows them after 2030. @@ -400,14 +426,16 @@ The `fips` feature composes with the default `serde` feature: ```toml [dependencies] -multi-hash = { version = "2.0", features = ["fips"] } +multi-hash = { version = "3.0", features = ["fips"] } ``` ### Disabling Default Features +This build has no hash crates and no serde; it decodes, encodes, and wraps existing digests only: + ```toml [dependencies] -multi-hash = { version = "2.0", default-features = false } +multi-hash = { version = "3.0", default-features = false } ``` ## Security diff --git a/SECURITY.md b/SECURITY.md index 441fff0..5ebe584 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ The `multi-hash` crate gives self-describing cryptographic hash digests. It foll ## std-only Status -The crate is std-only. The hash path holds each hasher as a concrete type in a private `Hasher` enum. The enum dispatches between the hashers without a trait object. The BLAKE3 arm holds a boxed `blake3::Hasher`. The boxed hasher and the digest buffers need the allocator in `std`. A `no_std` conversion is not planned for this crate. +The crate is std-only. The hash path holds each hasher as a concrete type in a private `Hasher` enum. The enum dispatches between the hashers without a trait object. Each arm exists only when its hash family feature (`blake2`, `blake3`, `md5`, `ripemd`, `sha1`, `sha2`, `sha3`, `shake`) is enabled. The BLAKE3 arm holds a boxed `blake3::Hasher`. The boxed hasher and the digest buffers need the allocator in `std`. A `no_std` conversion is not planned for this crate. ## Security Properties diff --git a/src/error.rs b/src/error.rs index 18c7e96..e6f9ba5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,8 +64,9 @@ pub enum Error { /// Unsupported hash algorithm /// - /// The specified codec is not a supported cryptographic hash algorithm - /// or is not implemented in this crate. + /// The specified codec is not a supported cryptographic hash algorithm, + /// is not implemented in this crate, or belongs to a hash family whose + /// feature is disabled in this build. /// /// # Context /// @@ -90,7 +91,8 @@ pub enum Error { #[error( "Unsupported hash algorithm: {codec:?}\n\ The codec {codec:?} is not a supported cryptographic hash algorithm.\n\ - See HASH_CODECS or SAFE_HASH_CODECS for supported algorithms." + See HASH_CODECS or SAFE_HASH_CODECS for supported algorithms;\n\ + computing a digest also needs the codec's hash family feature." )] UnsupportedHash { /// The unsupported codec that was requested @@ -433,6 +435,7 @@ mod tests { assert_ne!(context, ""); } + #[cfg(feature = "sha2")] #[test] fn test_invalid_digest_length_error() { let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16); @@ -442,6 +445,7 @@ mod tests { assert!(msg.contains("16")); } + #[cfg(feature = "sha2")] #[test] fn test_hash_compute_failed_error() { let err = Error::hash_compute_failed(Codec::Sha2256, "test failure"); @@ -449,6 +453,7 @@ mod tests { assert!(err.to_string().contains("test failure")); } + #[cfg(feature = "shake")] #[test] fn test_output_len_required_error() { let err = Error::output_len_required(Codec::Shake128); @@ -459,6 +464,7 @@ mod tests { assert!(context.contains("Shake128") || context.contains("shake-128")); } + #[cfg(feature = "shake")] #[test] fn test_output_len_invalid_error() { let err = Error::output_len_invalid(Codec::Shake256, 0, 16 * 1024 * 1024); @@ -471,6 +477,7 @@ mod tests { assert!(context.contains("Shake256") || context.contains("shake-256")); } + #[cfg(all(feature = "sha2", feature = "shake"))] #[test] fn test_error_kind_uniqueness() { let errors = [ @@ -495,6 +502,7 @@ mod tests { } } + #[cfg(feature = "sha2")] #[test] fn test_error_context_informative() { let err = Error::unsupported_hash(Codec::Sha2256); diff --git a/src/lib.rs b/src/lib.rs index b5a3c37..eb96059 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ //! ### Computing a Hash //! //! ```rust +//! # #[cfg(feature = "sha2")] { //! use multi_hash::Builder; //! use multi_codec::Codec; //! use multi_util::CodecInfo; @@ -54,6 +55,7 @@ //! //! assert_eq!(multihash.codec(), Codec::Sha2256); //! assert_eq!(multihash.as_ref().len(), 32); // SHA2-256 outputs 32 bytes +//! # } //! ``` //! //! ### Computing a SHAKE256 Digest of a Chosen Length @@ -63,6 +65,7 @@ //! [`Builder::output_len`] before `try_build`: //! //! ```rust +//! # #[cfg(feature = "shake")] { //! use multi_hash::Builder; //! use multi_codec::Codec; //! use multi_util::CodecInfo; @@ -74,6 +77,7 @@ //! //! assert_eq!(multihash.codec(), Codec::Shake256); //! assert_eq!(multihash.as_ref().len(), 64); +//! # } //! ``` //! //! ### Creating from Existing Hash @@ -94,6 +98,7 @@ //! ### Encoding and Decoding //! //! ```rust +//! # #[cfg(feature = "sha2")] { //! use multi_hash::{Builder, Multihash}; //! use multi_codec::Codec; //! @@ -107,11 +112,13 @@ //! // Decode from bytes //! let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); //! assert_eq!(mh1, mh2); +//! # } //! ``` //! //! ### Base Encoding //! //! ```rust +//! # #[cfg(feature = "sha2")] { //! use multi_hash::Builder; //! use multi_codec::Codec; //! use multi_base::Base; @@ -127,6 +134,7 @@ //! // Display as base58-encoded string //! let base58_string = encoded.to_string(); //! println!("Multihash: {}", base58_string); +//! # } //! ``` //! //! ## Type Safety @@ -187,6 +195,7 @@ //! All types are `Send + Sync` and safe for concurrent use: //! //! ```rust +//! # #[cfg(feature = "sha2")] { //! use std::sync::Arc; //! use std::thread; //! use multi_hash::Builder; @@ -201,6 +210,7 @@ //! }); //! //! handle.join().unwrap(); +//! # } //! ``` //! //! ## Performance @@ -237,16 +247,23 @@ //! //! ## Features //! +//! Each hash family is its own feature, all enabled by default: `blake2`, +//! `blake3`, `md5`, `ripemd`, `sha1`, `sha2`, `sha3`, and `shake`. A family +//! feature is only needed to compute a digest. With `default-features = +//! false` the crate provides the [`Multihash`] type, its encoding, and +//! [`Builder::with_hash`] for digests computed elsewhere; streaming into a +//! codec whose family is disabled fails with `Error::UnsupportedHash`. +//! //! - **`serde`** (default): Enables serde serialization support //! - **`fips`**: Exports the `FIPS_CODECS` and `SAFE_FIPS_CODECS` //! constants. They list the hash algorithms on the NIST FIPS approved //! lists. `SAFE_FIPS_CODECS` omits SHA-1 and the 224-bit algorithms that //! SP 800-131A restricts. //! -//! To disable serde: +//! To select hash families, disable the defaults and list them: //! ```toml //! [dependencies] -//! multi-hash = { version = "2.0", default-features = false } +//! multi-hash = { version = "3.0", default-features = false, features = ["sha2", "sha3"] } //! ``` #![warn(missing_docs)] @@ -282,6 +299,7 @@ pub mod serde; /// Commonly used items /// /// ``` +/// # #[cfg(feature = "sha2")] { /// use multi_hash::prelude::*; /// /// let mut builder = Builder::new(Codec::Sha2256).unwrap(); @@ -289,6 +307,7 @@ pub mod serde; /// let mh = builder.try_build().unwrap(); /// // CodecInfo trait is in prelude /// assert_eq!(mh.codec(), Codec::Sha2256); +/// # } /// ``` pub mod prelude { pub use super::*; diff --git a/src/mh.rs b/src/mh.rs index df37939..de70b1f 100644 --- a/src/mh.rs +++ b/src/mh.rs @@ -9,12 +9,23 @@ use crate::Error; use core::fmt; use core::hash::Hash; -use digest::{Digest, ExtendableOutput, Update}; +#[cfg(any( + feature = "blake2", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3" +))] +use digest::Digest; +#[cfg(feature = "shake")] +use 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; +#[cfg(feature = "blake2")] use typenum::consts::{U28, U32, U48, U64}; /// the maximum multihash digest output length in bytes @@ -258,95 +269,167 @@ const fn validate_output_len(codec: Codec, output_len: Option) -> Result< /// /// Enum dispatch keeps the hashing state concrete: every arm is `Send`, /// `Sync`, `Clone`, and `Debug`, and hashing avoids a trait-object -/// allocation per hash. +/// allocation per hash. Each arm exists only when its hash family +/// feature is enabled. +#[cfg(any( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] #[derive(Clone)] enum Hasher { /// blake2b with a 224-bit digest + #[cfg(feature = "blake2")] Blake2B224(blake2::Blake2b), /// blake2b with a 256-bit digest + #[cfg(feature = "blake2")] Blake2B256(blake2::Blake2b), /// blake2b with a 384-bit digest + #[cfg(feature = "blake2")] Blake2B384(blake2::Blake2b), /// blake2b with a 512-bit digest + #[cfg(feature = "blake2")] Blake2B512(blake2::Blake2b), /// blake2s with a 224-bit digest + #[cfg(feature = "blake2")] Blake2S224(blake2::Blake2s), /// blake2s with a 256-bit digest + #[cfg(feature = "blake2")] Blake2S256(blake2::Blake2s), /// blake3 extendable-output function /// /// `blake3::Hasher` is roughly 2 KB, so its arm is boxed to keep the /// other arms, and every [`Builder`] value, small + #[cfg(feature = "blake3")] Blake3(Box), /// md5 with a 128-bit digest + #[cfg(feature = "md5")] Md5(md5::Md5), /// ripemd128 with a 128-bit digest + #[cfg(feature = "ripemd")] Ripemd128(ripemd::Ripemd128), /// ripemd160 with a 160-bit digest + #[cfg(feature = "ripemd")] Ripemd160(ripemd::Ripemd160), /// ripemd256 with a 256-bit digest + #[cfg(feature = "ripemd")] Ripemd256(ripemd::Ripemd256), /// ripemd320 with a 320-bit digest + #[cfg(feature = "ripemd")] Ripemd320(ripemd::Ripemd320), /// sha1 with a 160-bit digest + #[cfg(feature = "sha1")] Sha1(sha1::Sha1), /// sha2-224 with a 224-bit digest + #[cfg(feature = "sha2")] Sha2224(sha2::Sha224), /// sha2-256 with a 256-bit digest + #[cfg(feature = "sha2")] Sha2256(sha2::Sha256), /// sha2-384 with a 384-bit digest + #[cfg(feature = "sha2")] Sha2384(sha2::Sha384), /// sha2-512 with a 512-bit digest + #[cfg(feature = "sha2")] Sha2512(sha2::Sha512), /// sha2-512/224 with a 224-bit digest + #[cfg(feature = "sha2")] Sha2512224(sha2::Sha512_224), /// sha2-512/256 with a 256-bit digest + #[cfg(feature = "sha2")] Sha2512256(sha2::Sha512_256), /// sha3-224 with a 224-bit digest + #[cfg(feature = "sha3")] Sha3224(sha3::Sha3_224), /// sha3-256 with a 256-bit digest + #[cfg(feature = "sha3")] Sha3256(sha3::Sha3_256), /// sha3-384 with a 384-bit digest + #[cfg(feature = "sha3")] Sha3384(sha3::Sha3_384), /// sha3-512 with a 512-bit digest + #[cfg(feature = "sha3")] Sha3512(sha3::Sha3_512), /// shake128 extendable-output function + #[cfg(feature = "shake")] Shake128(shake::Shake128), /// shake256 extendable-output function + #[cfg(feature = "shake")] Shake256(shake::Shake256), } +#[cfg(any( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] impl Hasher { /// create the streaming hasher for a codec /// - /// Returns `None` only for codecs outside [`HASH_CODECS`], which - /// [`Builder::new`] rejects up front. + /// Returns `None` for codecs outside [`HASH_CODECS`], which + /// [`Builder::new`] rejects up front, and for codecs whose hash family + /// feature is disabled. fn new(codec: Codec) -> Option { Some(match codec { + #[cfg(feature = "blake2")] Codec::Blake2B224 => Self::Blake2B224(blake2::Blake2b::::new()), + #[cfg(feature = "blake2")] Codec::Blake2B256 => Self::Blake2B256(blake2::Blake2b::::new()), + #[cfg(feature = "blake2")] Codec::Blake2B384 => Self::Blake2B384(blake2::Blake2b::::new()), + #[cfg(feature = "blake2")] Codec::Blake2B512 => Self::Blake2B512(blake2::Blake2b::::new()), + #[cfg(feature = "blake2")] Codec::Blake2S224 => Self::Blake2S224(blake2::Blake2s::::new()), + #[cfg(feature = "blake2")] Codec::Blake2S256 => Self::Blake2S256(blake2::Blake2s::::new()), + #[cfg(feature = "blake3")] Codec::Blake3 => Self::Blake3(Box::new(blake3::Hasher::new())), + #[cfg(feature = "md5")] Codec::Md5 => Self::Md5(md5::Md5::new()), + #[cfg(feature = "ripemd")] Codec::Ripemd128 => Self::Ripemd128(ripemd::Ripemd128::new()), + #[cfg(feature = "ripemd")] Codec::Ripemd160 => Self::Ripemd160(ripemd::Ripemd160::new()), + #[cfg(feature = "ripemd")] Codec::Ripemd256 => Self::Ripemd256(ripemd::Ripemd256::new()), + #[cfg(feature = "ripemd")] Codec::Ripemd320 => Self::Ripemd320(ripemd::Ripemd320::new()), + #[cfg(feature = "sha1")] Codec::Sha1 => Self::Sha1(sha1::Sha1::new()), + #[cfg(feature = "sha2")] Codec::Sha2224 => Self::Sha2224(sha2::Sha224::new()), + #[cfg(feature = "sha2")] Codec::Sha2256 => Self::Sha2256(sha2::Sha256::new()), + #[cfg(feature = "sha2")] Codec::Sha2384 => Self::Sha2384(sha2::Sha384::new()), + #[cfg(feature = "sha2")] Codec::Sha2512 => Self::Sha2512(sha2::Sha512::new()), + #[cfg(feature = "sha2")] Codec::Sha2512224 => Self::Sha2512224(sha2::Sha512_224::new()), + #[cfg(feature = "sha2")] Codec::Sha2512256 => Self::Sha2512256(sha2::Sha512_256::new()), + #[cfg(feature = "sha3")] Codec::Sha3224 => Self::Sha3224(sha3::Sha3_224::new()), + #[cfg(feature = "sha3")] Codec::Sha3256 => Self::Sha3256(sha3::Sha3_256::new()), + #[cfg(feature = "sha3")] Codec::Sha3384 => Self::Sha3384(sha3::Sha3_384::new()), + #[cfg(feature = "sha3")] Codec::Sha3512 => Self::Sha3512(sha3::Sha3_512::new()), + #[cfg(feature = "shake")] Codec::Shake128 => Self::Shake128(shake::Shake128::default()), + #[cfg(feature = "shake")] Codec::Shake256 => Self::Shake256(shake::Shake256::default()), _ => return None, }) @@ -355,89 +438,132 @@ impl Hasher { /// feed data to the streaming hasher fn update(&mut self, data: &[u8]) { match self { + #[cfg(feature = "blake2")] Self::Blake2B224(h) => Digest::update(h, data), + #[cfg(feature = "blake2")] Self::Blake2B256(h) => Digest::update(h, data), + #[cfg(feature = "blake2")] Self::Blake2B384(h) => Digest::update(h, data), + #[cfg(feature = "blake2")] Self::Blake2B512(h) => Digest::update(h, data), + #[cfg(feature = "blake2")] Self::Blake2S224(h) => Digest::update(h, data), + #[cfg(feature = "blake2")] Self::Blake2S256(h) => Digest::update(h, data), // the streaming calls go through the fully qualified inherent // path: blake3 also supplies trait methods of the same names + #[cfg(feature = "blake3")] Self::Blake3(h) => { blake3::Hasher::update(h, data); } + #[cfg(feature = "md5")] Self::Md5(h) => Digest::update(h, data), + #[cfg(feature = "ripemd")] Self::Ripemd128(h) => Digest::update(h, data), + #[cfg(feature = "ripemd")] Self::Ripemd160(h) => Digest::update(h, data), + #[cfg(feature = "ripemd")] Self::Ripemd256(h) => Digest::update(h, data), + #[cfg(feature = "ripemd")] Self::Ripemd320(h) => Digest::update(h, data), + #[cfg(feature = "sha1")] Self::Sha1(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2224(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2256(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2384(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2512(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2512224(h) => Digest::update(h, data), + #[cfg(feature = "sha2")] Self::Sha2512256(h) => Digest::update(h, data), + #[cfg(feature = "sha3")] Self::Sha3224(h) => Digest::update(h, data), + #[cfg(feature = "sha3")] Self::Sha3256(h) => Digest::update(h, data), + #[cfg(feature = "sha3")] Self::Sha3384(h) => Digest::update(h, data), + #[cfg(feature = "sha3")] Self::Sha3512(h) => Digest::update(h, data), // the XOF arms take `Update::update`; they do not implement // the fixed-output `Digest` trait + #[cfg(feature = "shake")] Self::Shake128(h) => Update::update(h, data), + #[cfg(feature = "shake")] Self::Shake256(h) => Update::update(h, data), } } /// finish the streamed hash and return the digest bytes /// - /// Fixed-output arms ignore `output_len` and produce their policy - /// length. The XOF arms require an output length of `1..=MAX_HASH_LENGTH` - /// bytes, checked before any allocation or squeeze. - fn finalize(self, codec: Codec, output_len: Option) -> Result, 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()), + /// `out_len` is the codec's resolved output length: fixed-output arms + /// ignore it and produce their policy length, and the XOF arms squeeze + /// exactly `out_len` bytes, already checked against `1..=MAX_HASH_LENGTH`. + fn finalize(self, out_len: usize) -> Vec { + match (self, out_len) { + #[cfg(feature = "blake2")] + (Self::Blake2B224(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "blake2")] + (Self::Blake2B256(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "blake2")] + (Self::Blake2B384(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "blake2")] + (Self::Blake2B512(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "blake2")] + (Self::Blake2S224(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "blake2")] + (Self::Blake2S256(h), _) => Digest::finalize(h).to_vec(), // see `update`: fully qualified inherent path for blake3 // // `finalize_xof` gives a fresh reader at stream offset zero, so // the digest is always the first `out_len` bytes of the BLAKE3 // output stream. A 32-byte request matches the classic fixed // `blake3` digest - Self::Blake3(h) => { - let out_len = validate_output_len(codec, output_len)?; + #[cfg(feature = "blake3")] + (Self::Blake3(h), out_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)) + out } + #[cfg(feature = "md5")] + (Self::Md5(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "ripemd")] + (Self::Ripemd128(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "ripemd")] + (Self::Ripemd160(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "ripemd")] + (Self::Ripemd256(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "ripemd")] + (Self::Ripemd320(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha1")] + (Self::Sha1(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2224(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2256(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2384(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2512(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2512224(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha2")] + (Self::Sha2512256(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha3")] + (Self::Sha3224(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha3")] + (Self::Sha3256(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha3")] + (Self::Sha3384(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "sha3")] + (Self::Sha3512(h), _) => Digest::finalize(h).to_vec(), + #[cfg(feature = "shake")] + (Self::Shake128(h), out_len) => xof_finalize(h, out_len), + #[cfg(feature = "shake")] + (Self::Shake256(h), out_len) => xof_finalize(h, out_len), } } } @@ -446,45 +572,125 @@ impl Hasher { /// /// `ExtendableOutput::finalize_xof_into` reads the first `out_len` bytes of /// the XOF stream, so the read always starts at offset zero. +#[cfg(feature = "shake")] fn xof_finalize(hasher: T, out_len: usize) -> Vec { let mut out = vec![0u8; out_len]; ExtendableOutput::finalize_xof_into(hasher, &mut out); out } +#[cfg(any( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] impl fmt::Debug for Hasher { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { + #[cfg(feature = "blake2")] Self::Blake2B224(_) => "Blake2B224(..)", + #[cfg(feature = "blake2")] Self::Blake2B256(_) => "Blake2B256(..)", + #[cfg(feature = "blake2")] Self::Blake2B384(_) => "Blake2B384(..)", + #[cfg(feature = "blake2")] Self::Blake2B512(_) => "Blake2B512(..)", + #[cfg(feature = "blake2")] Self::Blake2S224(_) => "Blake2S224(..)", + #[cfg(feature = "blake2")] Self::Blake2S256(_) => "Blake2S256(..)", + #[cfg(feature = "blake3")] Self::Blake3(_) => "Blake3(..)", + #[cfg(feature = "md5")] Self::Md5(_) => "Md5(..)", + #[cfg(feature = "ripemd")] Self::Ripemd128(_) => "Ripemd128(..)", + #[cfg(feature = "ripemd")] Self::Ripemd160(_) => "Ripemd160(..)", + #[cfg(feature = "ripemd")] Self::Ripemd256(_) => "Ripemd256(..)", + #[cfg(feature = "ripemd")] Self::Ripemd320(_) => "Ripemd320(..)", + #[cfg(feature = "sha1")] Self::Sha1(_) => "Sha1(..)", + #[cfg(feature = "sha2")] Self::Sha2224(_) => "Sha2224(..)", + #[cfg(feature = "sha2")] Self::Sha2256(_) => "Sha2256(..)", + #[cfg(feature = "sha2")] Self::Sha2384(_) => "Sha2384(..)", + #[cfg(feature = "sha2")] Self::Sha2512(_) => "Sha2512(..)", + #[cfg(feature = "sha2")] Self::Sha2512224(_) => "Sha2512224(..)", + #[cfg(feature = "sha2")] Self::Sha2512256(_) => "Sha2512256(..)", + #[cfg(feature = "sha3")] Self::Sha3224(_) => "Sha3224(..)", + #[cfg(feature = "sha3")] Self::Sha3256(_) => "Sha3256(..)", + #[cfg(feature = "sha3")] Self::Sha3384(_) => "Sha3384(..)", + #[cfg(feature = "sha3")] Self::Sha3512(_) => "Sha3512(..)", + #[cfg(feature = "shake")] Self::Shake128(_) => "Shake128(..)", + #[cfg(feature = "shake")] Self::Shake256(_) => "Shake256(..)", }; f.write_str(name) } } +/// the streaming hasher state when no hash family feature is enabled +/// +/// No hasher can exist, so [`Builder::update`] has nothing to feed and a +/// streamed build fails with `Error::UnsupportedHash`. +#[cfg(not(any( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +)))] +#[derive(Clone, Debug)] +struct Hasher(core::convert::Infallible); + +#[cfg(not(any( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +)))] +impl Hasher { + /// no hash family is compiled in, so no codec has a hasher + const fn new(_codec: Codec) -> Option { + None + } + + /// unreachable: no `Hasher` value can exist + const fn update(&self, _data: &[u8]) { + match self.0 {} + } + + /// unreachable: no `Hasher` value can exist + const fn finalize(self, _out_len: usize) -> Vec { + match self.0 {} + } +} + /// inner implementation of the multihash /// /// # Constant-Time Comparison @@ -527,6 +733,13 @@ impl EncodingInfo for Multihash { impl From for Vec { fn from(mh: Multihash) -> Self { + Self::from(&mh) + } +} + +/// Encode a borrowed multihash without cloning it +impl From<&Multihash> for Vec { + fn from(mh: &Multihash) -> Self { // Pre-calculate total size: codec varint + length varint + hash bytes let codec_bytes: Self = mh.codec.into(); let len_bytes = mh.hash.len().encode_into(); @@ -633,6 +846,7 @@ impl fmt::Debug for Multihash { /// # Examples /// /// ``` +/// # #[cfg(feature = "sha2")] { /// use multi_hash::Builder; /// use multi_codec::Codec; /// @@ -642,6 +856,7 @@ impl fmt::Debug for Multihash { /// let multihash = builder.try_build().unwrap(); /// /// assert_eq!(multihash.as_ref().len(), 32); +/// # } /// ``` #[derive(Clone, Debug)] pub struct Builder { @@ -700,6 +915,7 @@ impl Builder { /// # Examples /// /// ``` + /// # #[cfg(feature = "sha2")] { /// use multi_hash::Builder; /// use multi_codec::Codec; /// @@ -709,17 +925,14 @@ impl Builder { /// let multihash = builder.try_build().unwrap(); /// /// assert_eq!(multihash.as_ref().len(), 32); + /// # } /// ``` pub fn update(&mut self, data: impl AsRef<[u8]>) { - // `Builder::new` accepts only codecs in `HASH_CODECS`, for which - // `Hasher::new` always produces a hasher + // a codec whose hash family feature is disabled has no hasher; the + // data is dropped and `try_build` reports `UnsupportedHash` 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()); } @@ -738,6 +951,7 @@ impl Builder { /// # Examples /// /// ``` + /// # #[cfg(feature = "blake3")] { /// use multi_hash::Builder; /// use multi_codec::Codec; /// @@ -747,6 +961,7 @@ impl Builder { /// let multihash = builder.try_build().unwrap(); /// /// assert_eq!(multihash.as_ref().len(), 32); + /// # } /// ``` /// /// # XOF notes @@ -842,8 +1057,9 @@ impl Builder { /// finish hash state into a validated multihash /// /// An explicit digest takes precedence over the streaming hasher. A missing -/// digest fails with `Error::MissingHash`. The digest then runs through the -/// codec's output policy. +/// digest fails with `Error::MissingHash`, or with `Error::UnsupportedHash` +/// when the codec's hash family feature is disabled. The digest then runs +/// through the codec's output policy. fn build_multihash( codec: Codec, hasher: Option, @@ -853,7 +1069,16 @@ fn build_multihash( let hash = match hash { Some(hash) => hash, None => match hasher { - Some(hasher) => hasher.finalize(codec, output_len)?, + Some(hasher) => { + // the XOF length is checked before any allocation or squeeze + let out_len = match output_policy(codec) { + Some(OutputPolicy::Fixed(len)) => len, + Some(OutputPolicy::Xof) => validate_output_len(codec, output_len)?, + None => return Err(Error::unsupported_hash(codec)), + }; + hasher.finalize(out_len) + } + None if Hasher::new(codec).is_none() => return Err(Error::unsupported_hash(codec)), None => return Err(Error::MissingHash), }, }; @@ -869,6 +1094,7 @@ mod tests { /// /// Fixed-output codecs ignore an output length, so the helper applies /// only to the XOF arms, `Blake3`, `Shake128`, and `Shake256`. + #[cfg(any(feature = "sha1", feature = "sha2", feature = "sha3"))] fn xof_output_len(codec: Codec) -> Option { match codec { Codec::Blake3 | Codec::Shake128 => Some(32), @@ -878,6 +1104,7 @@ mod tests { } /// build a multihash of `data` with `codec`, streaming the data + #[cfg(any(feature = "sha2", feature = "sha3"))] fn streamed_multihash(codec: Codec, data: &[u8]) -> Multihash { let mut builder = Builder::new(codec).unwrap(); builder.update(data); @@ -888,6 +1115,7 @@ mod tests { } /// build a base encoded multihash of `data` with `codec` + #[cfg(any(feature = "sha1", feature = "sha2", feature = "sha3"))] fn streamed_encoded(codec: Codec, base: Base, data: &[u8]) -> EncodedMultihash { let mut builder = Builder::new(codec).unwrap(); builder.update(data); @@ -900,6 +1128,16 @@ mod tests { .unwrap() } + #[cfg(all( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" + ))] #[test] fn test_matrix() { let hashers = vec![ @@ -964,6 +1202,7 @@ mod tests { } } + #[cfg(feature = "sha3")] #[test] fn test_binary_roundtrip() { let mh1 = streamed_multihash(Codec::Sha3384, b"for great justice, move every zig!"); @@ -972,6 +1211,7 @@ mod tests { assert_eq!(mh1, mh2); } + #[cfg(feature = "sha3")] #[test] fn test_encoded() { let mh = streamed_encoded( @@ -985,6 +1225,7 @@ mod tests { assert_eq!(mh, EncodedMultihash::try_from(s.as_str()).unwrap()); } + #[cfg(feature = "sha3")] #[test] fn test_matching() { let mh1 = streamed_multihash(Codec::Sha3256, b"for great justice, move every zig!"); @@ -1006,6 +1247,7 @@ mod tests { assert!(mh2.is_null()); } + #[cfg(feature = "sha1")] #[test] fn test_multihash_sha1() { // test cases from: https://github.com/multiformats/multihash?tab=readme-ov-file#example @@ -1026,6 +1268,7 @@ mod tests { } } + #[cfg(feature = "sha2")] #[test] fn test_multihash_sha2_256() { // test cases from: https://github.com/multiformats/multihash?tab=readme-ov-file#example @@ -1055,6 +1298,7 @@ mod tests { } } + #[cfg(feature = "sha2")] #[test] fn test_multihash_in_indexmap() { let mut map = std::collections::HashMap::new(); @@ -1069,6 +1313,7 @@ mod tests { assert_eq!(map.len(), 2); } + #[cfg(feature = "sha2")] #[test] fn test_ct_eq_equal() { let mh1 = streamed_multihash(Codec::Sha2256, b"hello"); @@ -1077,6 +1322,7 @@ mod tests { assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 1); } + #[cfg(feature = "sha2")] #[test] fn test_ct_eq_unequal_hash() { let mh1 = streamed_multihash(Codec::Sha2256, b"hello"); @@ -1085,6 +1331,7 @@ mod tests { assert_eq!(mh1.ct_eq(&mh2).unwrap_u8(), 0); } + #[cfg(feature = "sha2")] #[test] fn test_ct_eq_unequal_codec() { let mh1 = streamed_multihash(Codec::Sha2256, b"hello"); @@ -1099,6 +1346,7 @@ mod tests { assert_eq!(mh1.ct_eq(&mh3).unwrap_u8(), 0); } + #[cfg(feature = "sha2")] #[test] fn test_ct_eq_unequal_length() { let mh1 = streamed_multihash(Codec::Sha2256, b"hello"); @@ -1125,6 +1373,7 @@ mod tests { /// a builder without streamed data and without an explicit digest fails /// at build time + #[cfg(feature = "sha2")] #[test] fn test_builder_missing_hash_state() { let result = Builder::new(Codec::Sha2256).unwrap().try_build(); @@ -1132,6 +1381,7 @@ mod tests { } /// streaming feeds data through the lazy hasher + #[cfg(feature = "sha2")] #[test] fn test_builder_update_streaming() { let mut builder = Builder::new(Codec::Sha2256).unwrap(); @@ -1148,6 +1398,7 @@ mod tests { } /// streaming in chunks equals hashing the whole input at once + #[cfg(feature = "sha2")] #[test] fn test_builder_update_chunks_match_whole() { let data = b"for great justice, move every zig!"; @@ -1214,7 +1465,36 @@ mod tests { } } + /// streaming a codec whose hash family feature is disabled fails with + /// `UnsupportedHash`, while an explicit digest still builds + #[cfg(not(feature = "md5"))] + #[test] + fn test_builder_disabled_codec() { + let mut builder = Builder::new(Codec::Md5).unwrap(); + builder.update(b"no md5 hasher"); + let result = builder.try_build(); + assert!( + matches!(result, Err(Error::UnsupportedHash { codec }) if codec == Codec::Md5), + "streamed build: {result:?}" + ); + + let result = Builder::new(Codec::Md5).unwrap().try_build(); + assert!( + matches!(result, Err(Error::UnsupportedHash { .. })), + "empty build: {result:?}" + ); + + let mh = Builder::new(Codec::Md5) + .unwrap() + .with_hash(vec![5u8; 16]) + .try_build() + .unwrap(); + assert_eq!(mh.codec(), Codec::Md5); + assert_eq!(mh.as_ref(), [5u8; 16].as_slice()); + } + /// a digest set with `with_hash` takes precedence over streamed data + #[cfg(feature = "sha2")] #[test] fn test_with_hash_precedence() { let mut builder = Builder::new(Codec::Sha2256).unwrap(); @@ -1225,6 +1505,7 @@ mod tests { } /// a wrong-length explicit digest fails even with valid streamed data + #[cfg(feature = "sha2")] #[test] fn test_with_hash_precedence_validates_length() { let mut builder = Builder::new(Codec::Sha2256).unwrap(); @@ -1235,6 +1516,16 @@ mod tests { /// every codec streams to its output policy length, and an explicit /// digest of that length is accepted + #[cfg(all( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" + ))] #[test] fn test_builder_stream_policy_lengths() { for &codec in &HASH_CODECS { @@ -1273,6 +1564,7 @@ mod tests { } /// `try_build` consumes the builder; a clone keeps building independently + #[cfg(all(feature = "blake3", feature = "sha2", feature = "sha3"))] #[test] fn test_try_build_consumes_builder() { for &codec in &[Codec::Sha2256, Codec::Blake3, Codec::Sha3384] { @@ -1289,6 +1581,7 @@ mod tests { } /// `try_build_encoded` consumes the builder too + #[cfg(feature = "sha3")] #[test] fn test_try_build_encoded_consumes_builder() { let mut builder = Builder::new(Codec::Sha3256).unwrap(); @@ -1303,6 +1596,7 @@ mod tests { /// a builder with live hashing state stays `Send`, `Sync`, `Clone`, and /// `Debug`, and its cloned state builds the same digest + #[cfg(feature = "sha3")] #[test] fn test_builder_send_sync_streaming() { fn assert_send() {} @@ -1328,6 +1622,7 @@ mod tests { /// builder debug output names the codec and the active hasher without /// exposing hash state + #[cfg(feature = "sha2")] #[test] fn test_builder_debug() { let mut builder = Builder::new(Codec::Sha2256).unwrap(); @@ -1372,6 +1667,7 @@ mod tests { /// a streamed XOF build without an output length fails with /// `OutputLenRequired` + #[cfg(all(feature = "blake3", feature = "shake"))] #[test] fn test_xof_output_len_required() { for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] { @@ -1395,6 +1691,7 @@ mod tests { /// a zero or over-limit XOF output length fails with `OutputLenInvalid` /// before any allocation or squeeze + #[cfg(all(feature = "blake3", feature = "shake"))] #[test] fn test_xof_output_len_invalid() { for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] { @@ -1419,6 +1716,7 @@ mod tests { } /// the minimum XOF output length of one byte builds + #[cfg(all(feature = "blake3", feature = "shake"))] #[test] fn test_xof_output_len_minimum() { for &codec in &[Codec::Blake3, Codec::Shake128, Codec::Shake256] { @@ -1432,6 +1730,7 @@ mod tests { } /// fixed-output codecs ignore an `output_len` setting + #[cfg(feature = "sha2")] #[test] fn test_fixed_output_ignores_output_len() { for &output_len in &[0, 33, MAX_HASH_LENGTH + 1] { @@ -1453,6 +1752,7 @@ mod tests { /// XOF prefix consistency: a short streamed digest prefixes the long /// digest of the same input, while the two multihashes still encode /// differently + #[cfg(all(feature = "blake3", feature = "shake"))] #[test] fn test_xof_prefix_consistency() { for (codec, short_len) in [ @@ -1480,8 +1780,8 @@ mod tests { "codec {codec:?}: short digest is not a prefix of long digest" ); - let short_bytes: Vec = short.clone().into(); - let long_bytes: Vec = long.clone().into(); + let short_bytes: Vec = short.into(); + let long_bytes: Vec = long.into(); assert_ne!( short_bytes, long_bytes, "codec {codec:?}: digest encodings matched across lengths" @@ -1491,6 +1791,7 @@ mod tests { /// a builder streaming an XOF stays `Send` and `Sync`, and the digest /// built across a thread matches the one-shot digest + #[cfg(feature = "shake")] #[test] fn test_xof_send_sync_streaming() { fn assert_send() {} @@ -1519,6 +1820,7 @@ mod tests { /// an explicit XOF digest inside the `1..=MAX_HASH_LENGTH` policy /// builds, and the digest takes precedence over an `output_len` /// setting + #[cfg(feature = "shake")] #[test] fn test_xof_with_hash_policy() { let hash = vec![3u8; 8]; diff --git a/src/serde/de.rs b/src/serde/de.rs index 0ee9782..faf52df 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -73,30 +73,27 @@ impl<'de> Deserialize<'de> for Multihash { // (BorrowedBytes), `serde_cbor` (borrowed), and `ciborium` // (owned). The previous `&'de [u8]` bound only worked with // deserializers that support borrowing from input. + // decodes straight from the input bytes, without first copying + // them into an intermediate buffer struct ByteBufVisitor; - impl<'de> Visitor<'de> for ByteBufVisitor { - type Value = Vec; + impl Visitor<'_> for ByteBufVisitor { + type Value = Multihash; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("byte buffer") } - fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(v.to_vec()) - } - fn visit_bytes(self, v: &[u8]) -> Result { - Ok(v.to_vec()) + Multihash::try_from(v).map_err(E::custom) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(v) + self.visit_bytes(v.as_slice()) } } - let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; - Ok(Self::try_from(b.as_slice()).map_err(|e| Error::custom(e.to_string()))?) + deserializer.deserialize_byte_buf(ByteBufVisitor) } } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 16bebd7..6d71944 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -5,11 +5,14 @@ mod ser; #[cfg(test)] mod tests { - use crate::prelude::{Base, Builder, Codec, Multihash}; + use crate::prelude::Multihash; + #[cfg(feature = "blake2")] + use crate::prelude::{Base, Builder, Codec}; use multi_trait::Null; use serde_test::{Configure, Token, assert_tokens}; /// build a multihash of `data` with `codec`, streaming the data + #[cfg(feature = "blake2")] fn hash_bytes(codec: Codec, data: &[u8]) -> Multihash { let mut builder = Builder::new(codec).unwrap(); builder.update(data); @@ -18,12 +21,14 @@ mod tests { /// Serialize a value to CBOR bytes using `ciborium` (replaces the /// unmaintained `serde_cbor` dev-dependency). + #[cfg(feature = "blake2")] fn cbor_to_vec(value: &T) -> Vec { let mut buf = Vec::new(); ciborium::into_writer(value, &mut buf).expect("CBOR serialize"); buf } + #[cfg(feature = "blake2")] #[test] fn test_serde_compact() { let mh = hash_bytes(Codec::Blake2S256, b"for great justice, move every zig!"); @@ -39,6 +44,7 @@ mod tests { ); } + #[cfg(feature = "blake2")] #[test] fn test_serde_encoded_string() { let mut builder = Builder::new(Codec::Blake2S256).unwrap(); @@ -56,6 +62,7 @@ mod tests { ); } + #[cfg(feature = "blake2")] #[test] fn test_serde_string() { let mh = hash_bytes(Codec::Blake2S256, b"for great justice, move every zig!"); @@ -77,6 +84,7 @@ mod tests { ); } + #[cfg(feature = "blake2")] #[test] fn test_serde_json() { let mh1 = hash_bytes(Codec::Blake2S256, b"for great justice, move every zig!"); @@ -86,6 +94,7 @@ mod tests { assert_eq!(mh1, mh2); } + #[cfg(feature = "blake2")] #[test] fn test_serde_cbor() { let mh1 = hash_bytes(Codec::Blake2S256, b"for great justice, move every zig!"); diff --git a/src/serde/ser.rs b/src/serde/ser.rs index cafdaea..430c2ee 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -18,7 +18,7 @@ impl ser::Serialize for Multihash { )?; ss.end() } else { - let v: Vec = self.clone().into(); + let v: Vec = self.into(); serializer.serialize_bytes(v.as_slice()) } } diff --git a/src/types.rs b/src/types.rs index 03665e8..1b0f0a1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -305,24 +305,28 @@ mod tests { assert_eq!(map.get(&digest), Some(&"test")); } + #[cfg(feature = "sha2")] #[test] fn test_algorithm_id_new() { let algo = AlgorithmId::new(Codec::Sha2256); assert_eq!(algo.codec(), Codec::Sha2256); } + #[cfg(feature = "sha2")] #[test] fn test_algorithm_id_code() { let algo = AlgorithmId::new(Codec::Sha2256); assert_eq!(algo.code(), 0x12); } + #[cfg(feature = "sha2")] #[test] fn test_algorithm_id_name() { let algo = AlgorithmId::new(Codec::Sha2256); assert_eq!(algo.name(), "sha2-256"); } + #[cfg(feature = "sha2")] #[test] fn test_algorithm_id_conversions() { let codec = Codec::Sha2512; @@ -331,12 +335,14 @@ mod tests { assert_eq!(back, codec); } + #[cfg(feature = "sha3")] #[test] fn test_algorithm_id_display() { let algo = AlgorithmId::new(Codec::Sha3256); assert_eq!(algo.to_string(), "sha3-256"); } + #[cfg(feature = "blake3")] #[test] fn test_algorithm_id_copy() { let algo1 = AlgorithmId::new(Codec::Blake3); diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs index 761b26f..cf88c2d 100644 --- a/tests/edge_case_tests.rs +++ b/tests/edge_case_tests.rs @@ -1,5 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 //! Edge case tests for multi-hash +#![cfg(all( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] use multi_codec::Codec; use multi_hash::{Builder, Error, HASH_CODECS, Multihash, SAFE_HASH_CODECS}; diff --git a/tests/fips_tests.rs b/tests/fips_tests.rs index 35402c1..0fa90b8 100644 --- a/tests/fips_tests.rs +++ b/tests/fips_tests.rs @@ -7,7 +7,17 @@ //! keep them contained in the general codec lists, and build a multihash //! for every approved codec through the builder, checking the digests //! of a sample against independently generated known-answer values. -#![cfg(feature = "fips")] +#![cfg(all( + feature = "fips", + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] use multi_codec::Codec; use multi_hash::{Builder, FIPS_CODECS, HASH_CODECS, Multihash, SAFE_FIPS_CODECS}; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index b36afe4..1c16871 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 //! Integration tests for multi-hash with other workspace crates +#![cfg(all( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] #![allow(clippy::unreadable_literal)] use multi_base::Base; diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs index aeae821..ab8de37 100644 --- a/tests/proptest_tests.rs +++ b/tests/proptest_tests.rs @@ -1,5 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 //! Property-based tests for multi-hash using proptest +#![cfg(all( + feature = "blake2", + feature = "blake3", + feature = "md5", + feature = "ripemd", + feature = "sha1", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] use multi_codec::Codec; use multi_hash::{Builder, HASH_CODECS, Multihash}; diff --git a/tests/security_tests.rs b/tests/security_tests.rs index ab6091a..9c74bb8 100644 --- a/tests/security_tests.rs +++ b/tests/security_tests.rs @@ -1,5 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 //! Security-focused tests for multi-hash +#![cfg(all( + feature = "blake2", + feature = "blake3", + feature = "sha2", + feature = "sha3", + feature = "shake" +))] use multi_codec::Codec; use multi_hash::{Builder, Error, Multihash, SAFE_HASH_CODECS}; diff --git a/tests/xof_tests.rs b/tests/xof_tests.rs index 316b594..0f2edea 100644 --- a/tests/xof_tests.rs +++ b/tests/xof_tests.rs @@ -17,6 +17,7 @@ //! value for the same message. A 32-byte BLAKE3 digest stays a BLAKE3 //! digest: it is no BLAKE2b-256 value, because the codecs are distinct //! algorithms. +#![cfg(all(feature = "blake3", feature = "sha2", feature = "shake"))] #![allow(clippy::unreadable_literal)] use multi_base::Base; @@ -262,7 +263,7 @@ fn test_distinct_lengths_distinct_encodings() { "{codec:?}: short digest is not a prefix of the long digest" ); - let short_bytes: Vec = short.clone().into(); + let short_bytes: Vec = short.into(); let long_bytes: Vec = long.into(); assert_ne!(short_bytes, long_bytes, "{codec:?}: encodings matched");