diff --git a/minicbor-serde/src/de.rs b/minicbor-serde/src/de.rs index d8fa63e..c69797d 100644 --- a/minicbor-serde/src/de.rs +++ b/minicbor-serde/src/de.rs @@ -3,7 +3,7 @@ use serde::de::{ value::{BorrowedStrDeserializer, U64Deserializer}, }; -use minicbor::data::{Tag, Type}; +use minicbor::data::{IanaTag, Tag, Type}; use minicbor::decode::{Decoder, Error}; use crate::{ @@ -121,10 +121,22 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { t @ (Type::BytesIndef | Type::StringIndef) => Err(Error::type_mismatch(t).with_message("unexpected type").at(self.decoder.position()).into()), + Type::Int => self.deserialize_i128(visitor), + + Type::Tag => { + let tag = self.decoder.probe().tag()?; + match tag.try_into() { + Ok(IanaTag::PosBignum) => self.deserialize_u128(visitor), + Ok(IanaTag::NegBignum) => self.deserialize_i128(visitor), + _ => Err(Error::type_mismatch(Type::Tag) + .with_message("unexpected type") + .at(self.decoder.position()) + .into()) + } + } + t @ ( | Type::Undefined - | Type::Tag - | Type::Int | Type::Simple | Type::Break | Type::Unknown(_) @@ -168,6 +180,14 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> { visitor.visit_u64(self.decoder.u64()?) } + fn deserialize_i128>(self, visitor: V) -> Result { + visitor.visit_i128(self.decoder.i128()?) + } + + fn deserialize_u128>(self, visitor: V) -> Result { + visitor.visit_u128(self.decoder.u128()?) + } + fn deserialize_f32>(self, visitor: V) -> Result { visitor.visit_f32(self.decoder.f32()?) } diff --git a/minicbor-serde/src/ser.rs b/minicbor-serde/src/ser.rs index a64042e..12ee4d0 100644 --- a/minicbor-serde/src/ser.rs +++ b/minicbor-serde/src/ser.rs @@ -115,6 +115,16 @@ where Ok(()) } + fn serialize_i128(self, v: i128) -> Result { + self.encoder.i128(v)?; + Ok(()) + } + + fn serialize_u128(self, v: u128) -> Result { + self.encoder.u128(v)?; + Ok(()) + } + fn serialize_f32(self, v: f32) -> Result { self.encoder.f32(v)?; Ok(()) diff --git a/minicbor-serde/tests/i128.rs b/minicbor-serde/tests/i128.rs new file mode 100644 index 0000000..f0ae1e6 --- /dev/null +++ b/minicbor-serde/tests/i128.rs @@ -0,0 +1,113 @@ +#![cfg(feature = "alloc")] + +use serde::{Deserialize, Serialize}; + +fn roundtrip_u128(n: u128) { + let v = minicbor_serde::to_vec(n).unwrap(); + let m: u128 = minicbor_serde::from_slice(&v).unwrap(); + assert_eq!(n, m, "u128 round-trip failed for {n}"); + + // Wire format must match the raw `minicbor` encoding. + let direct = minicbor::to_vec(n).unwrap(); + assert_eq!(v, direct, "serde encoding diverges from minicbor for u128 {n}"); +} + +fn roundtrip_i128(n: i128) { + let v = minicbor_serde::to_vec(n).unwrap(); + let m: i128 = minicbor_serde::from_slice(&v).unwrap(); + assert_eq!(n, m, "i128 round-trip failed for {n}"); + + let direct = minicbor::to_vec(n).unwrap(); + assert_eq!(v, direct, "serde encoding diverges from minicbor for i128 {n}"); +} + +#[test] +fn u128_roundtrip() { + roundtrip_u128(0); + roundtrip_u128(1); + roundtrip_u128(u64::MAX as u128); + roundtrip_u128(u64::MAX as u128 + 1); + roundtrip_u128(u128::MAX); +} + +#[test] +fn i128_roundtrip() { + roundtrip_i128(0); + roundtrip_i128(1); + roundtrip_i128(-1); + roundtrip_i128(i64::MIN as i128); + roundtrip_i128(i64::MAX as i128); + roundtrip_i128(u64::MAX as i128); + roundtrip_i128(-(u64::MAX as i128) - 1); + roundtrip_i128(u64::MAX as i128 + 1); + roundtrip_i128(-(u64::MAX as i128) - 2); + roundtrip_i128(i128::MAX); + roundtrip_i128(i128::MIN); +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct Wrap { + u: u128, + i: i128, +} + +#[test] +fn struct_with_128_fields() { + let w = Wrap { u: u128::MAX, i: i128::MIN }; + let v = minicbor_serde::to_vec(&w).unwrap(); + let r: Wrap = minicbor_serde::from_slice(&v).unwrap(); + assert_eq!(w, r); +} + +#[test] +fn deserialize_any_routes_bignums() { + // Custom value type that calls `deserialize_any` and supports the full + // 128-bit range. (Serde's built-in `Content` buffer used by untagged enums + // only stores up to 64 bits — see serde-rs/serde#2912 — so we can't test + // bignum dispatch through an untagged enum here.) + use serde::de; + use std::fmt; + + #[derive(Debug, PartialEq)] + enum Any { + U128(u128), + I128(i128), + } + + struct AnyVisitor; + impl<'de> de::Visitor<'de> for AnyVisitor { + type Value = Any; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a 128-bit integer") + } + + fn visit_u128(self, v: u128) -> Result { Ok(Any::U128(v)) } + fn visit_i128(self, v: i128) -> Result { Ok(Any::I128(v)) } + fn visit_u64(self, v: u64) -> Result { Ok(Any::U128(u128::from(v))) } + fn visit_i64(self, v: i64) -> Result { Ok(Any::I128(i128::from(v))) } + fn visit_u32(self, v: u32) -> Result { Ok(Any::U128(u128::from(v))) } + fn visit_u16(self, v: u16) -> Result { Ok(Any::U128(u128::from(v))) } + fn visit_u8(self, v: u8) -> Result { Ok(Any::U128(u128::from(v))) } + } + + impl<'de> Deserialize<'de> for Any { + fn deserialize>(d: D) -> Result { + d.deserialize_any(AnyVisitor) + } + } + + // Large positive: encoded as positive bignum, deserialize_any must route to visit_u128. + let n = u64::MAX as u128 + 12345; + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(Any::U128(n), minicbor_serde::from_slice::(&v).unwrap()); + + // Large negative: encoded as negative bignum, deserialize_any must route to visit_i128. + let n: i128 = -(u64::MAX as i128) - 100; + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(Any::I128(n), minicbor_serde::from_slice::(&v).unwrap()); + + // Small unsigned: encoded as native CBOR u8, must reach visit_u8/u64. + let v = minicbor::to_vec(7u128).unwrap(); + assert_eq!(Any::U128(7), minicbor_serde::from_slice::(&v).unwrap()); +} diff --git a/minicbor-tests/tests/identities.rs b/minicbor-tests/tests/identities.rs index 73264b7..7d84677 100644 --- a/minicbor-tests/tests/identities.rs +++ b/minicbor-tests/tests/identities.rs @@ -58,6 +58,16 @@ fn i64() { quickcheck(identity as fn(i64) -> bool) } +#[test] +fn u128() { + quickcheck(identity as fn(u128) -> bool) +} + +#[test] +fn i128() { + quickcheck(identity as fn(i128) -> bool) +} + #[test] fn int() { assert!(identity(Int::try_from(-2_i128.pow(64)).unwrap())); @@ -87,6 +97,23 @@ fn nonzero_u64() { quickcheck(identity as fn(core::num::NonZeroU64) -> bool) } +#[test] +fn nonzero_u128() { + quickcheck(identity as fn(core::num::NonZeroU128) -> bool) +} + +#[test] +fn nonzero_i128() { + // quickcheck has no `Arbitrary` impl for `NonZeroI128`, so combine two `i64`s + // to cover the full `i128` range (including the bignum path). + fn property(hi: i64, lo: u64) -> bool { + let raw = (i128::from(hi) << 64) | i128::from(lo); + let n = core::num::NonZeroI128::new(raw).unwrap_or(core::num::NonZeroI128::new(1).unwrap()); + identity(n) + } + quickcheck(property as fn(i64, u64) -> bool) +} + #[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))] #[test] fn nonzero_usize() { diff --git a/minicbor-tests/tests/int.rs b/minicbor-tests/tests/int.rs index 90b2456..54921b2 100644 --- a/minicbor-tests/tests/int.rs +++ b/minicbor-tests/tests/int.rs @@ -143,4 +143,119 @@ quickcheck! { let j = minicbor::decode::(&v).unwrap(); TestResult::from_bool(n == i128::from(j)) } + + fn u128_id(n: u128) -> bool { + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(minicbor::len(n), v.len()); + let m = minicbor::decode::(&v).unwrap(); + n == m + } + + fn i128_id(n: i128) -> bool { + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(minicbor::len(n), v.len()); + let m = minicbor::decode::(&v).unwrap(); + n == m + } + + fn u128_compat_u64(n: u64) -> bool { + // u64 encoded values must round-trip through u128 decoding. + let v = minicbor::to_vec(n).unwrap(); + let m = minicbor::decode::(&v).unwrap(); + u128::from(n) == m + } + + fn i128_compat_i64(n: i64) -> bool { + let v = minicbor::to_vec(n).unwrap(); + let m = minicbor::decode::(&v).unwrap(); + i128::from(n) == m + } +} + +#[test] +fn u128_edges() { + fn roundtrip(n: u128) { + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(minicbor::len(n), v.len()); + assert_eq!(n, minicbor::decode::(&v).unwrap()); + } + roundtrip(0); + roundtrip(1); + roundtrip(u64::MAX as u128); + roundtrip(u64::MAX as u128 + 1); + roundtrip(u128::MAX); +} + +#[test] +fn i128_edges() { + fn roundtrip(n: i128) { + let v = minicbor::to_vec(n).unwrap(); + assert_eq!(minicbor::len(n), v.len()); + assert_eq!(n, minicbor::decode::(&v).unwrap()); + } + roundtrip(0); + roundtrip(1); + roundtrip(-1); + roundtrip(i64::MAX as i128); + roundtrip(i64::MIN as i128); + roundtrip(u64::MAX as i128); + roundtrip(-(u64::MAX as i128) - 1); // -2^64, lowest Int-representable value + roundtrip(u64::MAX as i128 + 1); // first value requiring bignum + roundtrip(-(u64::MAX as i128) - 2); // first negative value requiring bignum + roundtrip(i128::MAX); + roundtrip(i128::MIN); +} + +#[test] +fn u128_bignum_encoding() { + // u128::MAX -> tag 2 (0xc2), bytes(16) -> 0x50, then 16 x 0xff + let v = minicbor::to_vec(u128::MAX).unwrap(); + assert_eq!(v[0], 0xc2); + assert_eq!(v[1], 0x50); + assert_eq!(&v[2..], &[0xff; 16]); +} + +#[test] +fn i128_negative_bignum_encoding() { + // i128::MIN = -2^127. -1 - i128::MIN = 2^127 - 1. + // Encoded as tag 3 (0xc3) + bstr(16) + big-endian bytes of (2^127 - 1). + let v = minicbor::to_vec(i128::MIN).unwrap(); + assert_eq!(v[0], 0xc3); + assert_eq!(v[1], 0x50); + let mut expected = [0xff_u8; 16]; + expected[0] = 0x7f; + assert_eq!(&v[2..], &expected); +} + +#[test] +fn u128_decodes_with_leading_zero_bytes() { + // Non-canonical encoding: tag 2 + bstr containing 16 bytes, first three zero. + let mut v = vec![0xc2, 0x50, 0x00, 0x00, 0x00]; + v.extend_from_slice(&[0xab; 13]); + let n = minicbor::decode::(&v).unwrap(); + let mut expected = [0_u8; 16]; + expected[3..].copy_from_slice(&[0xab; 13]); + assert_eq!(n, u128::from_be_bytes(expected)); +} + +#[test] +fn u128_rejects_negative_bignum() { + let v = minicbor::to_vec(-1i128).unwrap(); + assert_eq!(v[0], 0x20); // -1 encodes as native signed, not bignum + // Force-construct a negative bignum and confirm u128 rejects it. + let v = vec![0xc3, 0x42, 0x01, 0x00]; + assert!(minicbor::decode::(&v).is_err()); +} + +#[test] +fn i128_rejects_overflowing_positive_bignum() { + // 17-byte bignum, all 0xff, overflows u128 capacity. + let mut v = vec![0xc2, 0x51]; + v.extend_from_slice(&[0xff; 17]); + assert!(minicbor::decode::(&v).is_err()); + // 16-byte positive bignum equal to 2^127 must overflow i128. + let mut v = vec![0xc2, 0x50]; + v.push(0x80); + v.extend_from_slice(&[0; 15]); + assert!(minicbor::decode::(&v).is_err()); } diff --git a/minicbor/src/data.rs b/minicbor/src/data.rs index 6009339..916d8f6 100644 --- a/minicbor/src/data.rs +++ b/minicbor/src/data.rs @@ -165,6 +165,18 @@ impl IanaTag { } } +impl PartialEq for IanaTag { + fn eq(&self, other: &Tag) -> bool { + self.tag() == *other + } +} + +impl PartialEq for Tag { + fn eq(&self, other: &IanaTag) -> bool { + *self == other.tag() + } +} + impl TryFrom for IanaTag { type Error = UnknownTag; diff --git a/minicbor/src/decode.rs b/minicbor/src/decode.rs index 50ef606..bcc7a71 100644 --- a/minicbor/src/decode.rs +++ b/minicbor/src/decode.rs @@ -341,7 +341,7 @@ macro_rules! decode_basic { } } -decode_basic!(u8 i8 u16 i16 u32 i32 u64 i64 bool f32 f64 char); +decode_basic!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 bool f32 f64 char); macro_rules! decode_nonzero { ($($t:ty, $msg:expr)*) => { @@ -357,14 +357,16 @@ macro_rules! decode_nonzero { } decode_nonzero! { - core::num::NonZeroU8, "unexpected 0 when decoding a `NonZeroU8`" - core::num::NonZeroU16, "unexpected 0 when decoding a `NonZeroU16`" - core::num::NonZeroU32, "unexpected 0 when decoding a `NonZeroU32`" - core::num::NonZeroU64, "unexpected 0 when decoding a `NonZeroU64`" - core::num::NonZeroI8, "unexpected 0 when decoding a `NonZeroI8`" - core::num::NonZeroI16, "unexpected 0 when decoding a `NonZeroI16`" - core::num::NonZeroI32, "unexpected 0 when decoding a `NonZeroI32`" - core::num::NonZeroI64, "unexpected 0 when decoding a `NonZeroI64`" + core::num::NonZeroU8, "unexpected 0 when decoding a `NonZeroU8`" + core::num::NonZeroU16, "unexpected 0 when decoding a `NonZeroU16`" + core::num::NonZeroU32, "unexpected 0 when decoding a `NonZeroU32`" + core::num::NonZeroU64, "unexpected 0 when decoding a `NonZeroU64`" + core::num::NonZeroU128, "unexpected 0 when decoding a `NonZeroU128`" + core::num::NonZeroI8, "unexpected 0 when decoding a `NonZeroI8`" + core::num::NonZeroI16, "unexpected 0 when decoding a `NonZeroI16`" + core::num::NonZeroI32, "unexpected 0 when decoding a `NonZeroI32`" + core::num::NonZeroI64, "unexpected 0 when decoding a `NonZeroI64`" + core::num::NonZeroI128, "unexpected 0 when decoding a `NonZeroI128`" } #[cfg(any(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))] diff --git a/minicbor/src/decode/decoder.rs b/minicbor/src/decode/decoder.rs index f3faf31..57f74fa 100644 --- a/minicbor/src/decode/decoder.rs +++ b/minicbor/src/decode/decoder.rs @@ -1,7 +1,7 @@ #![allow(clippy::unusual_byte_groupings)] -use crate::{ARRAY, BREAK, BYTES, MAP, SIMPLE, TAGGED, TEXT, SIGNED, UNSIGNED}; -use crate::data::{Int, Tag, Type}; +use crate::{ARRAY, BREAK, BYTES, MAP, SIGNED, SIMPLE, TAGGED, TEXT, UNSIGNED}; +use crate::data::{IanaTag, Int, Tag, Type}; use crate::decode::{Decode, Error}; use core::{marker, str}; @@ -183,6 +183,74 @@ impl<'b> Decoder<'b> { } } + /// Decode a `u128` value. + /// + /// Accepts either a native CBOR unsigned integer or a positive bignum, + /// i.e. tag 2 followed by a byte string holding the big-endian + /// representation of the value, as defined in [RFC 8949 §3.4.3][1]. + /// + /// [1]: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 + pub fn u128(&mut self) -> Result { + let p = self.pos; + let b = self.current()?; + match type_of(b) { + UNSIGNED => self.u64().map(u128::from), + TAGGED => { + let t = self.tag()?; + if t != IanaTag::PosBignum { + return Err(Error::tag_mismatch(t) + .with_message("expected positive bignum tag (2)") + .at(p)) + } + decode_bignum_u128(self, p) + } + _ => Err(Error::type_mismatch(self.type_of(b)?) + .with_message("expected u128") + .at(p)) + } + } + + /// Decode an `i128` value. + /// + /// Accepts native CBOR integers as well as positive (tag 2) and negative + /// (tag 3) bignums, as defined in [RFC 8949 §3.4.3][1]. + /// + /// [1]: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 + pub fn i128(&mut self) -> Result { + let p = self.pos; + let b = self.current()?; + match type_of(b) { + UNSIGNED => self.u64().map(i128::from), + SIGNED => self.int().map(i128::from), + TAGGED => { + let t = self.tag()?; + match t.try_into() { + Ok(IanaTag::PosBignum) => { + let n = decode_bignum_u128(self, p)?; + if n > i128::MAX as u128 { + return Err(Error::message("positive bignum exceeds i128 range").at(p)) + } + Ok(n as i128) + } + Ok(IanaTag::NegBignum) => { + let n = decode_bignum_u128(self, p)?; + if n > i128::MAX as u128 { + return Err(Error::message("negative bignum exceeds i128 range").at(p)) + } + // `!n as i128 == -1 - n` for `n <= i128::MAX as u128`. + Ok((!n) as i128) + } + _ => Err(Error::tag_mismatch(t) + .with_message("expected bignum tag (2 or 3)") + .at(p)) + } + } + _ => Err(Error::type_mismatch(self.type_of(b)?) + .with_message("expected i128") + .at(p)) + } + } + /// Decode a CBOR integer. /// /// See [`Int`] for details regarding the value range of CBOR integers. @@ -1076,3 +1144,24 @@ where { val.try_into().map_err(|_| Error::overflow(val.into()).at(pos).with_message(msg)) } + +/// Decode the byte-string payload of a bignum (tag 2 or 3) into a `u128`. +/// +/// Leading zero bytes are tolerated; values that would not fit into 16 +/// bytes after stripping leading zeros are rejected. +fn decode_bignum_u128(d: &mut Decoder<'_>, pos: usize) -> Result { + let bytes = { + let bs = d.bytes()?; + if bs.is_empty() { + return Ok(0) + } + let start = bs.iter().position(|&b| b != 0).unwrap_or_else(|| bs.len() - 1); + &bs[start ..] + }; + if bytes.len() > 16 { + return Err(Error::message("bignum exceeds 128 bits").at(pos)) + } + let mut buf = [0u8; 16]; + buf[16 - bytes.len()..].copy_from_slice(bytes); + Ok(u128::from_be_bytes(buf)) +} diff --git a/minicbor/src/encode.rs b/minicbor/src/encode.rs index c3add00..a97767b 100644 --- a/minicbor/src/encode.rs +++ b/minicbor/src/encode.rs @@ -494,7 +494,7 @@ macro_rules! encode_basic { } } -encode_basic!(u8 i8 u16 i16 u32 i32 u64 i64 bool f32 f64 char); +encode_basic!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 bool f32 f64 char); impl CborLen for bool { fn cbor_len(&self, _: &mut C) -> usize { @@ -575,6 +575,30 @@ impl CborLen for i64 { } } +impl CborLen for u128 { + fn cbor_len(&self, ctx: &mut C) -> usize { + if *self <= u64::MAX as u128 { + (*self as u64).cbor_len(ctx) + } else { + // tag (1) + byte string header (1) + payload bytes + let bytes = 16 - (self.leading_zeros() / 8) as usize; + 2 + bytes + } + } +} + +impl CborLen for i128 { + fn cbor_len(&self, ctx: &mut C) -> usize { + let n = if *self >= 0 { *self as u128 } else { !(*self as u128) }; + if n <= u64::MAX as u128 { + (n as u64).cbor_len(ctx) + } else { + let bytes = 16 - (n.leading_zeros() / 8) as usize; + 2 + bytes + } + } +} + impl CborLen for f32 { fn cbor_len(&self, _: &mut C) -> usize { 5 @@ -610,10 +634,12 @@ encode_nonzero! { core::num::NonZeroU16 core::num::NonZeroU32 core::num::NonZeroU64 + core::num::NonZeroU128 core::num::NonZeroI8 core::num::NonZeroI16 core::num::NonZeroI32 core::num::NonZeroI64 + core::num::NonZeroI128 } #[cfg(any(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))] diff --git a/minicbor/src/encode/encoder.rs b/minicbor/src/encode/encoder.rs index a8c72e8..3c70f75 100644 --- a/minicbor/src/encode/encoder.rs +++ b/minicbor/src/encode/encoder.rs @@ -1,5 +1,5 @@ use crate::{SIGNED, BYTES, TEXT, ARRAY, MAP, TAGGED, SIMPLE}; -use crate::data::{Int, Tag}; +use crate::data::{IanaTag, Int, Tag}; use crate::encode::{Encode, Error, Write}; /// A non-allocating CBOR encoder writing encoded bytes to the given [`Write`] sink. @@ -128,6 +128,47 @@ impl Encoder { } } + /// Encode a `u128` value. + /// + /// Values up to [`u64::MAX`] are encoded as native CBOR unsigned integers. + /// Larger values are encoded as a positive bignum, i.e. tag 2 followed by + /// the big-endian byte string of the value (with leading zero bytes + /// stripped), as defined in [RFC 8949 §3.4.3][1]. + /// + /// [1]: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 + pub fn u128(&mut self, x: u128) -> Result<&mut Self, Error> { + if x <= u64::MAX as u128 { + return self.u64(x as u64) + } + self.tag(IanaTag::PosBignum)?; + let bytes = x.to_be_bytes(); + let start = (x.leading_zeros() / 8) as usize; + self.bytes(&bytes[start..]) + } + + /// Encode an `i128` value. + /// + /// Values that fit into the CBOR integer range, i.e. `[-2^64, 2^64 - 1]`, + /// are encoded as native CBOR integers. Values outside that range are + /// encoded as a positive (tag 2) or negative (tag 3) bignum, as defined + /// in [RFC 8949 §3.4.3][1]. + /// + /// [1]: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 + pub fn i128(&mut self, x: i128) -> Result<&mut Self, Error> { + if x >= 0 { + return self.u128(x as u128) + } + // `!(x as u128) == (-1 - x) as u128` for any negative `x: i128`. + let n = !(x as u128); + if n <= u64::MAX as u128 { + return self.int(Int::neg(n as u64)) + } + self.tag(IanaTag::NegBignum)?; + let bytes = n.to_be_bytes(); + let start = (n.leading_zeros() / 8) as usize; + self.bytes(&bytes[start..]) + } + /// Encode a CBOR integer. /// /// See [`Int`] for details regarding the value range of CBOR integers.