From 976654b33dbf8fb68d2c435ce0bc2c22fb28dcfa Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Wed, 15 Jul 2026 15:34:42 -0600 Subject: [PATCH] hardening Signed-off-by: Dave Grantham --- .github/workflows/rust.yml | 55 +++++- CHANGELOG.md | 35 +++- Cargo.toml | 16 +- README.md | 374 +++++++++++++++++++++++++++++++++---- benches/multihash_bench.rs | 24 +-- src/error.rs | 32 ++-- src/lib.rs | 3 +- src/mh.rs | 46 +++-- src/serde/de.rs | 4 +- src/serde/mod.rs | 2 +- src/types.rs | 19 +- tests/edge_case_tests.rs | 23 ++- tests/integration_tests.rs | 11 +- tests/proptest_tests.rs | 8 +- tests/security_tests.rs | 4 +- 15 files changed, 540 insertions(+), 116 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..d20f7a0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,21 +2,62 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always jobs: build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Check Code Format + run: cargo fmt --all -- --check + - name: Code Lint + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Build + run: cargo build --verbose --all-features + + - name: Run tests + run: cargo test --verbose --all-features + + msrv: + name: Verify MSRV (1.85) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + + - name: Install Rust Toolchain (1.85) + uses: dtolnay/rust-toolchain@1.85.0 + + - name: Check + run: cargo check --all-features + + audit: + name: Security Audit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-audit + run: cargo install cargo-audit + + - name: Run cargo audit + run: cargo audit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a204b1c..a3d73d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,42 @@ 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.4] - 2026-07-15 + +### Added + +- **`#![deny(unsafe_code)]`** at the crate root. +- **`#[must_use]`** on `Builder::with_hash()` and `Builder::with_base_encoding()` + (methods returning `Self`). +- **`#[must_use]`** on `Error::unsupported_hash()`, + `Error::invalid_digest_length()`, `Error::hash_compute_failed()`, and + `Error::kind()`. +- **`# Errors`** doc sections on `Builder::new_from_bytes()`, + `Builder::try_build()`, and `Builder::try_build_encoded()`. +- **MSRV declared**: `rust-version = "1.85"` in `Cargo.toml`. CI verifies the + MSRV with a dedicated job. +- **`cargo audit`** job in CI. +- **`cargo fmt --check`** and **`clippy -D warnings`** steps in CI. +- **Clippy lint configuration**: `[lints.clippy]` with `pedantic`, `nursery`, + and `cargo` groups (all `warn`), plus `[lints.rust] unsafe_code = "deny"`. + +### Changed + +- **Edition 2024**: Updated from Rust 2021. +- **`From for Vec`**: Pre-calculates total size and uses a single + `with_capacity` + `extend_from_slice` instead of two `append` calls that each + allocate intermediate `Vec` buffers. +- **Clippy pedantic/nursery/cargo warnings** resolved across all source, tests, + and benchmarks. + ## [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 +- Added test suite (edge cases, integration, proptest, security) +- Initial published release on crates.io as `multi-hash` + +[1.0.4]: https://github.com/cryptidtech/multi-hash/compare/v1.0.0...v1.0.4 +[1.0.0]: https://github.com/cryptidtech/multi-hash/releases/tag/v1.0.0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f3337f6..aac79c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "multi-hash" -version = "1.0.3" -edition = "2021" +version = "1.0.4" +edition = "2024" +rust-version = "1.85" authors = ["Dave Grantham "] description = "Multihash self-describing cryptographic hash data" repository = "https://github.com/cryptidtech/multi-hash.git" @@ -40,6 +41,17 @@ serde_cbor = "0.11" serde_json = "1.0" serde_test = "1.0" +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +# blake3 pulls in digest 0.11 while the other RustCrypto crates use 0.10. +# This will resolve when the rest of the ecosystem upgrades. +multiple_crate_versions = { level = "allow", priority = 1 } + +[lints.rust] +unsafe_code = "deny" + [[bench]] name = "multihash_bench" harness = false diff --git a/README.md b/README.md index 06bcc72..0f8a184 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,364 @@ -[![](https://img.shields.io/badge/made%20by-Cryptid%20Technologies-gold.svg?style=flat-square)][CRYPTID] -[![](https://img.shields.io/badge/project-provenance-purple.svg?style=flat-square)][PROVENANCE] -[![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)][MULTIFORMATS] -![](https://github.com/cryptidtech/multihash/actions/workflows/rust.yml/badge.svg) +[![](https://img.shields.io/badge/made%20by-Cryptid%20Technologies-gold.svg?style=flat-square)](https://cryptid.tech/) +[![](https://img.shields.io/badge/project-provenance-purple.svg?style=flat-square)](https://github.com/cryptidtech/provenance-specifications/) +[![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)](https://github.com/multiformats/multiformats/) -# Multihash +[![Build Status](https://github.com/cryptidtech/multi-hash/actions/workflows/rust.yml/badge.svg)](https://github.com/cryptidtech/multi-hash/actions) +[![License](https://img.shields.io/crates/l/multi-hash?style=flat-square)](LICENSE) +[![Crates.io](https://img.shields.io/crates/v/multi-hash?style=flat-square)](https://crates.io/crates/multi-hash) +[![Documentation](https://docs.rs/multi-hash/badge.svg?style=flat-square)](https://docs.rs/multi-hash) -Multiformats [Multihash][MULTIHASH] implementation without constant size in the -type signature. +# multi-hash + +Rust implementation of the [Multihash](https://github.com/multiformats/multihash) +specification for self-describing cryptographic hash digests. + +Multihash is a self-describing format that pairs a hash algorithm identifier +(multicodec tag) with the raw digest bytes, enabling systems to switch hash +algorithms without breaking compatibility. This crate provides 23 supported hash +algorithms, type-safe wrappers, serde integration, and multibase encoding via +the `multi-util` crate stack. + +## Table of Contents + +- [Features](#features) +- [Install](#install) +- [Supported Algorithms](#supported-algorithms) +- [Usage](#usage) + - [Computing a Hash](#computing-a-hash) + - [Building from an Existing Digest](#building-from-an-existing-digest) + - [Encoding and Decoding](#encoding-and-decoding) + - [Base Encoding](#base-encoding) + - [Converting to EncodedMultihash](#converting-to-encodedmultihash) + - [Serde Integration](#serde-integration) + - [Error Handling](#error-handling) + - [Type-Safe Newtypes](#type-safe-newtypes) +- [Testing](#testing) +- [Feature Flags](#feature-flags) +- [Security](#security) +- [Maintainers](#maintainers) +- [Contribute](#contribute) +- [License](#license) ## Features -* Uses the [`multiutil::BaseEncoded`] smart pointers to wrap the Multihash into - EncodecMultihash for base encoded multihashes; automating the - encoding/decoding to/from strings and byte slices. -* Serde support to/from human readable and binary formats. -* Supports raw binary encoding and decoding using [`Into>`] and - [`TryFrom<&[u8]>`] trait implementations. +- **23 Hash Algorithms**: SHA1, SHA2 family, SHA3 family, Blake2, Blake3, + MD5, RIPEMD +- **Builder Pattern**: Fluent API for creating multihashes from raw data or + existing digests +- **Multibase Encoding**: `EncodedMultihash` smart pointer for base-encoded + string representation via `multi-util`'s `BaseEncoded` +- **Serde Support**: JSON (human-readable → codec name string) and binary + (varint bytes) serialization (feature-gated) +- **Binary Round-Trip**: `Into>` and `TryFrom<&[u8]>` for raw wire format +- **Type-Safe Newtypes**: `HashDigest` and `AlgorithmId` wrappers +- **Zero Unsafe Code**: `#![deny(unsafe_code)]` enforced at compile time +- **Thread-Safe**: All types are `Send + Sync` + +## Install + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +multi-hash = "1.0" +``` + +To disable serde support: + +```toml +[dependencies] +multi-hash = { version = "1.0", default-features = false } +``` + +**MSRV**: Rust 1.85 (Edition 2024) + +## Supported Algorithms + +### Secure algorithms (recommended for cryptographic use) + +| Algorithm | Codec | Digest Size | +|-----------|-------|-------------| +| Blake2b-256 | `Blake2B256` | 32 bytes | +| Blake2b-384 | `Blake2B384` | 48 bytes | +| Blake2b-512 | `Blake2B512` | 64 bytes | +| Blake2s-256 | `Blake2S256` | 32 bytes | +| Blake3 | `Blake3` | 32 bytes | +| SHA3-256 | `Sha3256` | 32 bytes | +| SHA3-384 | `Sha3384` | 48 bytes | +| SHA3-512 | `Sha3512` | 64 bytes | + +See [`SAFE_HASH_CODECS`](https://docs.rs/multi-hash/latest/multi_hash/constant.SAFE_HASH_CODECS.html) +for the constant array. + +### Legacy algorithms (for compatibility) + +| Algorithm | Codec | Digest Size | +|-----------|-------|-------------| +| SHA1 | `Sha1` | 20 bytes | +| SHA2-224 | `Sha2224` | 28 bytes | +| SHA2-256 | `Sha2256` | 32 bytes | +| SHA2-384 | `Sha2384` | 48 bytes | +| SHA2-512 | `Sha2512` | 64 bytes | +| SHA2-512/224 | `Sha2512224` | 28 bytes | +| SHA2-512/256 | `Sha2512256` | 32 bytes | +| Blake2b-224 | `Blake2B224` | 28 bytes | +| Blake2s-224 | `Blake2S224` | 28 bytes | +| MD5 | `Md5` | 16 bytes | +| RIPEMD-128 | `Ripemd128` | 16 bytes | +| RIPEMD-160 | `Ripemd160` | 20 bytes | +| RIPEMD-256 | `Ripemd256` | 32 bytes | +| RIPEMD-320 | `Ripemd320` | 40 bytes | +| SHA3-224 | `Sha3224` | 28 bytes | + +See [`HASH_CODECS`](https://docs.rs/multi-hash/latest/multi_hash/constant.HASH_CODECS.html) +for the constant array of all 23 supported codecs. + +## Usage + +### Computing a Hash + +```rust +use multi_hash::Builder; +use multi_codec::Codec; + +// 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 +``` -## Examples +### Building from an Existing Digest -Bulding a multihash from some bytes: +If you already have a hash digest (e.g. from an external hashing library): ```rust -let mh = Builder::new_from_bytes(Codec::Sha3384, b"for great justice, move every zig!")? - .try_build()?; +use multi_hash::Builder; +use multi_codec::Codec; + +let digest = vec![0u8; 32]; // pre-computed SHA2-256 digest +let multihash = Builder::new(Codec::Sha2256) + .with_hash(digest) + .try_build() + .unwrap(); +``` + +### Encoding and Decoding + +Multihashes encode as `codec || length || hash` (varint-prefixed): + +```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 binary (varint wire format) +let bytes: Vec = mh1.clone().into(); + +// Decode from binary +let mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); +assert_eq!(mh1, mh2); ``` -Building a base encoded multihash from some bytes: +### Base Encoding + +Use `try_build_encoded()` with a specific base to get an `EncodedMultihash` that +supports `Display` and `TryFrom<&str>`: ```rust -let encoded_mh = Builder::new_from_bytes(Codec::Sha3256, b"for great justice, move every zig!")? +use multi_hash::Builder; +use multi_codec::Codec; +use multi_base::Base; + +let encoded = Builder::new_from_bytes(Codec::Sha2256, b"data") + .unwrap() .with_base_encoding(Base::Base58Btc) - .try_build_encoded()?; + .try_build_encoded() + .unwrap(); + +// Display as a base58-encoded multihash string +let base58_string = encoded.to_string(); +println!("Multihash: {}", base58_string); + +// Parse back from string +use multi_hash::EncodedMultihash; +let decoded: EncodedMultihash = EncodedMultihash::try_from(base58_string.as_str()).unwrap(); +assert_eq!(encoded, decoded); ``` -Existing multihash objects can be converted to [`crate::mh::EncodedMultihash`] -objects by using `.into()`: +### Converting to EncodedMultihash + +Existing `Multihash` objects can be converted to `EncodedMultihash` using `.into()` +(defaults to `Base16Lower`) or `EncodedMultihash::new()` with a chosen base: ```rust -let mh = Builder::new_from_bytes(Codec::Sha3384, b"for great justice, move every zig!")? - .try_build()?; +use multi_hash::{Builder, EncodedMultihash}; +use multi_base::Base; +use multi_codec::Codec; + +let mh = Builder::new_from_bytes(Codec::Sha3384, b"for great justice, move every zig!") + .unwrap() + .try_build() + .unwrap(); -// this will use the preferred encoding for multihash objects: Base::Base16Lower -let encoded_mh1: EncodedMultihash = mh.into(); +// Uses the preferred encoding for multihash objects: Base16Lower +let encoded_mh1: EncodedMultihash = mh.clone().into(); -// or you can chose the base enccoding... -let encoded_mh2: EncodedMultihash::new(Base::Base32Upper, mh); +// Or choose a specific base encoding +let encoded_mh2: EncodedMultihash = EncodedMultihash::new(Base::Base32Upper, mh); ``` -[CRYPTID]: https://cryptid.tech/ -[PROVENANCE]: https://github.com/cryptidtech/provenance-specifications/ -[MULTIFORMATS]: https://github.com/multiformats/multiformats/ -[MULTIHASH]: https://www.multiformats.io/multihash/ +### Serde Integration + +With the `serde` feature (enabled by default), `Multihash` implements +`Serialize` and `Deserialize` — strings in human-readable formats, varint bytes +in binary formats: + +```rust +use multi_hash::Builder; +use multi_codec::Codec; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct DocumentHash { + hash: multi_hash::Multihash, + timestamp: u64, +} + +let doc = DocumentHash { + hash: Builder::new_from_bytes(Codec::Sha2256, b"document content") + .unwrap() + .try_build() + .unwrap(), + timestamp: 1234567890, +}; + +// Serialize to JSON (human-readable → codec name + hex digest) +let json = serde_json::to_string(&doc).unwrap(); +println!("{}", json); + +// Deserialize from JSON +let deserialized: DocumentHash = serde_json::from_str(&json).unwrap(); +assert_eq!(doc, deserialized); +``` + +### Error Handling + +All conversion and builder errors return `Result` with a structured `Error` enum: + +```rust +use multi_hash::{Builder, Error}; +use multi_codec::Codec; + +// Handle unsupported algorithms +match Builder::new_from_bytes(Codec::Identity, b"data") { + Err(Error::UnsupportedHash { codec }) => { + eprintln!("Algorithm {:?} not supported", codec); + } + Err(e) => eprintln!("Other error: {}", e), + Ok(_) => unreachable!(), +} + +// Handle missing hash data +match Builder::new(Codec::Sha2256).try_build() { + Err(Error::MissingHash) => { + eprintln!("Must call with_hash() before build()"); + } + Err(e) => eprintln!("Other error: {}", e), + Ok(_) => unreachable!(), +} +``` + +### Type-Safe Newtypes + +For additional type safety, use the newtype wrappers: + +```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); +assert_eq!(digest.as_bytes().len(), 32); + +// Type-safe algorithm identifier +let algo = AlgorithmId::new(Codec::Sha2256); +assert_eq!(algo.codec(), Codec::Sha2256); +assert_eq!(algo.name(), "sha2-256"); +assert_eq!(algo.code(), 0x12); +``` + +## Testing + +The crate has 110 tests across unit, integration, property-based, security, and +doc-test suites: + +```bash +# Run all tests +cargo test --all-features + +# Run specific test suites +cargo test --test edge_case_tests +cargo test --test integration_tests +cargo test --test proptest_tests +cargo test --test security_tests + +# Run benchmarks +cargo bench +``` + +Linting and formatting: + +```bash +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings +``` + +## Feature Flags + +- **`serde`** (default): Enables serde serialization/deserialization. When + enabled, `Multihash` implements `Serialize` and `Deserialize` — codec name + and hex digest in human-readable formats, varint bytes in binary formats. + +### Disabling Default Features + +```toml +[dependencies] +multi-hash = { version = "1.0", default-features = false } +``` + +## Security + +- `#![deny(unsafe_code)]` enforced at compile time +- All errors return `Result` types — no panics on invalid input +- All types are `Send + Sync` with no shared mutable state +- Hash computation uses vetted cryptographic libraries from the RustCrypto + ecosystem + +## Maintainers + +This repo: [@dgrantham](https://github.com/dgrantham). + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/cryptidtech/multi-hash/issues). + +### Development Guidelines + +- Run `cargo fmt` before committing +- Run `cargo clippy -- -D warnings` to check for issues +- Add tests for new features +- Update documentation for API changes +- Run the full test suite: `cargo test --all-features` + +## License + +[Apache-2.0](LICENSE) © Cryptid Technologies \ No newline at end of file diff --git a/benches/multihash_bench.rs b/benches/multihash_bench.rs index ab318a8..0ed922c 100644 --- a/benches/multihash_bench.rs +++ b/benches/multihash_bench.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 //! Performance benchmarks for multi-hash -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use multi_codec::Codec; use multi_hash::{Builder, Multihash}; use multi_trait::TryDecodeFrom; @@ -22,7 +22,7 @@ fn bench_hash_computation(c: &mut Criterion) { 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()) + b.iter(|| Builder::new_from_bytes(codec, data).unwrap().try_build()); }); } @@ -39,7 +39,7 @@ fn bench_encoding(c: &mut Criterion) { c.bench_function("multihash_to_bytes", |b| { b.iter(|| { let _bytes: Vec = black_box(mh.clone()).into(); - }) + }); }); } @@ -52,7 +52,7 @@ fn bench_decoding(c: &mut Criterion) { let bytes: Vec = mh.into(); c.bench_function("multihash_from_bytes", |b| { - b.iter(|| Multihash::try_from(black_box(bytes.as_ref()))) + b.iter(|| Multihash::try_from(black_box(bytes.as_ref()))); }); } @@ -62,8 +62,8 @@ fn bench_roundtrip(c: &mut Criterion) { let data = b"roundtrip benchmark data"; - for &codec in [Codec::Blake3, Codec::Sha2256, Codec::Sha3256].iter() { - let name = format!("{:?}", codec); + for &codec in &[Codec::Blake3, Codec::Sha2256, Codec::Sha3256] { + 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) @@ -72,7 +72,7 @@ fn bench_roundtrip(c: &mut Criterion) { .unwrap(); let bytes: Vec = mh1.into(); let _mh2 = Multihash::try_from(bytes.as_ref()).unwrap(); - }) + }); }); } @@ -92,7 +92,7 @@ fn bench_data_sizes(c: &mut Criterion) { Builder::new_from_bytes(Codec::Sha2256, black_box(data)) .unwrap() .try_build() - }) + }); }); } @@ -102,7 +102,7 @@ fn bench_data_sizes(c: &mut Criterion) { /// 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"))) + b.iter(|| Builder::new_from_bytes(black_box(Codec::Sha2256), black_box(b"data"))); }); let hash = vec![0u8; 32]; @@ -111,11 +111,11 @@ fn bench_builder(c: &mut Criterion) { Builder::new(black_box(Codec::Sha2256)) .with_hash(black_box(hash.clone())) .try_build() - }) + }); }); } -/// Benchmark TryDecodeFrom with varying encoded sizes +/// Benchmark `TryDecodeFrom` with varying encoded sizes fn bench_decode_from(c: &mut Criterion) { let mut group = c.benchmark_group("decode_from"); @@ -133,7 +133,7 @@ fn bench_decode_from(c: &mut Criterion) { 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()))) + b.iter(|| Multihash::try_decode_from(black_box(bytes.as_ref()))); }); } diff --git a/src/error.rs b/src/error.rs index e202219..6d2781d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,5 @@ // 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 /// @@ -158,7 +155,7 @@ pub enum Error { } impl Error { - /// Create an UnsupportedHash error + /// Create an `UnsupportedHash` error /// /// # Examples /// @@ -169,11 +166,12 @@ impl Error { /// let err = Error::unsupported_hash(Codec::Identity); /// assert!(matches!(err, Error::UnsupportedHash { .. })); /// ``` - pub fn unsupported_hash(codec: multi_codec::Codec) -> Self { + #[must_use] + pub const fn unsupported_hash(codec: multi_codec::Codec) -> Self { Self::UnsupportedHash { codec } } - /// Create an InvalidDigestLength error + /// Create an `InvalidDigestLength` error /// /// # Examples /// @@ -184,7 +182,8 @@ impl Error { /// let err = Error::invalid_digest_length(Codec::Sha2256, 32, 16); /// assert!(matches!(err, Error::InvalidDigestLength { .. })); /// ``` - pub fn invalid_digest_length( + #[must_use] + pub const fn invalid_digest_length( algorithm: multi_codec::Codec, expected: usize, actual: usize, @@ -196,7 +195,7 @@ impl Error { } } - /// Create a HashComputeFailed error + /// Create a `HashComputeFailed` error /// /// # Examples /// @@ -231,7 +230,8 @@ impl Error { /// let err = Error::unsupported_hash(Codec::Identity); /// assert_eq!(err.kind(), "UnsupportedHash"); /// ``` - pub fn kind(&self) -> &str { + #[must_use] + pub const fn kind(&self) -> &str { match self { Self::Multicodec(_) => "Multicodec", Self::Multiutil(_) => "Multiutil", @@ -257,22 +257,22 @@ impl Error { /// let context = err.context(); /// assert!(!context.is_empty()); /// ``` + #[must_use] pub fn context(&self) -> String { match self { - Self::Multicodec(e) => format!("Multicodec error: {}", e), - Self::Multiutil(e) => format!("Multiutil error: {}", e), + 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::UnsupportedHash { codec } => format!("Unsupported hash: {codec:?}"), Self::InvalidDigestLength { algorithm, expected, actual, } => format!( - "Invalid digest length for {:?}: expected {}, got {}", - algorithm, expected, actual + "Invalid digest length for {algorithm:?}: expected {expected}, got {actual}" ), Self::HashComputeFailed { algorithm, message } => { - format!("Hash computation failed for {:?}: {}", algorithm, message) + format!("Hash computation failed for {algorithm:?}: {message}") } } } @@ -328,7 +328,7 @@ mod tests { Error::hash_compute_failed(Codec::Sha2256, "test"), ]; - let kinds: Vec<_> = errors.iter().map(|e| e.kind()).collect(); + let kinds: Vec<_> = errors.iter().map(Error::kind).collect(); assert_eq!(kinds.len(), 4); // All kinds should be unique diff --git a/src/lib.rs b/src/lib.rs index f91febe..8bcfc8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,6 +188,7 @@ #![warn(missing_docs)] #![deny( + unsafe_code, trivial_casts, trivial_numeric_casts, unused_import_braces, @@ -200,7 +201,7 @@ pub use error::Error; /// Multihash type and functions pub mod mh; -pub use mh::{Builder, EncodedMultihash, Multihash, HASH_CODECS, SAFE_HASH_CODECS}; +pub use mh::{Builder, EncodedMultihash, HASH_CODECS, Multihash, SAFE_HASH_CODECS}; /// Type-safe wrappers for multihash components pub mod types; diff --git a/src/mh.rs b/src/mh.rs index c00d636..e812f41 100644 --- a/src/mh.rs +++ b/src/mh.rs @@ -9,9 +9,9 @@ use core::fmt; use digest::{Digest, DynDigest, InvalidBufferSize}; use multi_base::Base; use multi_codec::Codec; -use multi_trait::{Null, TryDecodeFrom}; +use multi_trait::{EncodeInto, Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, DetectedEncoder, EncodingInfo, Varbytes}; -use typenum::consts::*; +use typenum::consts::{U28, U32, U48, U64}; /// the hash codecs currently supported pub const HASH_CODECS: [Codec; 23] = [ @@ -135,12 +135,16 @@ impl EncodingInfo for Multihash { } impl From for Vec { - fn from(mh: Multihash) -> Vec { - let mut v = Vec::default(); - // add in the hash codec - v.append(&mut mh.codec.into()); - // add in the hash data - v.append(&mut Varbytes::new(mh.hash).into()); + 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(); + let total = codec_bytes.len() + len_bytes.len() + mh.hash.len(); + + let mut v = Self::with_capacity(total); + v.extend_from_slice(&codec_bytes); + v.extend_from_slice(&len_bytes); + v.extend_from_slice(&mh.hash); v } } @@ -178,11 +182,11 @@ impl AsRef<[u8]> for Multihash { /// Multihashes can have a null value impl Null for Multihash { fn null() -> Self { - Multihash::default() + Self::default() } fn is_null(&self) -> bool { - *self == Multihash::default() + *self == Self::default() } } @@ -208,14 +212,20 @@ pub struct Builder { impl Builder { /// create a hash with the given codec + #[must_use] pub fn new(codec: Codec) -> Self { - Builder { + Self { codec, ..Default::default() } } /// create a new builder from a hash + /// + /// # Errors + /// + /// Returns `Error::UnsupportedHash` if `codec` is not a recognized hash + /// algorithm in [`HASH_CODECS`]. pub fn new_from_bytes(codec: Codec, bytes: impl AsRef<[u8]>) -> Result { let mut hasher: Box = match codec { Codec::Blake2B224 => Box::new(blake2::Blake2b::::new()), @@ -255,18 +265,24 @@ impl Builder { } /// set the hash data + #[must_use] pub fn with_hash(mut self, hash: impl Into>) -> Self { self.hash = Some(hash.into()); self } /// set the base encoding codec - pub fn with_base_encoding(mut self, base: Base) -> Self { + #[must_use] + pub const fn with_base_encoding(mut self, base: Base) -> Self { self.base_encoding = Some(base); self } /// build a base encoded multihash + /// + /// # Errors + /// + /// Returns `Error::MissingHash` if no hash data was set on the builder. pub fn try_build_encoded(&self) -> Result { Ok(BaseEncoded::new( self.base_encoding @@ -276,6 +292,10 @@ impl Builder { } /// build the multihash by hashing the provided data + /// + /// # Errors + /// + /// Returns `Error::MissingHash` if no hash data was set on the builder. pub fn try_build(&self) -> Result { Ok(Multihash { codec: self.codec, @@ -374,7 +394,7 @@ mod tests { .try_build_encoded() .unwrap(); let s = mh.to_string(); - println!("{:?}", mh); + println!("{mh:?}"); println!("{s}"); assert_eq!(mh, EncodedMultihash::try_from(s.as_str()).unwrap()); } diff --git a/src/serde/de.rs b/src/serde/de.rs index 4d060af..16161d6 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,11 +1,11 @@ // SPDX-License-Idnetifier: Apache-2.0 -use crate::{mh::SIGIL, Multihash}; +use crate::{Multihash, mh::SIGIL}; use core::fmt; use multi_codec::Codec; use multi_util::EncodedVarbytes; use serde::{ - de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, + de::{Error, MapAccess, Visitor}, }; /// Deserialize instance of [`crate::Multihash`] diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 85c2c2d..677708e 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -7,7 +7,7 @@ mod ser; mod tests { use crate::prelude::{Base, Builder, Codec, Multihash}; use multi_trait::Null; - use serde_test::{assert_tokens, Configure, Token}; + use serde_test::{Configure, Token, assert_tokens}; #[test] fn test_serde_compact() { diff --git a/src/types.rs b/src/types.rs index 0d9570e..756956e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,7 +25,7 @@ use multi_codec::Codec; pub struct HashDigest(Vec); impl HashDigest { - /// Create a new HashDigest from bytes + /// Create a new `HashDigest` from bytes /// /// # Examples /// @@ -35,7 +35,8 @@ impl HashDigest { /// let digest = HashDigest::new(vec![1, 2, 3, 4]); /// assert_eq!(digest.len(), 4); /// ``` - pub fn new(bytes: Vec) -> Self { + #[must_use] + pub const fn new(bytes: Vec) -> Self { Self(bytes) } @@ -49,6 +50,7 @@ impl HashDigest { /// let digest = HashDigest::new(vec![1, 2, 3]); /// assert_eq!(digest.as_bytes(), &[1, 2, 3]); /// ``` + #[must_use] pub fn as_bytes(&self) -> &[u8] { &self.0 } @@ -63,6 +65,7 @@ impl HashDigest { /// let digest = HashDigest::new(vec![0u8; 32]); /// assert_eq!(digest.len(), 32); /// ``` + #[must_use] pub fn len(&self) -> usize { self.0.len() } @@ -80,6 +83,7 @@ impl HashDigest { /// let digest = HashDigest::new(vec![1, 2, 3]); /// assert!(!digest.is_empty()); /// ``` + #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -95,6 +99,7 @@ impl HashDigest { /// let bytes: Vec = digest.into_bytes(); /// assert_eq!(bytes, vec![1, 2, 3]); /// ``` + #[must_use] pub fn into_bytes(self) -> Vec { self.0 } @@ -107,7 +112,7 @@ impl From> for HashDigest { } impl From for Vec { - fn from(digest: HashDigest) -> Vec { + fn from(digest: HashDigest) -> Self { digest.0 } } @@ -142,7 +147,7 @@ impl fmt::Display for HashDigest { pub struct AlgorithmId(Codec); impl AlgorithmId { - /// Create a new AlgorithmId + /// Create a new `AlgorithmId` /// /// # Examples /// @@ -153,6 +158,7 @@ impl AlgorithmId { /// let algo = AlgorithmId::new(Codec::Sha2256); /// assert_eq!(algo.codec(), Codec::Sha2256); /// ``` + #[must_use] pub const fn new(codec: Codec) -> Self { Self(codec) } @@ -168,6 +174,7 @@ impl AlgorithmId { /// let algo = AlgorithmId::new(Codec::Sha2512); /// assert_eq!(algo.codec(), Codec::Sha2512); /// ``` + #[must_use] pub const fn codec(self) -> Codec { self.0 } @@ -183,6 +190,7 @@ impl AlgorithmId { /// let algo = AlgorithmId::new(Codec::Sha2256); /// assert_eq!(algo.code(), 0x12); /// ``` + #[must_use] pub fn code(self) -> u64 { self.0.code() } @@ -198,6 +206,7 @@ impl AlgorithmId { /// let algo = AlgorithmId::new(Codec::Sha2256); /// assert_eq!(algo.name(), "sha2-256"); /// ``` + #[must_use] pub fn name(self) -> &'static str { self.0.into() } @@ -210,7 +219,7 @@ impl From for AlgorithmId { } impl From for Codec { - fn from(algo: AlgorithmId) -> Codec { + fn from(algo: AlgorithmId) -> Self { algo.0 } } diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs index c2b16e7..41f2d9e 100644 --- a/tests/edge_case_tests.rs +++ b/tests/edge_case_tests.rs @@ -2,16 +2,16 @@ //! Edge case tests for multi-hash use multi_codec::Codec; -use multi_hash::{Builder, Error, Multihash, HASH_CODECS, SAFE_HASH_CODECS}; +use multi_hash::{Builder, Error, HASH_CODECS, Multihash, 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() { + for &codec in &HASH_CODECS { let result = Builder::new_from_bytes(codec, []); - assert!(result.is_ok(), "Failed for {:?}", codec); + assert!(result.is_ok(), "Failed for {codec:?}"); let mh = result.unwrap().try_build().unwrap(); assert_eq!(mh.codec(), codec); @@ -21,7 +21,7 @@ fn test_all_algorithms_empty_input() { /// Test all supported hash algorithms with single byte #[test] fn test_all_algorithms_single_byte() { - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { let mh = Builder::new_from_bytes(codec, [0x42]) .unwrap() .try_build() @@ -76,7 +76,7 @@ fn test_large_data() { fn test_binary_roundtrip_all_algorithms() { let data = b"test data"; - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { let mh1 = Builder::new_from_bytes(codec, data) .unwrap() .try_build() @@ -90,14 +90,13 @@ fn test_binary_roundtrip_all_algorithms() { } } -/// Test SAFE_HASH_CODECS are subset of HASH_CODECS +/// Test `SAFE_HASH_CODECS` are subset of `HASH_CODECS` #[test] fn test_safe_codecs_subset() { - for &safe_codec in SAFE_HASH_CODECS.iter() { + for &safe_codec in &SAFE_HASH_CODECS { assert!( HASH_CODECS.contains(&safe_codec), - "{:?} not in HASH_CODECS", - safe_codec + "{safe_codec:?} not in HASH_CODECS" ); } } @@ -131,7 +130,7 @@ fn test_multihash_ordering() { assert_ne!(mh1, mh2); } -/// Test AsRef implementation +/// Test `AsRef` implementation #[test] fn test_as_ref() { let mh = Builder::new_from_bytes(Codec::Sha2256, b"test") @@ -151,7 +150,7 @@ fn test_debug_format() { .try_build() .unwrap(); - let debug_str = format!("{:?}", mh); + let debug_str = format!("{mh:?}"); assert!(!debug_str.is_empty()); assert!(debug_str.len() > 10); } @@ -183,7 +182,7 @@ fn test_clone() { assert_eq!(mh1.as_ref(), mh2.as_ref()); } -/// Test TryDecodeFrom with trailing data +/// Test `TryDecodeFrom` with trailing data #[test] fn test_decode_with_trailing_data() { let mh1 = Builder::new_from_bytes(Codec::Sha2256, b"test") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 899a0ff..5b4b7d7 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 //! Integration tests for multi-hash with other workspace crates +#![allow(clippy::unreadable_literal)] use multi_base::Base; use multi_codec::Codec; @@ -13,7 +14,7 @@ fn test_multicodec_integration() { // Verify all hash codecs are valid Codec values use multi_hash::HASH_CODECS; - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { // Should be able to get codec properties let code = codec.code(); let name = codec.as_str(); @@ -23,7 +24,7 @@ fn test_multicodec_integration() { } } -/// Test integration with multi-base through EncodedMultihash +/// Test integration with multi-base through `EncodedMultihash` #[test] fn test_multibase_integration() { // Test with various base encodings @@ -68,7 +69,7 @@ fn test_multitrait_integration() { assert!(remaining.is_empty()); } -/// Test integration with multi-util BaseEncoded +/// Test integration with multi-util `BaseEncoded` #[test] fn test_multiutil_integration() { let mh = Builder::new_from_bytes(Codec::Blake3, b"multiutil test") @@ -85,7 +86,7 @@ fn test_multiutil_integration() { assert_eq!(Multihash::preferred_encoding(), Base::Base16Lower); } -/// Test EncodedMultihash uses BaseEncoded from multiutil +/// Test `EncodedMultihash` uses `BaseEncoded` from multiutil #[test] fn test_encoded_multihash_type() { use multi_hash::EncodedMultihash; @@ -124,7 +125,7 @@ fn test_in_collections() { assert_eq!(btree.len(), 2); // Vec - let vec = [mh1.clone(), mh2.clone()]; + let vec = [mh1, mh2]; assert_eq!(vec.len(), 2); } diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs index a6b80b7..def8c89 100644 --- a/tests/proptest_tests.rs +++ b/tests/proptest_tests.rs @@ -2,7 +2,7 @@ //! Property-based tests for multi-hash using proptest use multi_codec::Codec; -use multi_hash::{Builder, Multihash, HASH_CODECS}; +use multi_hash::{Builder, HASH_CODECS, Multihash}; use multi_trait::TryDecodeFrom; use multi_util::CodecInfo; use proptest::prelude::*; @@ -11,7 +11,7 @@ use proptest::prelude::*; #[test] fn test_multihash_roundtrip() { proptest!(|(data in prop::collection::vec(any::(), 0..1024))| { - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { 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(); @@ -57,7 +57,7 @@ fn test_different_data_different_hash() { #[test] fn test_codec_preserved() { proptest!(|(data in prop::collection::vec(any::(), 0..256))| { - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { 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(); @@ -73,7 +73,7 @@ fn test_codec_preserved() { #[test] fn test_empty_data_valid() { proptest!(|(_unit in 0..1u8)| { - for &codec in HASH_CODECS.iter() { + for &codec in &HASH_CODECS { let result = Builder::new_from_bytes(codec, []); prop_assert!(result.is_ok()); } diff --git a/tests/security_tests.rs b/tests/security_tests.rs index 2ba329a..165692b 100644 --- a/tests/security_tests.rs +++ b/tests/security_tests.rs @@ -106,7 +106,7 @@ fn test_null_safety() { assert!(null_mh.is_null()); // Encoding null should not panic - let bytes: Vec = null_mh.clone().into(); + let bytes: Vec = null_mh.into(); let decoded = Multihash::try_from(bytes.as_ref()).unwrap(); assert!(decoded.is_null()); } @@ -126,7 +126,7 @@ fn test_error_send_sync() { fn test_safe_hash_codecs_functional() { let data = b"safety test data"; - for &codec in SAFE_HASH_CODECS.iter() { + for &codec in &SAFE_HASH_CODECS { let mh = Builder::new_from_bytes(codec, data) .unwrap() .try_build()