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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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-multiutil 0.7.0)
- Renamed crate from `bs-multiutil` to `multi-util`
- Initial published release on crates.io as `multi-util`
18 changes: 11 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
[package]
name = "multiutil"
version = "1.0.15"
name = "multi-util"
version = "1.0.2"
edition = "2021"
authors = ["Dave Huseby <dwh@linuxprogrammer.org>"]
description = "Multiformat utility functions and types"
repository = "https://github.com/cryptidtech/multiutil.git"
repository = "https://github.com/cryptidtech/multi-util.git"
readme = "README.md"
license = "Apache-2.0"
keywords = ["multiformats", "multicodec", "multibase", "encoding"]
categories = ["encoding"]

[features]
default = ["serde"]

[dependencies]
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" }
multi-base = "1.0"
multi-codec = "1.0"
multi-trait = "1.0"
serde = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
thiserror = "1.0"
thiserror = { version = "2.0" }

[dev-dependencies]
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"
4 changes: 2 additions & 2 deletions src/base_encoded.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::{
error::BaseEncodedError, prelude::Base, BaseEncoder, EncodingInfo, Error, MultibaseEncoder,
};
Expand Down Expand Up @@ -88,7 +88,7 @@ where
base,
t,
enc: PhantomData,
})
});
}
}
Err(BaseEncodedError::ValueFailed.into())
Expand Down
29 changes: 17 additions & 12 deletions src/base_encoder.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::{
base_name,
BaseIter,
error::{BaseEncodedError, BaseEncoderError},
prelude::Base,
Error,
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>`
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error>;

/// get the debug string for the given base
Expand All @@ -28,11 +27,13 @@ pub struct MultibaseEncoder {}

impl BaseEncoder for MultibaseEncoder {
fn to_base_encoded(base: Base, b: &[u8]) -> String {
multibase::encode(base, b)
multi_base::encode(base, b)
}
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error> {
// try permissive multibase decoding
Ok(vec![multibase::decode(s, false).map_err(|_| BaseEncodedError::ValueFailed)?])
Ok(vec![
multi_base::decode(s, false).map_err(|_| BaseEncodedError::ValueFailed)?
])
}
fn debug_string(base: Base) -> String {
format!("{} ('{}')", base_name(base), base.code())
Expand All @@ -58,30 +59,34 @@ impl BaseEncoder for Base58Encoder {
}
}
fn debug_string(_base: Base) -> String {
format!("{} ('{}')", base_name(Base::Base58Btc), Base::Base58Btc.code())
format!(
"{} ('{}')",
base_name(Base::Base58Btc),
Base::Base58Btc.code()
)
}
fn preferred_encoding(_base: Base) -> Base {
Base::Base58Btc
}
}

/// 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
/// 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.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DetectedEncoder {}

impl BaseEncoder for DetectedEncoder {
fn to_base_encoded(base: Base, b: &[u8]) -> String {
multibase::encode(base, b)
multi_base::encode(base, b)
}
fn from_base_encoded(s: &str) -> Result<Vec<(Base, Vec<u8>)>, Error> {
// first try permissive multibase decoding
if let Ok((base, data)) = multibase::decode(s, false) {
if let Ok((base, data)) = multi_base::decode(s, false) {
return Ok(vec![(base, data)]);
}

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

Expand Down
8 changes: 4 additions & 4 deletions src/base_util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::prelude::Base;

/// convert a multibase Base to its string equivalent
Expand Down Expand Up @@ -87,7 +87,7 @@ impl Iterator for BaseIter {
Base64Url => Base64UrlPad,
Base64UrlPad => Base256Emoji,
Base256Emoji => return None,
}
},
};
self.0 = Some(result);
Some(result)
Expand All @@ -111,8 +111,8 @@ mod tests {

#[test]
fn test_all_iter() {
let mut iter = BaseIter::new();
while let Some(b) = iter.next() {
let iter = BaseIter::new();
for b in iter {
println!("{}", base_name(b));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/codec_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::prelude::Codec;

/// This trait exposes the codec information for multicoded types
Expand Down
2 changes: 1 addition & 1 deletion src/encoding_info.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::prelude::Base;

/// This trait exposes the preferred encoding for this multicodec type
Expand Down
35 changes: 30 additions & 5 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
/// Errors generated by the numeric type impls
#[derive(Clone, Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Multitrait decode error
#[error(transparent)]
Multitrait(#[from] multitrait::Error),
Multitrait(#[from] multi_trait::Error),
/// Multicodec decode error
#[error(transparent)]
Multicodec(#[from] multicodec::Error),
Multicodec(#[from] multi_codec::Error),
/// BaseEncoded error
#[error(transparent)]
BaseEncoded(#[from] BaseEncodedError),
Expand All @@ -18,6 +18,31 @@ pub enum Error {
/// Custom error for inner types to use when nothing else works
#[error("Custom error: {0}")]
Custom(String),
/// Insufficient data available for the claimed length
///
/// This error occurs when a varint length claim exceeds the available buffer size.
/// This prevents buffer overflow attacks where an attacker claims a large length
/// but provides minimal data.
///
/// # Security
///
/// This validation prevents CWE-125 (Out-of-bounds Read) vulnerabilities.
///
/// # Example
///
/// ```rust
/// use multi_util::Error;
///
/// // This would trigger InsufficientData:
/// // Varint claims 1000 bytes but only 3 bytes available
/// ```
#[error("insufficient data: expected {expected} bytes, but only {actual} bytes available")]
InsufficientData {
/// The number of bytes claimed by the length prefix
expected: usize,
/// The actual number of bytes available in the buffer
actual: usize,
},
}

impl Error {
Expand Down Expand Up @@ -45,7 +70,7 @@ pub enum BaseEncodedError {
pub enum BaseEncoderError {
/// Multibase decode error
#[error(transparent)]
Multibase(#[from] multibase::Error),
Multibase(#[from] multi_base::Error),

/// Base58 decode error
#[error("Base58 error: {0}")]
Expand Down
14 changes: 7 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
//! multiutil
#![warn(missing_docs)]
#![deny(
Expand Down Expand Up @@ -36,7 +36,7 @@ pub use error::Error;
#[cfg(feature = "serde")]
pub mod serde;

/// Varbytes type for forcing serde of Vec<u8> to/from bytes
/// Varbytes type for forcing serde of `Vec<u8>` to/from bytes
pub mod varbytes;
pub use varbytes::{EncodedVarbytes, Varbytes};

Expand All @@ -52,8 +52,8 @@ pub mod prelude {
};

/// re-exports
pub use multibase::Base;
pub use multicodec::Codec;
pub use multi_base::Base;
pub use multi_codec::Codec;
}

#[cfg(test)]
Expand Down Expand Up @@ -118,10 +118,10 @@ mod test {
}
}

impl Into<Vec<u8>> for Unit {
fn into(self) -> Vec<u8> {
impl From<Unit> for Vec<u8> {
fn from(unit: Unit) -> Vec<u8> {
let mut v = Vec::default();
v.extend_from_slice(&self.0);
v.extend_from_slice(&unit.0);
v
}
}
Expand Down
21 changes: 10 additions & 11 deletions src/serde/de.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
use crate::{BaseEncoded, BaseEncoder, EncodingInfo, Varbytes, Varuint};
use core::{fmt, marker};
use multibase::Base;
use multitrait::prelude::TryDecodeFrom;
use multi_base::Base;
use multi_trait::prelude::TryDecodeFrom;
use serde::de;

/// Deserialize instance of [`crate::BaseEncoded`] from a byte slice
impl<'de, T, Enc> de::Deserialize<'de> for BaseEncoded<T, Enc>
where
T: de::Deserialize<'de> + EncodingInfo + for<'a> TryFrom<&'a [u8]> + ?Sized,
T: de::Deserialize<'de> + EncodingInfo + for<'a> TryFrom<&'a [u8]>,
Enc: BaseEncoder,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
Expand All @@ -23,7 +23,7 @@ where

impl<'de, T, Enc> de::Visitor<'de> for BaseEncodedVisitor<T, Enc>
where
T: de::Deserialize<'de> + EncodingInfo + for<'a> TryFrom<&'a [u8]> + ?Sized,
T: de::Deserialize<'de> + EncodingInfo + for<'a> TryFrom<&'a [u8]>,
Enc: BaseEncoder,
{
type Value = BaseEncoded<T, Enc>;
Expand Down Expand Up @@ -57,8 +57,7 @@ where
where
E: de::Error,
{
Self::Value::try_from(s.as_str())
.map_err(|e| de::Error::custom(e.to_string()))
Self::Value::try_from(s.as_str()).map_err(|e| de::Error::custom(e.to_string()))
}

// binary
Expand Down Expand Up @@ -198,7 +197,7 @@ impl<'de> de::Deserialize<'de> for Varbytes {
let (len, ptr) = usize::try_decode_from(v)
.map_err(|_| de::Error::custom("failed to deserialize varuint len"))?;
let v = ptr[..len].to_vec();
Ok(Varbytes(v))
Ok(Varbytes::new(v))
}

#[inline]
Expand All @@ -209,7 +208,7 @@ impl<'de> de::Deserialize<'de> for Varbytes {
let (len, ptr) = usize::try_decode_from(v)
.map_err(|_| de::Error::custom("failed to deserialize varuint len"))?;
let v = ptr[..len].to_vec();
Ok(Varbytes(v))
Ok(Varbytes::new(v))
}

// longest lifetime
Expand All @@ -221,7 +220,7 @@ impl<'de> de::Deserialize<'de> for Varbytes {
let (len, ptr) = usize::try_decode_from(v.as_slice())
.map_err(|_| de::Error::custom("failed to deserialize varuint len"))?;
let v = ptr[..len].to_vec();
Ok(Varbytes(v))
Ok(Varbytes::new(v))
}

// binary / human readable
Expand All @@ -240,7 +239,7 @@ impl<'de> de::Deserialize<'de> for Varbytes {
let (len, ptr) = usize::try_decode_from(v.as_slice())
.map_err(|_| de::Error::custom("failed to deserialize varuint len"))?;
let v = ptr[..len].to_vec();
Ok(Varbytes(v))
Ok(Varbytes::new(v))
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/serde/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Idnetifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
//! Serde (de)serialization for ['crate::prelude::Tagged'] wrapped objects
mod de;
mod ser;
Expand Down Expand Up @@ -48,11 +48,11 @@ mod tests {
}
}

impl Into<Vec<u8>> for Unit {
fn into(self) -> Vec<u8> {
impl From<Unit> for Vec<u8> {
fn from(unit: Unit) -> Vec<u8> {
let mut v: Vec<u8> = Vec::default();
v.push(self.0 .0);
v.extend_from_slice(&self.0 .1);
v.push(unit.0 .0);
v.extend_from_slice(&unit.0 .1);
v
}
}
Expand Down Expand Up @@ -276,7 +276,7 @@ mod tests {

#[test]
fn test_varbytes() {
let v = Varbytes(vec![0x01, 0x02, 0x03]);
let v = Varbytes::new(vec![0x01, 0x02, 0x03]);
assert_tokens(&v, &[Token::Bytes(&[0x03, 0x01, 0x02, 0x03])]);
}

Expand Down
Loading
Loading