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
55 changes: 48 additions & 7 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 38 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,46 @@ 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.3] - 2026-07-15

### Added

- **`#![deny(unsafe_code)]`** at the crate root.
- **`#[must_use]`** on `Error::custom()`.
- **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"`.
- **`no_std` documentation** in the crate root noting that the crate is
currently std-only and the `serde` alloc feature config does not make the
crate `no_std`.
- **`Varuint` module documentation** explaining the relationship between
`Varuint` and the integer trait impls in `multi-trait`.
- **`# Errors`** doc section on `BaseEncoder::from_base_encoded`.
- **`#[inline]`** on `Deref`/`DerefMut` impls (replaced `#[inline(always)]`).

### Changed

- **Edition 2024**: Updated from Rust 2021.
- **`Varbytes::encode_into`**: Replaced `v.append(&mut self.0.clone())` with
`v.extend_from_slice(&self.0)` to avoid cloning the entire payload.
- **`Varbytes` serde `Serialize`**: Replaced
`v.append(&mut self.as_bytes().to_vec())` with
`v.extend_from_slice(self.as_bytes())` to avoid an intermediate allocation.
- **`DetectedEncoder::from_base_encoded`**: Bails on the first strict decode
success instead of collecting all successful decodings, avoiding O(n)
redundant decodes and false positives from overlapping alphabets.
- **`DetectedEncoder` doc comment**: Fixed typos and formatting.
- **Clippy pedantic/nursery/cargo warnings** resolved across all source.

## [1.0.0] - 2026-07-13

### Changed
- Synced from bettersign workspace (bs-multiutil 0.7.0)
- Renamed crate from `bs-multiutil` to `multi-util`
- Initial published release on crates.io as `multi-util`
- Initial published release on crates.io as `multi-util`

[1.0.3]: https://github.com/cryptidtech/multi-util/compare/v1.0.0...v1.0.3
[1.0.0]: https://github.com/cryptidtech/multi-util/releases/tag/v1.0.0
13 changes: 11 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[package]
name = "multi-util"
version = "1.0.2"
edition = "2021"
version = "1.0.3"
edition = "2024"
rust-version = "1.85"
authors = ["Dave Huseby <dwh@linuxprogrammer.org>"]
description = "Multiformat utility functions and types"
repository = "https://github.com/cryptidtech/multi-util.git"
Expand All @@ -27,3 +28,11 @@ proptest = "1.4"
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 }

[lints.rust]
unsafe_code = "deny"
25 changes: 12 additions & 13 deletions src/base_encoded.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
error::BaseEncodedError, prelude::Base, BaseEncoder, EncodingInfo, Error, MultibaseEncoder,
BaseEncoder, EncodingInfo, Error, MultibaseEncoder, error::BaseEncodedError, prelude::Base,
};
use core::{
cmp::Ordering,
Expand All @@ -12,7 +12,7 @@ use core::{

/// Smart pointer for multibase encoded data. This supports encoding to and
/// decoding from multibase encoding strings using [`TryFrom<&str>`] and
/// ['to_string()']
/// [`to_string()`](core::fmt::Display::to_string)
#[derive(Clone)]
pub struct BaseEncoded<T, Enc = MultibaseEncoder>
where
Expand All @@ -29,8 +29,8 @@ where
T: EncodingInfo,
Enc: BaseEncoder,
{
/// Construct a new BaseEncoded instance with the given base
pub fn new(base: Base, t: T) -> Self {
/// Construct a new `BaseEncoded` instance with the given base
pub const fn new(base: Base, t: T) -> Self {
Self {
base,
t,
Expand Down Expand Up @@ -98,22 +98,22 @@ where
}
}

impl<T, Enc> PartialEq<BaseEncoded<T, Enc>> for BaseEncoded<T, Enc>
impl<T, Enc> PartialEq<Self> for BaseEncoded<T, Enc>
where
T: EncodingInfo + PartialEq<T> + ?Sized,
Enc: BaseEncoder,
{
fn eq(&self, other: &BaseEncoded<T, Enc>) -> bool {
fn eq(&self, other: &Self) -> bool {
self.base == other.base && self.t == other.t
}
}

impl<T, Enc> PartialOrd<BaseEncoded<T, Enc>> for BaseEncoded<T, Enc>
impl<T, Enc> PartialOrd<Self> for BaseEncoded<T, Enc>
where
T: EncodingInfo + PartialEq<T> + PartialOrd<T> + ?Sized,
Enc: BaseEncoder,
{
fn partial_cmp(&self, other: &BaseEncoded<T, Enc>) -> Option<Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.t.partial_cmp(&other.t)
}
}
Expand Down Expand Up @@ -141,8 +141,7 @@ where
Enc: BaseEncoder,
{
fn hash<H: Hasher>(&self, state: &mut H) {
let s = self.to_string();
s.hash(state);
self.to_string().hash(state);
}
}

Expand All @@ -153,7 +152,7 @@ where
{
type Target = T;

#[inline(always)]
#[inline]
fn deref(&self) -> &Self::Target {
&self.t
}
Expand All @@ -164,7 +163,7 @@ where
T: EncodingInfo,
Enc: BaseEncoder,
{
#[inline(always)]
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.t
}
Expand All @@ -190,6 +189,6 @@ where
Enc: BaseEncoder,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} - {:?}", Enc::debug_string(self.base), self.t,)
write!(f, "{} - {:?}", Enc::debug_string(self.base), self.t)
}
}
44 changes: 22 additions & 22 deletions src/base_encoder.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
base_name,
BaseIter, Error, base_name,
error::{BaseEncodedError, BaseEncoderError},
prelude::Base,
BaseIter, Error,
};

/// a trait for base encoding implementations
pub trait BaseEncoder {
/// convert a &[u8] to a base encoded value
fn to_base_encoded(base: Base, b: &[u8]) -> String;

/// convert a base encoded value to a `Vec<u8>`
/// convert a base encoded value to a `Vec<u8>`.
///
/// # Errors
///
/// Returns an error if the input cannot be decoded as a valid base-encoded
/// value for any of the encoder's supported bases.
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error>;

/// get the debug string for the given base
Expand All @@ -32,7 +36,7 @@ impl BaseEncoder for MultibaseEncoder {
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error> {
// try permissive multibase decoding
Ok(vec![
multi_base::decode(s, false).map_err(|_| BaseEncodedError::ValueFailed)?
multi_base::decode(s, false).map_err(|_| BaseEncodedError::ValueFailed)?,
])
}
fn debug_string(base: Base) -> String {
Expand All @@ -43,7 +47,7 @@ impl BaseEncoder for MultibaseEncoder {
}
}

/// a bare Base58Btc encoder implementation for use with legacy CIDs
/// a bare `Base58Btc` encoder implementation for use with legacy CIDs
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Base58Encoder {}

Expand All @@ -55,7 +59,7 @@ impl BaseEncoder for Base58Encoder {
// try strict Base58Btc decoding
match Base::Base58Btc.decode(s, true) {
Ok(v) => Ok(vec![(Base::Base58Btc, v)]),
Err(e) => Err(BaseEncoderError::Base58(format!("{:?}", e)).into()),
Err(e) => Err(BaseEncoderError::Base58(format!("{e:?}")).into()),
}
}
fn debug_string(_base: Base) -> String {
Expand All @@ -70,10 +74,11 @@ impl BaseEncoder for Base58Encoder {
}
}

/// a speculative encoder that tries to detect the correct encoding and decode it
/// encoding is always done using multibase so this does not support symetric
/// decode/encode round trips. this is useful for decoding CIDs that might be
/// base58 encoded "legacy" CIDs but alsy may be multibase encoded CIDs.
/// A speculative encoder that tries to detect the correct encoding and decode it.
///
/// Encoding is always done using multibase so this does not support symmetric
/// decode/encode round trips. This is useful for decoding CIDs that might be
/// base58 encoded "legacy" CIDs but also may be multibase encoded CIDs.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DetectedEncoder {}

Expand All @@ -82,28 +87,23 @@ impl BaseEncoder for DetectedEncoder {
multi_base::encode(base, b)
}
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error> {
// first try permissive multibase decoding
// First try permissive multibase decoding (prefix-based).
if let Ok((base, data)) = multi_base::decode(s, false) {
return Ok(vec![(base, data)]);
}

// start at the Identity base so we skip it
// Start after the Identity base so we skip it.
let iter: BaseIter = Base::Identity.into();

// next try "naked" encoding in increasing symbol space size order.
// these have to be strict decodings to avoid confusion
let mut v = Vec::default();
// Try "naked" encoding in increasing symbol-space size order.
// Use strict decoding and bail on the first success to avoid
// false positives from bases with overlapping alphabets.
for encoding in iter {
if let Ok(data) = encoding.decode(s, true) {
v.push((encoding, data));
return Ok(vec![(encoding, data)]);
}
}
if v.is_empty() {
// raise an error
Err(BaseEncodedError::ValueFailed.into())
} else {
Ok(v)
}
Err(BaseEncodedError::ValueFailed.into())
}
fn debug_string(base: Base) -> String {
format!("{} ('{}')", base_name(base), base.code())
Expand Down
22 changes: 17 additions & 5 deletions src/base_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
use crate::prelude::Base;

/// convert a multibase Base to its string equivalent
#[must_use]
pub fn base_name(b: Base) -> String {
use Base::*;
use Base::{
Base2, Base8, Base10, Base16Lower, Base16Upper, Base32HexLower, Base32HexPadLower,
Base32HexPadUpper, Base32HexUpper, Base32Lower, Base32PadLower, Base32PadUpper,
Base32Upper, Base32Z, Base36Lower, Base36Upper, Base58Btc, Base58Flickr, Base64, Base64Pad,
Base64Url, Base64UrlPad, Base256Emoji, Identity,
};
match b {
Identity => "Identity",
Base2 => "Base2",
Expand Down Expand Up @@ -43,8 +49,9 @@ impl Default for BaseIter {
}

impl BaseIter {
/// create a new BaseIter
pub fn new() -> Self {
/// create a new `BaseIter`
#[must_use]
pub const fn new() -> Self {
Self(None)
}
}
Expand All @@ -59,7 +66,12 @@ impl Iterator for BaseIter {
type Item = Base;

fn next(&mut self) -> Option<Self::Item> {
use Base::*;
use Base::{
Base2, Base8, Base10, Base16Lower, Base16Upper, Base32HexLower, Base32HexPadLower,
Base32HexPadUpper, Base32HexUpper, Base32Lower, Base32PadLower, Base32PadUpper,
Base32Upper, Base32Z, Base36Lower, Base36Upper, Base58Btc, Base58Flickr, Base64,
Base64Pad, Base64Url, Base64UrlPad, Base256Emoji, Identity,
};
let result = match self.0 {
None => Identity,
Some(b) => match b {
Expand Down Expand Up @@ -106,7 +118,7 @@ mod tests {
#[test]
fn test_last_iter() {
let mut iter: BaseIter = Base::Base256Emoji.into();
assert_eq!(iter.next(), None)
assert_eq!(iter.next(), None);
}

#[test]
Expand Down
Loading
Loading