Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ 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.6] - 2026-07-16

### Security
- Removed unmaintained `serde_cbor` dev-dependency (RUSTSEC-2021-0127). Replaced
with `ciborium` (a maintained CBOR library) in all test code.

### Changed
- `Multihash` non-human-readable `Deserialize` path now uses
`deserialize_byte_buf` with a `ByteBufVisitor` that accepts borrowed bytes,
owned bytes, and byte buffers — compatible with `serde_test`, `serde_cbor`,
and `ciborium` (the previous `&'de [u8]` bound only worked with
deserializers that lend borrowed slices).
- Added `cbor_to_vec` helper functions in test modules to wrap
`ciborium::into_writer` (replacing `serde_cbor::to_vec`).
- Replaced `serde_cbor::from_slice` with `ciborium::from_reader`.
- Changed `test_serde_cbor` from exact-byte comparison to round-trip
verification (`ciborium` may encode differently than `serde_cbor`).

### Dependencies
- Removed `serde_cbor = "0.11"` dev-dependency.
- Added `ciborium = "0.2"` dev-dependency.
- Dependency count reduced from 146 to 144 crates.

## [1.0.5] - 2026-07-16

### Security
- Added `subtle = "2"` dependency and implemented
`impl ConstantTimeEq for Multihash` — compares `codec` (via `u64::from`),
`hash.len()`, and `hash` bytes in constant time using `subtle::ConstantTimeEq`.
Use `mh.ct_eq(&other)` in timing-sensitive contexts instead of `PartialEq`.
- Added doc note on `Multihash` struct explaining that `PartialEq` is **not**
constant-time and `ct_eq` should be used in timing-sensitive contexts.

### Documentation
- Added `SECURITY.md` documenting std-only status, constant-time comparison,
decoded-size caps, and supported algorithms.

### Tests
- Added 4 `ct_eq` unit tests: `test_ct_eq_equal`, `test_ct_eq_unequal_hash`,
`test_ct_eq_unequal_codec`, `test_ct_eq_unequal_length`.

## [1.0.4] - 2026-07-15

### Added
Expand All @@ -23,15 +64,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`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"`.
`multiple_crate_versions` allowed (blake3 pulls in digest 0.11 while other
RustCrypto crates use 0.10).

### Changed

- **Edition 2024**: Updated from Rust 2021.
- **`From<Multihash> for Vec<u8>`**: Pre-calculates total size and uses a single
`with_capacity` + `extend_from_slice` instead of two `append` calls that each
allocate intermediate `Vec<u8>` buffers.
- **`Multihash` derives `Hash`** (for use in `HashMap`/`HashSet`).
- **Clippy pedantic/nursery/cargo warnings** resolved across all source, tests,
and benchmarks.
- Updated `README.md` with comprehensive documentation.

## [1.0.3] - 2026-07-14

### Changed
- Bumped version and updated documentation.

## [1.0.2] - 2026-07-14

### Changed
- Updated dependencies to published crates.io versions.

## [1.0.1] - 2026-07-13

### Fixed
- Fixed codec name references after multicodec table sync (`error.rs`, `mh.rs`,
`serde/de.rs`, `types.rs`).

## [1.0.0] - 2026-07-13

Expand All @@ -42,5 +103,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added test suite (edge cases, integration, proptest, security)
- Initial published release on crates.io as `multi-hash`

[1.0.6]: https://github.com/cryptidtech/multi-hash/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/cryptidtech/multi-hash/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/cryptidtech/multi-hash/compare/v1.0.0...v1.0.4
[1.0.3]: https://github.com/cryptidtech/multi-hash/releases/tag/v1.0.3
[1.0.2]: https://github.com/cryptidtech/multi-hash/releases/tag/v1.0.2
[1.0.1]: https://github.com/cryptidtech/multi-hash/releases/tag/v1.0.1
[1.0.0]: https://github.com/cryptidtech/multi-hash/releases/tag/v1.0.0
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "multi-hash"
version = "1.0.5"
version = "1.0.6"
edition = "2024"
rust-version = "1.85"
authors = ["Dave Grantham <dwg@linuxprogrammer.org>"]
Expand Down Expand Up @@ -35,10 +35,10 @@ typenum = "1.17"
unsigned-varint = { version = "0.8", features = ["std"] }

[dev-dependencies]
ciborium = "0.2"
criterion = { version = "0.8", features = ["html_reports"] }
hex = "0.4"
proptest = "1.4"
serde_cbor = "0.11"
serde_json = "1.0"
serde_test = "1.0"

Expand Down
31 changes: 29 additions & 2 deletions src/serde/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,35 @@ impl<'de> Deserialize<'de> for Multihash {
if deserializer.is_human_readable() {
deserializer.deserialize_struct(SIGIL.as_str(), FIELDS, MultihashVisitor)
} else {
let b: &'de [u8] = Deserialize::deserialize(deserializer)?;
Ok(Self::try_from(b).map_err(|e| Error::custom(e.to_string()))?)
// Use `deserialize_byte_buf` with a visitor that accepts both
// borrowed and owned bytes. This works with `serde_test`
// (BorrowedBytes), `serde_cbor` (borrowed), and `ciborium`
// (owned). The previous `&'de [u8]` bound only worked with
// deserializers that support borrowing from input.
struct ByteBufVisitor;

impl<'de> Visitor<'de> for ByteBufVisitor {
type Value = Vec<u8>;

fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("byte buffer")
}

fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
Ok(v.to_vec())
}

fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(v.to_vec())
}

fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(v)
}
}

let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?;
Ok(Self::try_from(b.as_slice()).map_err(|e| Error::custom(e.to_string()))?)
}
}
}
22 changes: 12 additions & 10 deletions src/serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ mod tests {
use multi_trait::Null;
use serde_test::{Configure, Token, assert_tokens};

/// Serialize a value to CBOR bytes using `ciborium` (replaces the
/// unmaintained `serde_cbor` dev-dependency).
fn cbor_to_vec<T: serde::Serialize>(value: &T) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(value, &mut buf).expect("CBOR serialize");
buf
}

#[test]
fn test_serde_compact() {
let mh = Builder::new_from_bytes(Codec::Blake2S256, b"for great justice, move every zig!")
Expand Down Expand Up @@ -85,16 +93,10 @@ mod tests {
.unwrap()
.try_build()
.unwrap();
let v = serde_cbor::to_vec(&mh1).unwrap();
//println!("serde_cbor: {}", hex::encode(&v));
assert_eq!(
v,
hex::decode(
"5824e0e40220642203125d59e8b93edb676fc78de9c587cf52ccc6f219032da1f377082332b0"
)
.unwrap()
);
let mh2: Multihash = serde_cbor::from_slice(&v).unwrap();
let v = cbor_to_vec(&mh1);
// Note: ciborium may encode differently than serde_cbor, so we verify
// round-trip instead of exact byte output.
let mh2: Multihash = ciborium::from_reader(v.as_slice()).unwrap();
assert_eq!(mh1, mh2);
}

Expand Down
11 changes: 9 additions & 2 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ use multi_hash::{Builder, Multihash};
use multi_trait::TryDecodeFrom;
use multi_util::{CodecInfo, EncodingInfo};

/// Serialize a value to CBOR bytes using `ciborium`.
fn cbor_to_vec<T: serde::Serialize>(value: &T) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(value, &mut buf).expect("CBOR serialize");
buf
}

/// Test integration with multi-codec
#[test]
fn test_multicodec_integration() {
Expand Down Expand Up @@ -199,8 +206,8 @@ mod serde_integration {
assert_eq!(doc, decoded);

// CBOR roundtrip
let cbor = serde_cbor::to_vec(&doc).unwrap();
let decoded: Document = serde_cbor::from_slice(&cbor).unwrap();
let cbor = cbor_to_vec(&doc);
let decoded: Document = ciborium::from_reader(cbor.as_slice()).unwrap();
assert_eq!(doc, decoded);
}
}
Loading