diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a204b1c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-07-13 + +### Changed +- Synced from bettersign workspace (bs-multihash 0.7.0) +- Renamed crate from `bs-multihash` to `multi-hash` +- Added `types.rs` module with type-safe wrappers +- Added comprehensive test suite (edge cases, integration, proptest, security) +- Initial published release on crates.io as `multi-hash` \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 15ff176..6745b5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [package] -name = "multihash" -version = "1.0.4" +name = "multi-hash" +version = "1.0.2" edition = "2021" authors = ["Dave Grantham "] description = "Multihash self-describing cryptographic hash data" -repository = "https://github.com/cryptidtech/multihash.git" +repository = "https://github.com/cryptidtech/multi-hash.git" readme = "README.md" license = "Apache-2.0" +keywords = ["multihash", "multiformats", "hash", "crypto"] +categories = ["cryptography", "encoding"] [features] default = ["serde"] @@ -14,24 +16,31 @@ default = ["serde"] [dependencies] blake2 = "0.10" blake3 = { version = "1.5.1", features = ["traits-preview", "zeroize"] } +multi-base = "1.0" +multi-codec = "1.0" +multi-trait = "1.0" +multi-util = "1.0" digest = "0.10" hex = "0.4" md-5 = "0.10" -multibase = { version = "1.0", git = "https://github.com/cryptidtech/rust-multibase.git" } -multicodec = { version = "1.0", git = "https://github.com/cryptidtech/rust-multicodec.git" } -multitrait = { version = "1.0", git = "https://github.com/cryptidtech/multitrait.git" } -multiutil = { version = "1.0", git = "https://github.com/cryptidtech/multiutil.git" } ripemd = "0.1.3" serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } sha1 = "0.10" sha2 = "0.10" sha3 = "0.10" -thiserror = "1.0" +thiserror = { version = "2.0" } typenum = "1.17" -unsigned-varint = { version = "0.8", features = ["std"]} +unsigned-varint = { version = "0.8", features = ["std"] } [dev-dependencies] +criterion = { version = "0.8", features = ["html_reports"] } hex = "0.4" -serde_test = "1.0" -serde_json = "1.0" +proptest = "1.4" serde_cbor = "0.11" +serde_json = "1.0" +serde_test = "1.0" + +[[bench]] +name = "multihash_bench" +harness = false +path = "benches/multihash_bench.rs" diff --git a/benches/multihash_bench.rs b/benches/multihash_bench.rs new file mode 100644 index 0000000..ab318a8 --- /dev/null +++ b/benches/multihash_bench.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Performance benchmarks for multi-hash + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use multi_codec::Codec; +use multi_hash::{Builder, Multihash}; +use multi_trait::TryDecodeFrom; +use std::hint::black_box; + +/// Benchmark hash computation for various algorithms +fn bench_hash_computation(c: &mut Criterion) { + let mut group = c.benchmark_group("hash_computation"); + let data = black_box(b"benchmark data for hashing"); + + let algorithms = vec![ + ("Blake3", Codec::Blake3), + ("SHA2-256", Codec::Sha2256), + ("SHA2-512", Codec::Sha2512), + ("SHA3-256", Codec::Sha3256), + ("SHA3-512", Codec::Sha3512), + ]; + + for (name, codec) in algorithms { + group.bench_with_input(BenchmarkId::new("compute", name), &codec, |b, &codec| { + b.iter(|| Builder::new_from_bytes(codec, data).unwrap().try_build()) + }); + } + + group.finish(); +} + +/// Benchmark encoding multihash to bytes +fn bench_encoding(c: &mut Criterion) { + let mh = Builder::new_from_bytes(Codec::Sha2256, b"test data") + .unwrap() + .try_build() + .unwrap(); + + c.bench_function("multihash_to_bytes", |b| { + b.iter(|| { + let _bytes: Vec = black_box(mh.clone()).into(); + }) + }); +} + +/// Benchmark decoding multihash from bytes +fn bench_decoding(c: &mut Criterion) { + let mh = Builder::new_from_bytes(Codec::Sha2256, b"test data") + .unwrap() + .try_build() + .unwrap(); + let bytes: Vec = mh.into(); + + c.bench_function("multihash_from_bytes", |b| { + b.iter(|| Multihash::try_from(black_box(bytes.as_ref()))) + }); +} + +/// Benchmark roundtrip (encode + decode) +fn bench_roundtrip(c: &mut Criterion) { + let mut group = c.benchmark_group("roundtrip"); + + let data = b"roundtrip benchmark data"; + + for &codec in [Codec::Blake3, Codec::Sha2256, Codec::Sha3256].iter() { + let name = format!("{:?}", codec); + group.bench_with_input(BenchmarkId::new("full", &name), &codec, |b, &codec| { + b.iter(|| { + let mh1 = Builder::new_from_bytes(codec, data) + .unwrap() + .try_build() + .unwrap(); + let bytes: Vec = mh1.into(); + let _mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); + }) + }); + } + + group.finish(); +} + +/// Benchmark hash computation with varying data sizes +fn bench_data_sizes(c: &mut Criterion) { + let mut group = c.benchmark_group("data_sizes"); + + let sizes = vec![16, 256, 1024, 4096]; + + for size in sizes { + let data = vec![0u8; size]; + group.bench_with_input(BenchmarkId::new("sha2-256", size), &data, |b, data| { + b.iter(|| { + Builder::new_from_bytes(Codec::Sha2256, black_box(data)) + .unwrap() + .try_build() + }) + }); + } + + group.finish(); +} + +/// Benchmark builder pattern operations +fn bench_builder(c: &mut Criterion) { + c.bench_function("builder_from_bytes", |b| { + b.iter(|| Builder::new_from_bytes(black_box(Codec::Sha2256), black_box(b"data"))) + }); + + let hash = vec![0u8; 32]; + c.bench_function("builder_with_hash", |b| { + b.iter(|| { + Builder::new(black_box(Codec::Sha2256)) + .with_hash(black_box(hash.clone())) + .try_build() + }) + }); +} + +/// Benchmark TryDecodeFrom with varying encoded sizes +fn bench_decode_from(c: &mut Criterion) { + let mut group = c.benchmark_group("decode_from"); + + let test_cases = vec![ + ("small", Codec::Md5), // 16 byte output + ("medium", Codec::Sha2256), // 32 byte output + ("large", Codec::Sha2512), // 64 byte output + ]; + + for (name, codec) in test_cases { + let mh = Builder::new_from_bytes(codec, b"test") + .unwrap() + .try_build() + .unwrap(); + let bytes: Vec = mh.into(); + + group.bench_with_input(BenchmarkId::new("decode", name), &bytes, |b, bytes| { + b.iter(|| Multihash::try_decode_from(black_box(bytes.as_ref()))) + }); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_hash_computation, + bench_encoding, + bench_decoding, + bench_roundtrip, + bench_data_sizes, + bench_builder, + bench_decode_from +); + +criterion_main!(benches); diff --git a/src/error.rs b/src/error.rs index 0d83fe1..e202219 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,18 +1,365 @@ -// SPDX-License-Idnetifier: Apache-2.0 -/// Errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +// SPDX-License-Identifier: Apache-2.0 +//! Error types for multi-hash +//! +//! This module provides comprehensive error types with detailed context +//! for debugging and better user experience. + +/// Errors produced by the multi-hash crate +/// +/// All error variants include contextual information to help with debugging +/// and provide actionable error messages. +/// +/// # Examples +/// +/// ``` +/// use multi_hash::Error; +/// use multi_codec::Codec; +/// +/// // Unsupported algorithm error includes the codec +/// let err = Error::unsupported_hash(Codec::Identity); +/// assert!(matches!(err, Error::UnsupportedHash { .. })); +/// +/// // Missing hash error +/// let err = Error::MissingHash; +/// assert_eq!(err.kind(), "MissingHash"); +/// ``` +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { - /// A multicodec decoding error + /// Error from multi-codec crate + /// + /// This error occurs when codec operations fail, typically due to + /// invalid codec identifiers or encoding/decoding issues. #[error(transparent)] - Multicodec(#[from] multicodec::Error), - /// Multiutil error + Multicodec(#[from] multi_codec::Error), + + /// Error from multi-util crate + /// + /// This error occurs when utility operations fail, such as base encoding + /// or variable-length integer operations. #[error(transparent)] - Multiutil(#[from] multiutil::Error), + Multiutil(#[from] multi_util::Error), + /// Missing hash data - #[error("Missing hash data")] + /// + /// The multihash builder was used without setting the hash digest data. + /// + /// # Resolution + /// + /// Call `Builder::with_hash()` to set the hash digest before calling `build()`. + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// + /// let err = Error::MissingHash; + /// assert_eq!(err.kind(), "MissingHash"); + /// ``` + #[error( + "Missing hash data\n\ + The multihash builder requires hash digest data.\n\ + Call Builder::with_hash() before build()." + )] MissingHash, - /// Error with the hash scheme - #[error("Unsupported hash algorithm: {0}")] - UnsupportedHash(multicodec::Codec), + + /// Unsupported hash algorithm + /// + /// The specified codec is not a supported cryptographic hash algorithm + /// or is not implemented in this crate. + /// + /// # Context + /// + /// - `codec`: The unsupported codec that was requested + /// + /// # Resolution + /// + /// Use one of the supported hash algorithms. See the `HASH_CODECS` or + /// `SAFE_HASH_CODECS` constants for the list of supported algorithms. + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::unsupported_hash(Codec::Identity); + /// assert!(matches!(err, Error::UnsupportedHash { .. })); + /// ``` + #[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." + )] + UnsupportedHash { + /// The unsupported codec that was requested + codec: multi_codec::Codec, + }, + + /// Invalid hash digest length + /// + /// The provided hash digest length doesn't match the expected output + /// size for the specified hash algorithm. + /// + /// # Context + /// + /// - `algorithm`: The hash algorithm codec + /// - `expected`: The expected digest length in bytes + /// - `actual`: The actual digest length provided + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16); + /// assert!(matches!(err, Error::InvalidDigestLength { .. })); + /// ``` + #[error( + "Invalid hash digest length for {algorithm:?}\n\ + Expected {expected} bytes for {algorithm:?}, but got {actual} bytes.\n\ + Ensure the hash digest matches the algorithm's output size." + )] + InvalidDigestLength { + /// The hash algorithm + algorithm: multi_codec::Codec, + /// Expected digest length in bytes + expected: usize, + /// Actual digest length provided + actual: usize, + }, + + /// Hash computation failed + /// + /// An error occurred while computing the cryptographic hash. + /// + /// # Context + /// + /// - `algorithm`: The hash algorithm that failed + /// - `message`: Description of what went wrong + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::hash_compute_failed(Codec::Sha2256, "input too large"); + /// assert!(matches!(err, Error::HashComputeFailed { .. })); + /// ``` + #[error("Hash computation failed for {algorithm:?}: {message}")] + HashComputeFailed { + /// The hash algorithm + algorithm: multi_codec::Codec, + /// Error message + message: String, + }, +} + +impl Error { + /// Create an UnsupportedHash error + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::unsupported_hash(Codec::Identity); + /// assert!(matches!(err, Error::UnsupportedHash { .. })); + /// ``` + pub fn unsupported_hash(codec: multi_codec::Codec) -> Self { + Self::UnsupportedHash { codec } + } + + /// Create an InvalidDigestLength error + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16); + /// assert!(matches!(err, Error::InvalidDigestLength { .. })); + /// ``` + pub fn invalid_digest_length( + algorithm: multi_codec::Codec, + expected: usize, + actual: usize, + ) -> Self { + Self::InvalidDigestLength { + algorithm, + expected, + actual, + } + } + + /// Create a HashComputeFailed error + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::hash_compute_failed(Codec::Sha2256, "internal error"); + /// assert!(matches!(err, Error::HashComputeFailed { .. })); + /// ``` + pub fn hash_compute_failed(algorithm: multi_codec::Codec, message: impl Into) -> Self { + Self::HashComputeFailed { + algorithm, + message: message.into(), + } + } + + /// Get the error kind as a string + /// + /// Returns a short identifier for the error type, useful for + /// programmatic error handling and logging. + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::MissingHash; + /// assert_eq!(err.kind(), "MissingHash"); + /// + /// let err = Error::unsupported_hash(Codec::Identity); + /// assert_eq!(err.kind(), "UnsupportedHash"); + /// ``` + pub fn kind(&self) -> &str { + match self { + Self::Multicodec(_) => "Multicodec", + Self::Multiutil(_) => "Multiutil", + Self::MissingHash => "MissingHash", + Self::UnsupportedHash { .. } => "UnsupportedHash", + Self::InvalidDigestLength { .. } => "InvalidDigestLength", + Self::HashComputeFailed { .. } => "HashComputeFailed", + } + } + + /// Get additional context about the error + /// + /// Returns human-readable context information that can help + /// diagnose the error. + /// + /// # Examples + /// + /// ``` + /// use multi_hash::Error; + /// use multi_codec::Codec; + /// + /// let err = Error::unsupported_hash(Codec::Identity); + /// let context = err.context(); + /// assert!(!context.is_empty()); + /// ``` + pub fn context(&self) -> String { + match self { + Self::Multicodec(e) => format!("Multicodec error: {}", e), + Self::Multiutil(e) => format!("Multiutil error: {}", e), + Self::MissingHash => "Missing hash data in builder".to_string(), + Self::UnsupportedHash { codec } => format!("Unsupported hash: {:?}", codec), + Self::InvalidDigestLength { + algorithm, + expected, + actual, + } => format!( + "Invalid digest length for {:?}: expected {}, got {}", + algorithm, expected, actual + ), + Self::HashComputeFailed { algorithm, message } => { + format!("Hash computation failed for {:?}: {}", algorithm, message) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use multi_codec::Codec; + + #[test] + fn test_missing_hash_error() { + let err = Error::MissingHash; + assert_eq!(err.kind(), "MissingHash"); + assert!(err.to_string().contains("Missing hash data")); + } + + #[test] + fn test_unsupported_hash_error() { + let err = Error::unsupported_hash(Codec::Identity); + assert_eq!(err.kind(), "UnsupportedHash"); + let msg = err.to_string(); + // Check error message is present and non-empty + assert!(!msg.is_empty()); + assert!(msg.len() > 10); + // Check context contains codec info + let context = err.context(); + assert!(!context.is_empty()); + } + + #[test] + fn test_invalid_digest_length_error() { + let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16); + assert_eq!(err.kind(), "InvalidDigestLength"); + let msg = err.to_string(); + assert!(msg.contains("32")); + assert!(msg.contains("16")); + } + + #[test] + fn test_hash_compute_failed_error() { + let err = Error::hash_compute_failed(Codec::Sha2256, "test failure"); + assert_eq!(err.kind(), "HashComputeFailed"); + assert!(err.to_string().contains("test failure")); + } + + #[test] + fn test_error_kind_uniqueness() { + let errors = [ + Error::MissingHash, + Error::unsupported_hash(Codec::Identity), + Error::invalid_digest_length(Codec::Sha2256, 32, 16), + Error::hash_compute_failed(Codec::Sha2256, "test"), + ]; + + let kinds: Vec<_> = errors.iter().map(|e| e.kind()).collect(); + assert_eq!(kinds.len(), 4); + + // All kinds should be unique + for (i, k1) in kinds.iter().enumerate() { + for (j, k2) in kinds.iter().enumerate() { + if i != j { + assert_ne!(k1, k2); + } + } + } + } + + #[test] + fn test_error_context_informative() { + let err = Error::unsupported_hash(Codec::Sha2256); + let context = err.context(); + assert!(!context.is_empty()); + assert!(context.contains("Sha2256") || context.contains("sha2-256")); + + let err = Error::invalid_digest_length(Codec::Sha2512, 64, 32); + let context = err.context(); + assert!(context.contains("64")); + assert!(context.contains("32")); + } + + #[test] + fn test_error_is_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + } } diff --git a/src/lib.rs b/src/lib.rs index f0ca6b8..1855683 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,191 @@ -// SPDX-License-Idnetifier: Apache-2.0 -//! multihash +// SPDX-License-Identifier: Apache-2.0 +//! # multi-hash +//! +//! Self-describing cryptographic hash implementation following the +//! [Multihash](https://github.com/multiformats/multihash) specification. +//! +//! ## Overview +//! +//! Multihash is a protocol for differentiating outputs from various well-established +//! cryptographic hash functions, addressing size and encoding considerations. It is +//! useful for applications that may switch between hash functions or need to future-proof +//! their use of hashes. +//! +//! This crate provides: +//! - Support for 23 cryptographic hash algorithms +//! - Type-safe hash digest and algorithm wrappers +//! - Encoding/decoding with multibase support +//! - Serde serialization (optional) +//! - Builder pattern for hash creation +//! +//! ## Supported Algorithms +//! +//! **Secure algorithms** (recommended for cryptographic use): +//! - Blake2b (256, 384, 512 bits) +//! - Blake2s (256 bits) +//! - Blake3 +//! - SHA3 (256, 384, 512 bits) +//! +//! **Legacy algorithms** (for compatibility): +//! - SHA1, SHA2 (224, 256, 384, 512 bits) +//! - MD5, RIPEMD (128, 160, 256, 320 bits) +//! +//! See [`HASH_CODECS`] for the complete list and [`SAFE_HASH_CODECS`] for recommended algorithms. +//! +//! ## Quick Start +//! +//! ### Computing a Hash +//! +//! ```rust +//! use multi_hash::Builder; +//! use multi_codec::Codec; +//! use multi_util::CodecInfo; +//! +//! // Compute a SHA2-256 hash +//! let multihash = Builder::new_from_bytes(Codec::Sha2256, b"hello world") +//! .unwrap() +//! .try_build() +//! .unwrap(); +//! +//! assert_eq!(multihash.codec(), Codec::Sha2256); +//! assert_eq!(multihash.as_ref().len(), 32); // SHA2-256 outputs 32 bytes +//! ``` +//! +//! ### Creating from Existing Hash +//! +//! ```rust +//! use multi_hash::Builder; +//! use multi_codec::Codec; +//! +//! // If you already have a hash digest +//! let digest = vec![0u8; 32]; // SHA2-256 digest +//! let multihash = Builder::new(Codec::Sha2256) +//! .with_hash(digest) +//! .try_build() +//! .unwrap(); +//! ``` +//! +//! ### Encoding and Decoding +//! +//! ```rust +//! use multi_hash::{Builder, Multihash}; +//! use multi_codec::Codec; +//! +//! let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"data") +//! .unwrap() +//! .try_build() +//! .unwrap(); +//! +//! // Encode to bytes +//! let bytes: Vec = mh1.clone().into(); +//! +//! // Decode from bytes +//! let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); +//! assert_eq!(mh1, mh2); +//! ``` +//! +//! ### Base Encoding +//! +//! ```rust +//! use multi_hash::Builder; +//! use multi_codec::Codec; +//! use multi_base::Base; +//! +//! // Create with specific base encoding +//! let encoded = Builder::new_from_bytes(Codec::Sha2256, b"data") +//! .unwrap() +//! .with_base_encoding(Base::Base58Btc) +//! .try_build_encoded() +//! .unwrap(); +//! +//! // Display as base58-encoded string +//! let base58_string = encoded.to_string(); +//! println!("Multihash: {}", base58_string); +//! ``` +//! +//! ## Type Safety +//! +//! Use the newtype wrappers for additional type safety: +//! +//! ```rust +//! use multi_hash::types::{HashDigest, AlgorithmId}; +//! use multi_codec::Codec; +//! +//! // Type-safe hash digest +//! let digest = HashDigest::new(vec![0u8; 32]); +//! assert_eq!(digest.len(), 32); +//! +//! // Type-safe algorithm identifier +//! let algo = AlgorithmId::new(Codec::Sha2256); +//! assert_eq!(algo.name(), "sha2-256"); +//! ``` +//! +//! ## Error Handling +//! +//! ```rust +//! use multi_hash::{Builder, Error}; +//! use multi_codec::Codec; +//! +//! // Handle unsupported algorithms +//! match Builder::new_from_bytes(Codec::Identity, b"data") { +//! Ok(_) => println!("Success"), +//! Err(Error::UnsupportedHash { codec }) => { +//! eprintln!("Algorithm {:?} not supported", codec); +//! } +//! Err(e) => eprintln!("Other error: {}", e), +//! } +//! +//! // Handle missing hash data +//! match Builder::new(Codec::Sha2256).try_build() { +//! Ok(_) => println!("Success"), +//! Err(Error::MissingHash) => { +//! eprintln!("Must call with_hash() before build()"); +//! } +//! Err(e) => eprintln!("Other error: {}", e), +//! } +//! ``` +//! +//! ## Thread Safety +//! +//! All types are `Send + Sync` and safe for concurrent use: +//! +//! ```rust +//! use std::sync::Arc; +//! use std::thread; +//! use multi_hash::Builder; +//! use multi_codec::Codec; +//! +//! let multihash = Arc::new( +//! Builder::new_from_bytes(Codec::Sha2256, b"shared data") +//! .unwrap() +//! .try_build() +//! .unwrap() +//! ); +//! +//! let handle = thread::spawn(move || { +//! println!("Hash: {}", hex::encode(multihash.as_ref())); +//! }); +//! +//! handle.join().unwrap(); +//! ``` +//! +//! ## Performance +//! +//! - Hash computation uses optimized cryptographic libraries +//! - Encoding/decoding is efficient with minimal allocations +//! - Builder pattern enables fluent, zero-cost construction +//! - Benchmarks available: `cargo bench -p multi-hash` +//! +//! ## Features +//! +//! - **`serde`** (default): Enables serde serialization support +//! +//! To disable serde: +//! ```toml +//! [dependencies] +//! multi-hash = { version = "1.0", default-features = false } +//! ``` + #![warn(missing_docs)] #![deny( trivial_casts, @@ -14,17 +200,32 @@ pub use error::Error; /// Multihash type and functions pub mod mh; -pub use mh::{HASH_CODECS, SAFE_HASH_CODECS, Builder, EncodedMultihash, Multihash}; +pub use mh::{Builder, EncodedMultihash, Multihash, HASH_CODECS, SAFE_HASH_CODECS}; + +/// Type-safe wrappers for multihash components +pub mod types; +pub use types::{AlgorithmId, HashDigest}; /// Serde serialization for Multihash #[cfg(feature = "serde")] pub mod serde; -/// ...and in the darkness bind them +/// Commonly used items +/// +/// ``` +/// use multi_hash::prelude::*; +/// +/// let mh = Builder::new_from_bytes(Codec::Sha2256, b"test") +/// .unwrap() +/// .try_build() +/// .unwrap(); +/// // CodecInfo trait is in prelude +/// assert_eq!(mh.codec(), Codec::Sha2256); +/// ``` pub mod prelude { pub use super::*; /// re-exports - pub use multibase::Base; - pub use multicodec::Codec; - pub use multiutil::BaseEncoded; + pub use multi_base::Base; + pub use multi_codec::Codec; + pub use multi_util::{BaseEncoded, CodecInfo}; } diff --git a/src/mh.rs b/src/mh.rs index 163ad6d..c00d636 100644 --- a/src/mh.rs +++ b/src/mh.rs @@ -1,11 +1,16 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 +//! Multihash implementation with support for multiple cryptographic hash algorithms +//! +//! This module provides the core [`Multihash`] type and [`Builder`] for creating +//! self-describing hash digests. + use crate::Error; use core::fmt; -use digest::{Digest, DynDigest}; -use multibase::Base; -use multicodec::Codec; -use multitrait::{Null, TryDecodeFrom}; -use multiutil::{BaseEncoded, CodecInfo, DetectedEncoder, EncodingInfo, Varbytes}; +use digest::{Digest, DynDigest, InvalidBufferSize}; +use multi_base::Base; +use multi_codec::Codec; +use multi_trait::{Null, TryDecodeFrom}; +use multi_util::{BaseEncoded, CodecInfo, DetectedEncoder, EncodingInfo, Varbytes}; use typenum::consts::*; /// the hash codecs currently supported @@ -32,7 +37,8 @@ pub const HASH_CODECS: [Codec; 23] = [ Codec::Sha3224, Codec::Sha3256, Codec::Sha3384, - Codec::Sha3512]; + Codec::Sha3512, +]; /// the safe hash codecs current supported pub const SAFE_HASH_CODECS: [Codec; 8] = [ @@ -43,7 +49,8 @@ pub const SAFE_HASH_CODECS: [Codec; 8] = [ Codec::Blake3, Codec::Sha3256, Codec::Sha3384, - Codec::Sha3512]; + Codec::Sha3512, +]; /// the multicodec sigil for multihash pub const SIGIL: Codec = Codec::Multihash; @@ -51,6 +58,50 @@ pub const SIGIL: Codec = Codec::Multihash; /// a base encoded multihash pub type EncodedMultihash = BaseEncoded; +#[derive(Clone)] +struct Blake3DynDigest(blake3::Hasher); + +impl Blake3DynDigest { + fn new() -> Self { + Self(blake3::Hasher::new()) + } +} + +impl DynDigest for Blake3DynDigest { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } + + fn finalize_into(self, buf: &mut [u8]) -> Result<(), InvalidBufferSize> { + if buf.len() != self.output_size() { + return Err(InvalidBufferSize); + } + buf.copy_from_slice(self.0.finalize().as_bytes()); + Ok(()) + } + + fn finalize_into_reset(&mut self, buf: &mut [u8]) -> Result<(), InvalidBufferSize> { + if buf.len() != self.output_size() { + return Err(InvalidBufferSize); + } + buf.copy_from_slice(self.0.finalize().as_bytes()); + self.reset(); + Ok(()) + } + + fn reset(&mut self) { + self.0 = blake3::Hasher::new(); + } + + fn output_size(&self) -> usize { + blake3::OUT_LEN + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + /// inner implementation of the multihash #[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct Multihash { @@ -89,7 +140,7 @@ impl From for Vec { // add in the hash codec v.append(&mut mh.codec.into()); // add in the hash data - v.append(&mut Varbytes(mh.hash).into()); + v.append(&mut Varbytes::new(mh.hash).into()); v } } @@ -173,7 +224,7 @@ impl Builder { Codec::Blake2B512 => Box::new(blake2::Blake2b::::new()), Codec::Blake2S224 => Box::new(blake2::Blake2s::::new()), Codec::Blake2S256 => Box::new(blake2::Blake2s::::new()), - Codec::Blake3 => Box::new(blake3::Hasher::new()), + Codec::Blake3 => Box::new(Blake3DynDigest::new()), Codec::Md5 => Box::new(md5::Md5::new()), Codec::Ripemd128 => Box::new(ripemd::Ripemd128::new()), Codec::Ripemd160 => Box::new(ripemd::Ripemd160::new()), @@ -190,7 +241,7 @@ impl Builder { Codec::Sha3256 => Box::new(sha3::Sha3_256::new()), Codec::Sha3384 => Box::new(sha3::Sha3_384::new()), Codec::Sha3512 => Box::new(sha3::Sha3_512::new()), - _ => return Err(Error::UnsupportedHash(codec)), + _ => return Err(Error::unsupported_hash(codec)), }; // hash the data @@ -334,7 +385,12 @@ mod tests { .unwrap() .try_build() .unwrap(); - let mh2 = Multihash::try_from(hex::decode("16206b761d3b2e7675e088e337a82207b55711d3957efdb877a3d261b0ca2c38e201").unwrap().as_ref()).unwrap(); + let mh2 = Multihash::try_from( + hex::decode("16206b761d3b2e7675e088e337a82207b55711d3957efdb877a3d261b0ca2c38e201") + .unwrap() + .as_ref(), + ) + .unwrap(); assert_eq!(mh1, mh2); } @@ -351,7 +407,10 @@ mod tests { fn test_multihash_sha1() { // test cases from: https://github.com/multiformats/multihash?tab=readme-ov-file#example let bases = vec![ - (Base::Base16Lower, "f111488c2f11fb2ce392acb5b2986e640211c4690073e"), + ( + Base::Base16Lower, + "f111488c2f11fb2ce392acb5b2986e640211c4690073e", + ), (Base::Base32Upper, "BCEKIRQXRD6ZM4OJKZNNSTBXGIAQRYRUQA47A"), (Base::Base58Btc, "z5dsgvJGnvAfiR3K6HCBc4hcokSfmjj"), (Base::Base64, "mERSIwvEfss45KstbKYbmQCEcRpAHPg"), @@ -372,10 +431,22 @@ mod tests { fn test_multihash_sha2_256() { // test cases from: https://github.com/multiformats/multihash?tab=readme-ov-file#example let bases = vec![ - (Base::Base16Lower, "f12209cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47"), - (Base::Base32Upper, "BCIQJZPAHYP4ZC4SYG2R2UKSYDSRAFEMYVJBAXHMZXQHBGHM7HYWL4RY"), - (Base::Base58Btc, "zQmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk"), - (Base::Base64, "mEiCcvAfD+ZFyWDajqipYHKICkZiqQgudmbwOEx2fPiy+Rw"), + ( + Base::Base16Lower, + "f12209cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47", + ), + ( + Base::Base32Upper, + "BCIQJZPAHYP4ZC4SYG2R2UKSYDSRAFEMYVJBAXHMZXQHBGHM7HYWL4RY", + ), + ( + Base::Base58Btc, + "zQmYtUc4iTCbbfVSDNKvtQqrfyezPPnFvE33wFmutw9PBBk", + ), + ( + Base::Base64, + "mEiCcvAfD+ZFyWDajqipYHKICkZiqQgudmbwOEx2fPiy+Rw", + ), ]; for (b, h) in bases { @@ -388,5 +459,4 @@ mod tests { assert_eq!(h, s.as_str()); } } - } diff --git a/src/serde/de.rs b/src/serde/de.rs index 2658dc4..4d060af 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,8 +1,8 @@ // SPDX-License-Idnetifier: Apache-2.0 use crate::{mh::SIGIL, Multihash}; use core::fmt; -use multicodec::Codec; -use multiutil::EncodedVarbytes; +use multi_codec::Codec; +use multi_util::EncodedVarbytes; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, diff --git a/src/serde/mod.rs b/src/serde/mod.rs index da58c3f..85c2c2d 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -6,7 +6,7 @@ mod ser; #[cfg(test)] mod tests { use crate::prelude::{Base, Builder, Codec, Multihash}; - use multitrait::Null; + use multi_trait::Null; use serde_test::{assert_tokens, Configure, Token}; #[test] @@ -18,14 +18,12 @@ mod tests { assert_tokens( &mh.compact(), // convert to Tagged - &[ - Token::BorrowedBytes(&[0xE0, 0xE4, 0x02, // Codec::Blake2S256 as varuint - 0x20, 0x64, 0x22, 0x03, 0x12, 0x5d, 0x59, 0xe8, - 0xb9, 0x3e, 0xdb, 0x67, 0x6f, 0xc7, 0x8d, 0xe9, - 0xc5, 0x87, 0xcf, 0x52, 0xcc, 0xc6, 0xf2, 0x19, - 0x03, 0x2d, 0xa1, 0xf3, 0x77, 0x08, 0x23, 0x32, 0xb0, - ]), - ], + &[Token::BorrowedBytes(&[ + 0xE0, 0xE4, 0x02, // Codec::Blake2S256 as varuint + 0x20, 0x64, 0x22, 0x03, 0x12, 0x5d, 0x59, 0xe8, 0xb9, 0x3e, 0xdb, 0x67, 0x6f, 0xc7, + 0x8d, 0xe9, 0xc5, 0x87, 0xcf, 0x52, 0xcc, 0xc6, 0xf2, 0x19, 0x03, 0x2d, 0xa1, 0xf3, + 0x77, 0x08, 0x23, 0x32, 0xb0, + ])], ); } @@ -89,7 +87,13 @@ mod tests { .unwrap(); let v = serde_cbor::to_vec(&mh1).unwrap(); //println!("serde_cbor: {}", hex::encode(&v)); - assert_eq!(v, hex::decode("5824e0e40220642203125d59e8b93edb676fc78de9c587cf52ccc6f219032da1f377082332b0").unwrap()); + assert_eq!( + v, + hex::decode( + "5824e0e40220642203125d59e8b93edb676fc78de9c587cf52ccc6f219032da1f377082332b0" + ) + .unwrap() + ); let mh2: Multihash = serde_cbor::from_slice(&v).unwrap(); assert_eq!(mh1, mh2); } @@ -113,7 +117,10 @@ mod tests { assert_tokens( &mh.readable(), &[ - Token::Struct { name: "multihash", len: 2 }, + Token::Struct { + name: "multihash", + len: 2, + }, Token::BorrowedStr("codec"), Token::BorrowedStr("identity"), Token::BorrowedStr("hash"), diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 7588135..cafdaea 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,6 +1,6 @@ // SPDX-License-Idnetifier: Apache-2.0 use crate::mh::{Multihash, SIGIL}; -use multiutil::{EncodingInfo, Varbytes}; +use multi_util::{EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; /// Serialize instance of [`crate::Multihash`] diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..0d9570e --- /dev/null +++ b/src/types.rs @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Type-safe wrappers for multihash components +//! +//! This module provides newtype wrappers that prevent mixing up hash digests +//! with other byte arrays and provide type-safe abstractions. + +use core::fmt; +use multi_codec::Codec; + +/// A cryptographic hash digest +/// +/// This newtype wrapper provides type safety for hash digest bytes, +/// preventing accidental confusion with other byte arrays. +/// +/// # Examples +/// +/// ``` +/// use multi_hash::types::HashDigest; +/// +/// let digest = HashDigest::new(vec![0u8; 32]); +/// assert_eq!(digest.len(), 32); +/// assert_eq!(digest.as_bytes().len(), 32); +/// ``` +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct HashDigest(Vec); + +impl HashDigest { + /// Create a new HashDigest from bytes + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::HashDigest; + /// + /// let digest = HashDigest::new(vec![1, 2, 3, 4]); + /// assert_eq!(digest.len(), 4); + /// ``` + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + /// Get the digest as a byte slice + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::HashDigest; + /// + /// let digest = HashDigest::new(vec![1, 2, 3]); + /// assert_eq!(digest.as_bytes(), &[1, 2, 3]); + /// ``` + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get the length of the digest in bytes + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::HashDigest; + /// + /// let digest = HashDigest::new(vec![0u8; 32]); + /// assert_eq!(digest.len(), 32); + /// ``` + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if the digest is empty + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::HashDigest; + /// + /// let empty = HashDigest::new(vec![]); + /// assert!(empty.is_empty()); + /// + /// let digest = HashDigest::new(vec![1, 2, 3]); + /// assert!(!digest.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Convert into the inner byte vector + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::HashDigest; + /// + /// let digest = HashDigest::new(vec![1, 2, 3]); + /// let bytes: Vec = digest.into_bytes(); + /// assert_eq!(bytes, vec![1, 2, 3]); + /// ``` + pub fn into_bytes(self) -> Vec { + self.0 + } +} + +impl From> for HashDigest { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(digest: HashDigest) -> Vec { + digest.0 + } +} + +impl AsRef<[u8]> for HashDigest { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for HashDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +/// A hash algorithm identifier +/// +/// This newtype wrapper provides type safety for hash algorithm codecs, +/// making it clear when a Codec is being used specifically as a hash algorithm. +/// +/// # Examples +/// +/// ``` +/// use multi_hash::types::AlgorithmId; +/// use multi_codec::Codec; +/// +/// let algo = AlgorithmId::new(Codec::Sha2256); +/// assert_eq!(algo.codec(), Codec::Sha2256); +/// ``` +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct AlgorithmId(Codec); + +impl AlgorithmId { + /// Create a new AlgorithmId + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::AlgorithmId; + /// use multi_codec::Codec; + /// + /// let algo = AlgorithmId::new(Codec::Sha2256); + /// assert_eq!(algo.codec(), Codec::Sha2256); + /// ``` + pub const fn new(codec: Codec) -> Self { + Self(codec) + } + + /// Get the underlying codec + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::AlgorithmId; + /// use multi_codec::Codec; + /// + /// let algo = AlgorithmId::new(Codec::Sha2512); + /// assert_eq!(algo.codec(), Codec::Sha2512); + /// ``` + pub const fn codec(self) -> Codec { + self.0 + } + + /// Get the codec code + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::AlgorithmId; + /// use multi_codec::Codec; + /// + /// let algo = AlgorithmId::new(Codec::Sha2256); + /// assert_eq!(algo.code(), 0x12); + /// ``` + pub fn code(self) -> u64 { + self.0.code() + } + + /// Get the algorithm name + /// + /// # Examples + /// + /// ``` + /// use multi_hash::types::AlgorithmId; + /// use multi_codec::Codec; + /// + /// let algo = AlgorithmId::new(Codec::Sha2256); + /// assert_eq!(algo.name(), "sha2-256"); + /// ``` + pub fn name(self) -> &'static str { + self.0.into() + } +} + +impl From for AlgorithmId { + fn from(codec: Codec) -> Self { + Self(codec) + } +} + +impl From for Codec { + fn from(algo: AlgorithmId) -> Codec { + algo.0 + } +} + +impl fmt::Display for AlgorithmId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_digest_new() { + let digest = HashDigest::new(vec![1, 2, 3]); + assert_eq!(digest.as_bytes(), &[1, 2, 3]); + } + + #[test] + fn test_hash_digest_len() { + let digest = HashDigest::new(vec![0u8; 32]); + assert_eq!(digest.len(), 32); + } + + #[test] + fn test_hash_digest_is_empty() { + let empty = HashDigest::new(vec![]); + assert!(empty.is_empty()); + + let digest = HashDigest::new(vec![1]); + assert!(!digest.is_empty()); + } + + #[test] + fn test_hash_digest_conversions() { + let bytes = vec![1, 2, 3, 4]; + let digest = HashDigest::from(bytes.clone()); + let back: Vec = digest.into_bytes(); + assert_eq!(back, bytes); + } + + #[test] + fn test_hash_digest_as_ref() { + let digest = HashDigest::new(vec![1, 2, 3]); + let slice: &[u8] = digest.as_ref(); + assert_eq!(slice, &[1, 2, 3]); + } + + #[test] + fn test_hash_digest_display() { + let digest = HashDigest::new(vec![0xDE, 0xAD, 0xBE, 0xEF]); + assert_eq!(digest.to_string(), "deadbeef"); + } + + #[test] + fn test_hash_digest_equality() { + let d1 = HashDigest::new(vec![1, 2, 3]); + let d2 = HashDigest::new(vec![1, 2, 3]); + let d3 = HashDigest::new(vec![4, 5, 6]); + + assert_eq!(d1, d2); + assert_ne!(d1, d3); + } + + #[test] + fn test_hash_digest_ordering() { + let d1 = HashDigest::new(vec![1, 2, 3]); + let d2 = HashDigest::new(vec![1, 2, 4]); + + assert!(d1 < d2); + } + + #[test] + fn test_hash_digest_hash_trait() { + use std::collections::HashMap; + + let mut map = HashMap::new(); + let digest = HashDigest::new(vec![1, 2, 3]); + map.insert(digest.clone(), "test"); + + assert_eq!(map.get(&digest), Some(&"test")); + } + + #[test] + fn test_algorithm_id_new() { + let algo = AlgorithmId::new(Codec::Sha2256); + assert_eq!(algo.codec(), Codec::Sha2256); + } + + #[test] + fn test_algorithm_id_code() { + let algo = AlgorithmId::new(Codec::Sha2256); + assert_eq!(algo.code(), 0x12); + } + + #[test] + fn test_algorithm_id_name() { + let algo = AlgorithmId::new(Codec::Sha2256); + assert_eq!(algo.name(), "sha2-256"); + } + + #[test] + fn test_algorithm_id_conversions() { + let codec = Codec::Sha2512; + let algo = AlgorithmId::from(codec); + let back: Codec = algo.into(); + assert_eq!(back, codec); + } + + #[test] + fn test_algorithm_id_display() { + let algo = AlgorithmId::new(Codec::Sha3256); + assert_eq!(algo.to_string(), "sha3-256"); + } + + #[test] + fn test_algorithm_id_copy() { + let algo1 = AlgorithmId::new(Codec::Blake3); + let algo2 = algo1; + assert_eq!(algo1, algo2); + } + + #[test] + fn test_newtypes_are_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); + assert_sync::(); + } +} diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs new file mode 100644 index 0000000..c2b16e7 --- /dev/null +++ b/tests/edge_case_tests.rs @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Edge case tests for multi-hash + +use multi_codec::Codec; +use multi_hash::{Builder, Error, Multihash, HASH_CODECS, SAFE_HASH_CODECS}; +use multi_trait::{Null, TryDecodeFrom}; +use multi_util::CodecInfo; + +/// Test all supported hash algorithms with empty input +#[test] +fn test_all_algorithms_empty_input() { + for &codec in HASH_CODECS.iter() { + let result = Builder::new_from_bytes(codec, []); + assert!(result.is_ok(), "Failed for {:?}", codec); + + let mh = result.unwrap().try_build().unwrap(); + assert_eq!(mh.codec(), codec); + } +} + +/// Test all supported hash algorithms with single byte +#[test] +fn test_all_algorithms_single_byte() { + for &codec in HASH_CODECS.iter() { + let mh = Builder::new_from_bytes(codec, [0x42]) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(mh.codec(), codec); + assert!(!mh.as_ref().is_empty()); + } +} + +/// Test null/default multihash +#[test] +fn test_null_multihash() { + let null_mh = Multihash::null(); + assert!(null_mh.is_null()); + assert_eq!(null_mh, Multihash::default()); + + // Null should have Identity codec and empty hash + assert_eq!(null_mh.codec(), Codec::Identity); + assert_eq!(null_mh.as_ref(), &[] as &[u8]); +} + +/// Test builder without hash data fails +#[test] +fn test_builder_missing_hash() { + let result = Builder::new(Codec::Sha2256).try_build(); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::MissingHash)); +} + +/// Test unsupported hash algorithm +#[test] +fn test_unsupported_algorithm() { + let result = Builder::new_from_bytes(Codec::Identity, b"data"); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::UnsupportedHash { .. })); +} + +/// Test multihash with maximum size data +#[test] +fn test_large_data() { + let large_data = vec![0u8; 1024 * 1024]; // 1MB + let mh = Builder::new_from_bytes(Codec::Sha2256, &large_data) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(mh.codec(), Codec::Sha2256); + assert_eq!(mh.as_ref().len(), 32); // SHA2-256 always outputs 32 bytes +} + +/// Test binary encoding/decoding roundtrip +#[test] +fn test_binary_roundtrip_all_algorithms() { + let data = b"test data"; + + for &codec in HASH_CODECS.iter() { + let mh1 = Builder::new_from_bytes(codec, data) + .unwrap() + .try_build() + .unwrap(); + + let bytes: Vec = mh1.clone().into(); + let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); + + assert_eq!(mh1, mh2); + assert_eq!(mh1.codec(), mh2.codec()); + } +} + +/// Test SAFE_HASH_CODECS are subset of HASH_CODECS +#[test] +fn test_safe_codecs_subset() { + for &safe_codec in SAFE_HASH_CODECS.iter() { + assert!( + HASH_CODECS.contains(&safe_codec), + "{:?} not in HASH_CODECS", + safe_codec + ); + } +} + +/// Test multihash equality +#[test] +fn test_multihash_equality() { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"data") + .unwrap() + .try_build() + .unwrap(); + let mh2 = mh1.clone(); + + assert_eq!(mh1, mh2); + assert_eq!(mh1, mh1); +} + +/// Test multihash ordering +#[test] +fn test_multihash_ordering() { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"aaa") + .unwrap() + .try_build() + .unwrap(); + let mh2 = Builder::new_from_bytes(Codec::Sha2256, b"bbb") + .unwrap() + .try_build() + .unwrap(); + + // Different data produces different hashes, so ordering is meaningful + assert_ne!(mh1, mh2); +} + +/// Test AsRef implementation +#[test] +fn test_as_ref() { + let mh = Builder::new_from_bytes(Codec::Sha2256, b"test") + .unwrap() + .try_build() + .unwrap(); + + let bytes: &[u8] = mh.as_ref(); + assert_eq!(bytes.len(), 32); // SHA2-256 output size +} + +/// Test Debug formatting +#[test] +fn test_debug_format() { + let mh = Builder::new_from_bytes(Codec::Sha2256, b"test") + .unwrap() + .try_build() + .unwrap(); + + let debug_str = format!("{:?}", mh); + assert!(!debug_str.is_empty()); + assert!(debug_str.len() > 10); +} + +/// Test builder with manual hash setting +#[test] +fn test_builder_with_hash() { + let hash_bytes = vec![0u8; 32]; + let mh = Builder::new(Codec::Sha2256) + .with_hash(hash_bytes.clone()) + .try_build() + .unwrap(); + + assert_eq!(mh.codec(), Codec::Sha2256); + assert_eq!(mh.as_ref(), hash_bytes.as_slice()); +} + +/// Test that Clone works correctly +#[test] +fn test_clone() { + let mh1 = Builder::new_from_bytes(Codec::Sha3256, b"clone test") + .unwrap() + .try_build() + .unwrap(); + let mh2 = mh1.clone(); + + assert_eq!(mh1, mh2); + assert_eq!(mh1.codec(), mh2.codec()); + assert_eq!(mh1.as_ref(), mh2.as_ref()); +} + +/// Test TryDecodeFrom with trailing data +#[test] +fn test_decode_with_trailing_data() { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"test") + .unwrap() + .try_build() + .unwrap(); + + let mut bytes: Vec = mh1.clone().into(); + bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC]); + + let (mh2, remaining) = Multihash::try_decode_from(&bytes).unwrap(); + assert_eq!(mh1, mh2); + assert_eq!(remaining, &[0xAA, 0xBB, 0xCC]); +} + +/// Test that truncated data fails gracefully +#[test] +fn test_truncated_data() { + let mh = Builder::new_from_bytes(Codec::Sha2256, b"test") + .unwrap() + .try_build() + .unwrap(); + + let bytes: Vec = mh.into(); + + // Try decoding with just first byte (codec only, no length/hash) + if !bytes.is_empty() { + let truncated = &bytes[..1]; + let result = Multihash::try_from(truncated); + // Should fail due to insufficient data + assert!(result.is_err()); + } +} + +/// Test Send and Sync bounds +#[test] +fn test_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); + assert_sync::(); +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..899a0ff --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Integration tests for multi-hash with other workspace crates + +use multi_base::Base; +use multi_codec::Codec; +use multi_hash::{Builder, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::{CodecInfo, EncodingInfo}; + +/// Test integration with multi-codec +#[test] +fn test_multicodec_integration() { + // Verify all hash codecs are valid Codec values + use multi_hash::HASH_CODECS; + + for &codec in HASH_CODECS.iter() { + // Should be able to get codec properties + let code = codec.code(); + let name = codec.as_str(); + + assert!(code > 0); + assert!(!name.is_empty()); + } +} + +/// Test integration with multi-base through EncodedMultihash +#[test] +fn test_multibase_integration() { + // Test with various base encodings + let bases = vec![ + Base::Base16Lower, + Base::Base32Lower, + Base::Base58Btc, + Base::Base64, + ]; + + for base in bases { + let encoded = Builder::new_from_bytes(Codec::Sha2256, b"test data") + .unwrap() + .with_base_encoding(base) + .try_build_encoded() + .unwrap(); + + // Should be able to convert to string and back + let s = encoded.to_string(); + assert!(!s.is_empty()); + + // Encoding should preserve the base + assert_eq!(encoded.encoding(), base); + } +} + +/// Test integration with multi-trait +#[test] +fn test_multitrait_integration() { + let mh1 = Builder::new_from_bytes(Codec::Sha3256, b"multitrait test") + .unwrap() + .try_build() + .unwrap(); + + // Convert to bytes using Into (from multitrait patterns) + let bytes: Vec = mh1.clone().into(); + + // Use TryDecodeFrom from multitrait + let (mh2, remaining) = Multihash::try_decode_from(&bytes).unwrap(); + + assert_eq!(mh1, mh2); + assert!(remaining.is_empty()); +} + +/// Test integration with multi-util BaseEncoded +#[test] +fn test_multiutil_integration() { + let mh = Builder::new_from_bytes(Codec::Blake3, b"multiutil test") + .unwrap() + .try_build() + .unwrap(); + + // CodecInfo trait from multiutil + assert_eq!(mh.codec(), Codec::Blake3); + assert_eq!(Multihash::preferred_codec(), Codec::Multihash); + + // EncodingInfo trait from multiutil + assert_eq!(mh.encoding(), Base::Base16Lower); + assert_eq!(Multihash::preferred_encoding(), Base::Base16Lower); +} + +/// Test EncodedMultihash uses BaseEncoded from multiutil +#[test] +fn test_encoded_multihash_type() { + use multi_hash::EncodedMultihash; + + let encoded = Builder::new_from_bytes(Codec::Sha2512, b"encoded test") + .unwrap() + .with_base_encoding(Base::Base64) + .try_build_encoded() + .unwrap(); + + // EncodedMultihash is a type alias to BaseEncoded + let _: &EncodedMultihash = &encoded; + + // Should have encoding info + assert_eq!(encoded.encoding(), Base::Base64); +} + +/// Test multihash in data structures +#[test] +fn test_in_collections() { + use std::collections::BTreeMap; + + let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"key1") + .unwrap() + .try_build() + .unwrap(); + let mh2 = Builder::new_from_bytes(Codec::Sha2256, b"key2") + .unwrap() + .try_build() + .unwrap(); + + // BTreeMap (requires Ord) + let mut btree = BTreeMap::new(); + btree.insert(mh1.clone(), "value1"); + btree.insert(mh2.clone(), "value2"); + assert_eq!(btree.len(), 2); + + // Vec + let vec = [mh1.clone(), mh2.clone()]; + assert_eq!(vec.len(), 2); +} + +/// Test builder pattern fluency +#[test] +fn test_builder_fluent_api() { + let result = Builder::new(Codec::Sha3384) + .with_hash(vec![0u8; 48]) + .with_base_encoding(Base::Base58Btc) + .try_build_encoded(); + + assert!(result.is_ok()); +} + +/// Test all workspace crate types work together +#[test] +fn test_full_workspace_integration() { + // Use types from all workspace crates together + let codec = Codec::Sha2256; // multi-codec + let base = Base::Base32Lower; // multi-base + + // Create multihash (multi-hash) + let mh = Builder::new_from_bytes(codec, b"integration") + .unwrap() + .try_build() + .unwrap(); + + // Verify traits from multi-util work + assert_eq!(mh.codec(), codec); + assert_eq!(mh.encoding(), Base::Base16Lower); + + // Verify we can create encoded version (uses BaseEncoded from multiutil) + let encoded = Builder::new_from_bytes(codec, b"integration") + .unwrap() + .with_base_encoding(base) + .try_build_encoded() + .unwrap(); + + assert_eq!(encoded.encoding(), base); + + // Verify encoding works + let s = encoded.to_string(); + assert!(!s.is_empty()); +} + +#[cfg(feature = "serde")] +mod serde_integration { + use super::*; + use serde::{Deserialize, Serialize}; + + /// Test multihash in serde structs + #[test] + fn test_multihash_in_struct() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Document { + hash: Multihash, + timestamp: u64, + } + + let doc = Document { + hash: Builder::new_from_bytes(Codec::Sha2256, b"document") + .unwrap() + .try_build() + .unwrap(), + timestamp: 1234567890, + }; + + // JSON roundtrip + let json = serde_json::to_string(&doc).unwrap(); + let decoded: Document = serde_json::from_str(&json).unwrap(); + assert_eq!(doc, decoded); + + // CBOR roundtrip + let cbor = serde_cbor::to_vec(&doc).unwrap(); + let decoded: Document = serde_cbor::from_slice(&cbor).unwrap(); + assert_eq!(doc, decoded); + } +} diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs new file mode 100644 index 0000000..a6b80b7 --- /dev/null +++ b/tests/proptest_tests.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Property-based tests for multi-hash using proptest + +use multi_codec::Codec; +use multi_hash::{Builder, Multihash, HASH_CODECS}; +use multi_trait::TryDecodeFrom; +use multi_util::CodecInfo; +use proptest::prelude::*; + +/// Property: Multihash encoding and decoding should roundtrip +#[test] +fn test_multihash_roundtrip() { + proptest!(|(data in prop::collection::vec(any::(), 0..1024))| { + for &codec in HASH_CODECS.iter() { + let mh1 = Builder::new_from_bytes(codec, &data).unwrap().try_build().unwrap(); + let bytes: Vec = mh1.clone().into(); + let (mh2, remaining) = Multihash::try_decode_from(&bytes).unwrap(); + + prop_assert_eq!(mh1, mh2); + prop_assert!(remaining.is_empty()); + } + }); +} + +/// Property: Hash output should be deterministic +#[test] +fn test_hash_deterministic() { + proptest!(|(data in prop::collection::vec(any::(), 0..256))| { + for &codec in HASH_CODECS.iter().take(5) { + let mh1 = Builder::new_from_bytes(codec, &data).unwrap().try_build().unwrap(); + let mh2 = Builder::new_from_bytes(codec, &data).unwrap().try_build().unwrap(); + + prop_assert_eq!(&mh1, &mh2); + + let bytes1: Vec = mh1.into(); + let bytes2: Vec = mh2.into(); + prop_assert_eq!(&bytes1, &bytes2); + } + }); +} + +/// Property: Different data should produce different hashes +#[test] +fn test_different_data_different_hash() { + proptest!(|(data1 in prop::collection::vec(any::(), 1..256), + data2 in prop::collection::vec(any::(), 1..256))| { + if data1 != data2 { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, &data1).unwrap().try_build().unwrap(); + let mh2 = Builder::new_from_bytes(Codec::Sha2256, &data2).unwrap().try_build().unwrap(); + + prop_assert_ne!(mh1, mh2); + } + }); +} + +/// Property: Codec is preserved through encoding/decoding +#[test] +fn test_codec_preserved() { + proptest!(|(data in prop::collection::vec(any::(), 0..256))| { + for &codec in HASH_CODECS.iter() { + let mh1 = Builder::new_from_bytes(codec, &data).unwrap().try_build().unwrap(); + let bytes: Vec = mh1.clone().into(); + let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); + + prop_assert_eq!(mh1.codec(), codec); + prop_assert_eq!(mh2.codec(), codec); + prop_assert_eq!(mh1.codec(), mh2.codec()); + } + }); +} + +/// Property: Empty data should produce valid hash +#[test] +fn test_empty_data_valid() { + proptest!(|(_unit in 0..1u8)| { + for &codec in HASH_CODECS.iter() { + let result = Builder::new_from_bytes(codec, []); + prop_assert!(result.is_ok()); + } + }); +} + +/// Property: Multihash equality is reflexive +#[test] +fn test_equality_reflexive() { + proptest!(|(data in prop::collection::vec(any::(), 0..256))| { + let mh = Builder::new_from_bytes(Codec::Sha2256, &data).unwrap().try_build().unwrap(); + prop_assert_eq!(&mh, &mh); + }); +} + +/// Property: Multihash equality is symmetric +#[test] +fn test_equality_symmetric() { + proptest!(|(data in prop::collection::vec(any::(), 0..256))| { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, &data).unwrap().try_build().unwrap(); + let mh2 = mh1.clone(); + + prop_assert_eq!(&mh1, &mh2); + prop_assert_eq!(&mh2, &mh1); + }); +} + +/// Property: Multihash ordering is consistent +#[test] +fn test_ordering_consistent() { + proptest!(|(data1 in prop::collection::vec(any::(), 0..128), + data2 in prop::collection::vec(any::(), 0..128))| { + let mh1 = Builder::new_from_bytes(Codec::Sha2256, &data1).unwrap().try_build().unwrap(); + let mh2 = Builder::new_from_bytes(Codec::Sha2256, &data2).unwrap().try_build().unwrap(); + + if mh1 < mh2 { + prop_assert!((mh2 >= mh1)); + } + }); +} diff --git a/tests/security_tests.rs b/tests/security_tests.rs new file mode 100644 index 0000000..2ba329a --- /dev/null +++ b/tests/security_tests.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Security-focused tests for multi-hash + +use multi_codec::Codec; +use multi_hash::{Builder, Error, Multihash, SAFE_HASH_CODECS}; +use multi_util::CodecInfo; + +/// Test that invalid codec is rejected +#[test] +fn test_invalid_codec_rejected() { + let result = Builder::new_from_bytes(Codec::DagCbor, b"data"); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::UnsupportedHash { .. })); +} + +/// Test that empty hash data is detected +#[test] +fn test_missing_hash_detected() { + let result = Builder::new(Codec::Sha2256).try_build(); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::MissingHash)); +} + +/// Test malformed multihash data is rejected +#[test] +fn test_malformed_data_rejected() { + // Invalid varint encoding + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + let result = Multihash::try_from(invalid.as_ref()); + assert!(result.is_err()); +} + +/// Test truncated multihash is rejected +#[test] +fn test_truncated_multihash() { + // Just a codec byte, no length or hash data + let truncated = vec![0x12]; // SHA2-256 codec + let result = Multihash::try_from(truncated.as_ref()); + assert!(result.is_err()); +} + +/// Test empty byte array is rejected +#[test] +fn test_empty_bytes_rejected() { + let result = Multihash::try_from(&[] as &[u8]); + assert!(result.is_err()); +} + +/// Test concurrent hash computation +#[test] +fn test_concurrent_hashing() { + use std::sync::Arc; + use std::thread; + + let data = Arc::new(b"concurrent test data".to_vec()); + let mut handles = vec![]; + + for _ in 0..4 { + let data_clone = Arc::clone(&data); + let handle = thread::spawn(move || { + for _ in 0..10 { + let mh = Builder::new_from_bytes(Codec::Sha2256, data_clone.as_ref()) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(mh.codec(), Codec::Sha2256); + } + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } +} + +/// Test that same input produces same hash across threads +#[test] +fn test_deterministic_across_threads() { + use std::thread; + + let data = b"deterministic test"; + + let mh1 = Builder::new_from_bytes(Codec::Sha2256, data) + .unwrap() + .try_build() + .unwrap(); + + let handle = thread::spawn(move || { + Builder::new_from_bytes(Codec::Sha2256, data) + .unwrap() + .try_build() + .unwrap() + }); + + let mh2 = handle.join().unwrap(); + assert_eq!(mh1, mh2); +} + +/// Test null multihash is safe +#[test] +fn test_null_safety() { + use multi_trait::Null; + + let null_mh = Multihash::null(); + assert!(null_mh.is_null()); + + // Encoding null should not panic + let bytes: Vec = null_mh.clone().into(); + let decoded = Multihash::try_from(bytes.as_ref()).unwrap(); + assert!(decoded.is_null()); +} + +/// Test error types are Send + Sync +#[test] +fn test_error_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); +} + +/// Test all safe hash codecs work +#[test] +fn test_safe_hash_codecs_functional() { + let data = b"safety test data"; + + for &codec in SAFE_HASH_CODECS.iter() { + let mh = Builder::new_from_bytes(codec, data) + .unwrap() + .try_build() + .unwrap(); + + assert_eq!(mh.codec(), codec); + + // Should roundtrip + let bytes: Vec = mh.clone().into(); + let decoded = Multihash::try_from(bytes.as_ref()).unwrap(); + assert_eq!(mh, decoded); + } +}