From 46b5e8e39e65a24bc091ec8d78ca282757632745 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 13:20:21 -0600 Subject: [PATCH 1/3] update from bettersign Signed-off-by: Dave Grantham --- CHANGELOG.md | 13 +++ Cargo.toml | 20 ++-- src/base_encoded.rs | 4 +- src/base_encoder.rs | 29 +++-- src/base_util.rs | 8 +- src/codec_info.rs | 2 +- src/encoding_info.rs | 2 +- src/error.rs | 35 +++++- src/lib.rs | 14 +-- src/serde/de.rs | 21 ++-- src/serde/mod.rs | 12 +- src/serde/ser.rs | 10 +- src/varbytes.rs | 268 +++++++++++++++++++++++++++++++++++++++++-- src/varuint.rs | 6 +- 14 files changed, 370 insertions(+), 74 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ba1ce6a --- /dev/null +++ b/CHANGELOG.md @@ -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` \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d4bb5c4..96a8fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,29 @@ [package] -name = "multiutil" -version = "1.0.15" +name = "multi-util" +version = "1.0.0" edition = "2021" authors = ["Dave Huseby "] 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 = { version = "1.0", path = "../multi-base" } +multi-codec = { version = "1.0", path = "../multi-codec" } +multi-trait = { version = "1.0", path = "../multi-trait" } 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" +serde_test = "1.0" \ No newline at end of file diff --git a/src/base_encoded.rs b/src/base_encoded.rs index 26b35cc..f1edcf8 100644 --- a/src/base_encoded.rs +++ b/src/base_encoded.rs @@ -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, }; @@ -88,7 +88,7 @@ where base, t, enc: PhantomData, - }) + }); } } Err(BaseEncodedError::ValueFailed.into()) diff --git a/src/base_encoder.rs b/src/base_encoder.rs index 4e60c0f..c2a5d96 100644 --- a/src/base_encoder.rs +++ b/src/base_encoder.rs @@ -1,10 +1,9 @@ -// 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 @@ -12,7 +11,7 @@ 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 + /// convert a base encoded value to a `Vec` fn from_base_encoded(s: &str) -> Result)>, Error>; /// get the debug string for the given base @@ -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)>, 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()) @@ -58,7 +59,11 @@ 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 @@ -66,22 +71,22 @@ 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 +/// 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)>, 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(); diff --git a/src/base_util.rs b/src/base_util.rs index 155eb69..e4cf093 100644 --- a/src/base_util.rs +++ b/src/base_util.rs @@ -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 @@ -87,7 +87,7 @@ impl Iterator for BaseIter { Base64Url => Base64UrlPad, Base64UrlPad => Base256Emoji, Base256Emoji => return None, - } + }, }; self.0 = Some(result); Some(result) @@ -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)); } } diff --git a/src/codec_info.rs b/src/codec_info.rs index 2bdb885..990e4c3 100644 --- a/src/codec_info.rs +++ b/src/codec_info.rs @@ -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 diff --git a/src/encoding_info.rs b/src/encoding_info.rs index 3929b2b..d58d23f 100644 --- a/src/encoding_info.rs +++ b/src/encoding_info.rs @@ -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 diff --git a/src/error.rs b/src/error.rs index f97f25e..892513e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), @@ -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 { @@ -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}")] diff --git a/src/lib.rs b/src/lib.rs index 4297b26..cd4e2ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 //! multiutil #![warn(missing_docs)] #![deny( @@ -36,7 +36,7 @@ pub use error::Error; #[cfg(feature = "serde")] pub mod serde; -/// Varbytes type for forcing serde of Vec to/from bytes +/// Varbytes type for forcing serde of `Vec` to/from bytes pub mod varbytes; pub use varbytes::{EncodedVarbytes, Varbytes}; @@ -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)] @@ -118,10 +118,10 @@ mod test { } } - impl Into> for Unit { - fn into(self) -> Vec { + impl From for Vec { + fn from(unit: Unit) -> Vec { let mut v = Vec::default(); - v.extend_from_slice(&self.0); + v.extend_from_slice(&unit.0); v } } diff --git a/src/serde/de.rs b/src/serde/de.rs index 30c4e57..9e2594e 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,14 +1,14 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, BaseEncoder, EncodingInfo, Varbytes, Varuint}; +use multi_base::Base; +use multi_trait::prelude::TryDecodeFrom; use core::{fmt, marker}; -use multibase::Base; -use multitrait::prelude::TryDecodeFrom; use serde::de; /// Deserialize instance of [`crate::BaseEncoded`] from a byte slice impl<'de, T, Enc> de::Deserialize<'de> for BaseEncoded 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(deserializer: D) -> Result @@ -23,7 +23,7 @@ where impl<'de, T, Enc> de::Visitor<'de> for BaseEncodedVisitor 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; @@ -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 @@ -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] @@ -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 @@ -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 @@ -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)) } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index f033dc9..fbf5c2d 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -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; @@ -48,11 +48,11 @@ mod tests { } } - impl Into> for Unit { - fn into(self) -> Vec { + impl From for Vec { + fn from(unit: Unit) -> Vec { let mut v: Vec = 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 } } @@ -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])]); } diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 67e3dba..f49602a 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,12 +1,12 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, BaseEncoder, EncodingInfo, Varbytes, Varuint}; -use multitrait::prelude::EncodeInto; +use multi_trait::prelude::EncodeInto; use serde::ser; /// Serialize instance of [`crate::BaseEncoded`] into impl ser::Serialize for BaseEncoded where - T: ser::Serialize + EncodingInfo + Clone + Into> + ?Sized, + T: ser::Serialize + EncodingInfo + Clone + Into>, Enc: BaseEncoder, { fn serialize(&self, serializer: S) -> Result @@ -40,8 +40,8 @@ impl ser::Serialize for Varbytes { where S: ser::Serializer, { - let mut v = self.0.len().encode_into(); - v.append(&mut self.0.clone()); + let mut v = self.as_bytes().len().encode_into(); + v.append(&mut self.as_bytes().to_vec()); serializer.serialize_bytes(v.as_slice()) } } diff --git a/src/varbytes.rs b/src/varbytes.rs index ad745fd..a8aa912 100644 --- a/src/varbytes.rs +++ b/src/varbytes.rs @@ -1,20 +1,35 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, EncodingInfo, Error}; +use multi_base::Base; +use multi_trait::prelude::{EncodeInto, TryDecodeFrom}; use core::{fmt, ops}; -use multibase::Base; -use multitrait::prelude::{EncodeInto, TryDecodeFrom}; /// A wrapper type to handle serde of byte arrays as bytes #[derive(Clone, Default, PartialEq)] -pub struct Varbytes(pub Vec); +pub struct Varbytes(Vec); /// type alias for a Varbytes base encoded to/from string pub type EncodedVarbytes = BaseEncoded; impl Varbytes { + /// Create a new Varbytes from a Vec + pub fn new(data: Vec) -> Self { + Self(data) + } + /// create an encoded varbytes pub fn encoded_new(base: Base, v: Vec) -> EncodedVarbytes { - BaseEncoded::new(base, Varbytes(v)) + BaseEncoded::new(base, Varbytes::new(v)) + } + + /// Get a reference to the inner byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get a mutable reference to the inner byte vector + pub fn as_bytes_mut(&mut self) -> &mut Vec { + &mut self.0 } /// consume self and return inner vec @@ -76,6 +91,16 @@ impl<'a> TryDecodeFrom<'a> for Varbytes { fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { let (len, ptr) = usize::try_decode_from(bytes)?; + + // Validate buffer has enough data for claimed length + // This prevents buffer overflow (CWE-125) when length claim exceeds available data + if len > ptr.len() { + return Err(Error::InsufficientData { + expected: len, + actual: ptr.len(), + }); + } + let v = ptr[..len].to_vec(); let ptr = &ptr[len..]; Ok((Self(v), ptr)) @@ -94,7 +119,7 @@ mod test { #[test] fn test_to_inner() { - let v = Varbytes(vec![1, 2, 3]); + let v = Varbytes::new(vec![1, 2, 3]); assert_eq!(vec![1, 2, 3], v.to_inner()); } @@ -108,14 +133,14 @@ mod test { #[test] fn test_encode_decode_round_trip() { - let v1 = Varbytes(vec![1, 2, 3]); + let v1 = Varbytes::new(vec![1, 2, 3]); let (v2, _) = Varbytes::try_decode_from(&v1.encode_into()).unwrap(); assert_eq!(v1, v2); } #[test] fn test_into_tryfrom_round_trip() { - let v1 = Varbytes(vec![1, 2, 3]); + let v1 = Varbytes::new(vec![1, 2, 3]); let data: Vec = v1.clone().into(); let v2 = Varbytes::try_from(data.as_slice()).unwrap(); assert_eq!(v1, v2); @@ -123,7 +148,232 @@ mod test { #[test] fn test_debug() { - let v = Varbytes(vec![1, 2, 3]); + let v = Varbytes::new(vec![1, 2, 3]); assert_eq!("[3, 1, 2, 3]".to_string(), format!("{:?}", v)); } + + // ============================================================================ + // SECURITY TESTS - CRIT-1: Buffer Overflow Prevention + // ============================================================================ + + #[test] + fn test_crit1_buffer_overflow_prevented() { + // CRIT-1: Test that buffer overflow vulnerability is fixed + // + // Attack scenario: Attacker crafts input with large length claim + // but provides minimal actual data, attempting to trigger out-of-bounds read + + use multi_trait::EncodeInto; + + // Create malicious input: claims 4GB length but only has 3 bytes + let mut malicious = Vec::new(); + + // Encode a huge length (0xFFFFFFFF = ~4GB) + let huge_length = 0xFFFF_FFFF_usize; + malicious.extend(huge_length.encode_into()); + + // Provide only 3 bytes of actual data + malicious.extend(&[0x01, 0x02, 0x03]); + + // Attempt to decode - should fail, not panic + let result = Varbytes::try_decode_from(&malicious); + + assert!( + result.is_err(), + "Should reject length claim that exceeds available data" + ); + + // Verify correct error type + match result.unwrap_err() { + Error::InsufficientData { expected, actual } => { + assert_eq!(expected, huge_length); + assert_eq!(actual, 3); + } + e => panic!("Expected InsufficientData error, got: {:?}", e), + } + } + + #[test] + fn test_crit1_regression_zero_length() { + // Regression test: zero-length varbytes should work + use multi_trait::EncodeInto; + + let encoded = 0usize.encode_into(); + let result = Varbytes::try_decode_from(&encoded); + + assert!(result.is_ok(), "Zero-length varbytes should be valid"); + + let (varbytes, remaining) = result.unwrap(); + assert_eq!(varbytes.to_inner(), Vec::::new()); + assert!(remaining.is_empty()); + } + + #[test] + fn test_crit1_regression_exact_length() { + // Regression test: exact length match should work + use multi_trait::EncodeInto; + + let data = vec![0xAA, 0xBB, 0xCC]; + let mut encoded = data.len().encode_into(); + encoded.extend(&data); + + let result = Varbytes::try_decode_from(&encoded); + + assert!(result.is_ok(), "Exact length match should succeed"); + + let (varbytes, remaining) = result.unwrap(); + assert_eq!(varbytes.to_inner(), data); + assert!(remaining.is_empty()); + } + + #[test] + fn test_crit1_one_byte_over() { + // Boundary test: length claim exceeds buffer by 1 byte + use multi_trait::EncodeInto; + + let mut malicious = Vec::new(); + + // Claim 4 bytes + malicious.extend(4usize.encode_into()); + + // Provide only 3 bytes + malicious.extend(&[0x01, 0x02, 0x03]); + + let result = Varbytes::try_decode_from(&malicious); + + assert!( + result.is_err(), + "Should reject length claim exceeding buffer by 1" + ); + + match result.unwrap_err() { + Error::InsufficientData { expected, actual } => { + assert_eq!(expected, 4); + assert_eq!(actual, 3); + } + _ => panic!("Expected InsufficientData error"), + } + } + + #[test] + fn test_crit1_empty_buffer_nonzero_length() { + // Edge case: length > 0 but no data provided + use multi_trait::EncodeInto; + + let mut malicious = Vec::new(); + + // Claim 100 bytes + malicious.extend(100usize.encode_into()); + + // Provide no data + // (nothing appended) + + let result = Varbytes::try_decode_from(&malicious); + + assert!(result.is_err(), "Should reject nonzero length with no data"); + } + + #[test] + fn test_crit1_legitimate_large_data() { + // Verify legitimate large data still works + use multi_trait::EncodeInto; + + // Create 1MB of legitimate data + let large_data = vec![0x42; 1024 * 1024]; + let mut encoded = large_data.len().encode_into(); + encoded.extend(&large_data); + + let result = Varbytes::try_decode_from(&encoded); + + assert!(result.is_ok(), "Legitimate large data should be accepted"); + + let (varbytes, remaining) = result.unwrap(); + let inner = varbytes.to_inner(); + assert_eq!(inner.len(), 1024 * 1024); + assert_eq!(inner[0], 0x42); + assert!(remaining.is_empty()); + } + + #[test] + fn test_crit1_with_trailing_data() { + // Test that remaining bytes are correctly returned + use multi_trait::EncodeInto; + + let data = vec![0xAA, 0xBB]; + let trailing = vec![0xCC, 0xDD, 0xEE]; + + let mut encoded = data.len().encode_into(); + encoded.extend(&data); + encoded.extend(&trailing); + + let result = Varbytes::try_decode_from(&encoded); + + assert!(result.is_ok()); + + let (varbytes, remaining) = result.unwrap(); + assert_eq!(varbytes.to_inner(), data); + assert_eq!(remaining, trailing.as_slice()); + } + + // ============================================================================ + // PROPERTY-BASED TESTS - CRIT-1 + // ============================================================================ + + #[cfg(test)] + use proptest::prelude::*; + + proptest! { + #[test] + fn prop_crit1_never_panics_on_random_input( + bytes in prop::collection::vec(any::(), 0..1000) + ) { + // Most important property: should never panic on any input + let _ = Varbytes::try_decode_from(&bytes); + // Should either succeed or return error, never panic + } + + #[test] + fn prop_crit1_valid_roundtrip( + data in prop::collection::vec(any::(), 0..10000) + ) { + // Valid varbytes should roundtrip correctly + let varbytes = Varbytes::new(data.clone()); + let encoded = varbytes.encode_into(); + let result = Varbytes::try_decode_from(&encoded); + + prop_assert!(result.is_ok()); + + let (decoded, remaining) = result.unwrap(); + prop_assert_eq!(decoded.to_inner(), data); + prop_assert!(remaining.is_empty()); + } + + #[test] + fn prop_crit1_length_mismatch_detected( + claimed_len in 1usize..1000, + actual_len in 0usize..100 + ) { + use multi_trait::EncodeInto; + + // Only test cases where claim exceeds actual + if claimed_len > actual_len { + let mut malicious = Vec::new(); + malicious.extend(claimed_len.encode_into()); + malicious.extend(vec![0u8; actual_len]); + + let result = Varbytes::try_decode_from(&malicious); + + // Should always reject length claim > available data + prop_assert!(result.is_err()); + + match result.unwrap_err() { + Error::InsufficientData { expected, actual } => { + prop_assert_eq!(expected, claimed_len); + prop_assert_eq!(actual, actual_len); + } + e => return Err(TestCaseError::fail(format!("Wrong error type: {:?}", e))), + } + } + } + } } diff --git a/src/varuint.rs b/src/varuint.rs index 208cd67..8f7b573 100644 --- a/src/varuint.rs +++ b/src/varuint.rs @@ -1,8 +1,8 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, EncodingInfo, Error}; +use multi_base::Base; +use multi_trait::{EncodeInto, TryDecodeFrom}; use core::{fmt, ops}; -use multibase::Base; -use multitrait::{EncodeInto, TryDecodeFrom}; /// A wrapper type to handle serde of numeric types as varuint bytes #[derive(Clone, PartialEq)] From fe1151ba6e8b757497de8d2139a7f9109f47ebe5 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 15:56:27 -0600 Subject: [PATCH 2/3] fix codecs Signed-off-by: Dave Grantham --- Cargo.toml | 4 ++-- src/serde/de.rs | 2 +- src/varbytes.rs | 2 +- src/varuint.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96a8fd4..22fbe0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-util" -version = "1.0.0" +version = "1.0.1" edition = "2021" authors = ["Dave Huseby "] description = "Multiformat utility functions and types" @@ -26,4 +26,4 @@ hex = "0.4" proptest = "1.4" serde_cbor = "0.11" serde_json = "1.0" -serde_test = "1.0" \ No newline at end of file +serde_test = "1.0" diff --git a/src/serde/de.rs b/src/serde/de.rs index 9e2594e..e58d184 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, BaseEncoder, EncodingInfo, Varbytes, Varuint}; +use core::{fmt, marker}; use multi_base::Base; use multi_trait::prelude::TryDecodeFrom; -use core::{fmt, marker}; use serde::de; /// Deserialize instance of [`crate::BaseEncoded`] from a byte slice diff --git a/src/varbytes.rs b/src/varbytes.rs index a8aa912..d61204c 100644 --- a/src/varbytes.rs +++ b/src/varbytes.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, EncodingInfo, Error}; +use core::{fmt, ops}; use multi_base::Base; use multi_trait::prelude::{EncodeInto, TryDecodeFrom}; -use core::{fmt, ops}; /// A wrapper type to handle serde of byte arrays as bytes #[derive(Clone, Default, PartialEq)] diff --git a/src/varuint.rs b/src/varuint.rs index 8f7b573..964b17a 100644 --- a/src/varuint.rs +++ b/src/varuint.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{BaseEncoded, EncodingInfo, Error}; +use core::{fmt, ops}; use multi_base::Base; use multi_trait::{EncodeInto, TryDecodeFrom}; -use core::{fmt, ops}; /// A wrapper type to handle serde of numeric types as varuint bytes #[derive(Clone, PartialEq)] From 43f1f76b4fb678135435d87bb994a18eed4db15a Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 16:06:42 -0600 Subject: [PATCH 3/3] update deps Signed-off-by: Dave Grantham --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22fbe0f..88c983f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-util" -version = "1.0.1" +version = "1.0.2" edition = "2021" authors = ["Dave Huseby "] description = "Multiformat utility functions and types" @@ -14,9 +14,9 @@ categories = ["encoding"] default = ["serde"] [dependencies] -multi-base = { version = "1.0", path = "../multi-base" } -multi-codec = { version = "1.0", path = "../multi-codec" } -multi-trait = { version = "1.0", path = "../multi-trait" } +multi-base = "1.0" +multi-codec = "1.0" +multi-trait = "1.0" serde = { version = "1.0", default-features = false, features = ["alloc"], optional = true } thiserror = { version = "2.0" }