Skip to content

Commit

Permalink
feat: introduce feature to limit integer to i64
Browse files Browse the repository at this point in the history
By default the IPLD Integer kind is represented internally as an `i128`. Serde
has problems with untagged enums that contain i128 types. Therefore a feature
flag called `integer-max-i64` is introduced, which reduces the internal integer
representation to `i64`.

This flag should be used with caution as e.g. not all valid DAG-CBOR data can
now be represented.

Closes #19.
  • Loading branch information
vmx committed Sep 2, 2024
1 parent 1a2ffc7 commit 90adbf7
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 49 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ serde = ["dep:serde", "dep:serde_bytes", "cid/serde"]
arb = ["dep:quickcheck", "cid/arb"]
# Enables support for the Codec trait, needs at least Rust 1.75
codec = []
# Makes the internal representation of an IPLD integer an `i64` instead of the default `i128`. This
# is usefult to work around Serde limitations in regards to untagged enums that contain `i128`
# types. **Warning** enabling this feature might break compatibility with existing data.
integer-max-i64 = []

[dependencies]
cid = { version = "0.11.1", default-features = false, features = ["alloc"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Feature flags
- `codec` (enabled by default): Provides the `Codec` trait, which enables encoding and decoding independent of the IPLD Codec. The minimum supported Rust version (MSRV) can significantly be reduced to 1.64 by disabling this feature.
- `serde`: Enables support for Serde serialization into/deserialization from the `Ipld` enum.
- `arb`: Enables support for property based testing.
- `integer-max-i64`: The IPLD integer type is by default an `i128`. With this feature set it's an `i64`. This is useful to work around Serde limitations in regards to untagged enums that contain `i128` types. **Warning** enabling this feature might break compatibility with existing data.


License
Expand Down
3 changes: 3 additions & 0 deletions src/arb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ impl Ipld {
match index {
0 => Ipld::Null,
1 => Ipld::Bool(bool::arbitrary(g)),
#[cfg(not(feature = "integer-max-i64"))]
2 => Ipld::Integer(i128::arbitrary(g)),
#[cfg(feature = "integer-max-i64")]
2 => Ipld::Integer(i64::arbitrary(g)),
3 => Ipld::Float(f64::arbitrary(g)),
4 => Ipld::String(String::arbitrary(g)),
5 => Ipld::Bytes(Vec::arbitrary(g)),
Expand Down
47 changes: 29 additions & 18 deletions src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,16 +217,21 @@ mod tests {

use crate::ipld::Ipld;

#[cfg(not(feature = "integer-max-i64"))]
type Integer = i128;
#[cfg(feature = "integer-max-i64")]
type Integer = i64;

#[test]
#[should_panic]
fn try_into_wrong_type() {
let _boolean: bool = Ipld::Integer(u8::MAX as i128).try_into().unwrap();
let _boolean: bool = Ipld::Integer(u8::MAX as Integer).try_into().unwrap();
}

#[test]
#[should_panic]
fn try_into_wrong_range() {
let int: u128 = Ipld::Integer(-1i128).try_into().unwrap();
let int: u128 = Ipld::Integer(-1 as Integer).try_into().unwrap();
assert_eq!(int, u128::MIN);
}

Expand All @@ -241,41 +246,47 @@ mod tests {

#[test]
fn try_into_ints() {
let int: u8 = Ipld::Integer(u8::MAX as i128).try_into().unwrap();
let int: u8 = Ipld::Integer(u8::MAX as Integer).try_into().unwrap();
assert_eq!(int, u8::MAX);

let int: u16 = Ipld::Integer(u16::MAX as i128).try_into().unwrap();
let int: u16 = Ipld::Integer(u16::MAX as Integer).try_into().unwrap();
assert_eq!(int, u16::MAX);

let int: u32 = Ipld::Integer(u32::MAX as i128).try_into().unwrap();
let int: u32 = Ipld::Integer(u32::MAX as Integer).try_into().unwrap();
assert_eq!(int, u32::MAX);

let int: u64 = Ipld::Integer(u64::MAX as i128).try_into().unwrap();
assert_eq!(int, u64::MAX);
#[cfg(not(feature = "integer-max-i64"))]
{
let int: u64 = Ipld::Integer(u64::MAX as i128).try_into().unwrap();
assert_eq!(int, u64::MAX);

let int: usize = Ipld::Integer(usize::MAX as i128).try_into().unwrap();
assert_eq!(int, usize::MAX);
let int: usize = Ipld::Integer(usize::MAX as i128).try_into().unwrap();
assert_eq!(int, usize::MAX);

let int: u128 = Ipld::Integer(i128::MAX).try_into().unwrap();
assert_eq!(int, i128::MAX as u128);
let int: u128 = Ipld::Integer(i128::MAX).try_into().unwrap();
assert_eq!(int, i128::MAX as u128);
}

let int: i8 = Ipld::Integer(i8::MIN as i128).try_into().unwrap();
let int: i8 = Ipld::Integer(i8::MIN as Integer).try_into().unwrap();
assert_eq!(int, i8::MIN);

let int: i16 = Ipld::Integer(i16::MIN as i128).try_into().unwrap();
let int: i16 = Ipld::Integer(i16::MIN as Integer).try_into().unwrap();
assert_eq!(int, i16::MIN);

let int: i32 = Ipld::Integer(i32::MIN as i128).try_into().unwrap();
let int: i32 = Ipld::Integer(i32::MIN as Integer).try_into().unwrap();
assert_eq!(int, i32::MIN);

let int: i64 = Ipld::Integer(i64::MIN as i128).try_into().unwrap();
let int: i64 = Ipld::Integer(i64::MIN as Integer).try_into().unwrap();
assert_eq!(int, i64::MIN);

let int: isize = Ipld::Integer(isize::MIN as i128).try_into().unwrap();
let int: isize = Ipld::Integer(isize::MIN as Integer).try_into().unwrap();
assert_eq!(int, isize::MIN);

let int: i128 = Ipld::Integer(i128::MIN).try_into().unwrap();
assert_eq!(int, i128::MIN);
#[cfg(not(feature = "integer-max-i64"))]
{
let int: i128 = Ipld::Integer(i128::MIN).try_into().unwrap();
assert_eq!(int, i128::MIN);
}

let int: Option<i32> = Ipld::Null.try_into().unwrap();
assert_eq!(int, Option::None)
Expand Down
4 changes: 4 additions & 0 deletions src/ipld.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ pub enum Ipld {
/// Represents a boolean value.
Bool(bool),
/// Represents an integer.
#[cfg(not(feature = "integer-max-i64"))]
Integer(i128),
/// Represents an integer.
#[cfg(feature = "integer-max-i64")]
Integer(i64),
/// Represents a floating point value.
Float(f64),
/// Represents an UTF-8 string.
Expand Down
27 changes: 24 additions & 3 deletions src/serde/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,41 @@ impl<'de> de::Deserialize<'de> for Ipld {
where
E: de::Error,
{
Ok(Ipld::Integer(v.into()))
#[cfg(not(feature = "integer-max-i64"))]
let integer = v.into();
#[cfg(feature = "integer-max-i64")]
let integer = v
.try_into()
.map_err(|_| de::Error::custom("integer out of i64 bounds"))?;
Ok(Ipld::Integer(integer))
}

#[inline]
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Ipld::Integer(v.into()))
#[cfg(not(feature = "integer-max-i64"))]
let integer = v.into();
#[cfg(feature = "integer-max-i64")]
let integer = v;
Ok(Ipld::Integer(integer))
}

#[inline]
#[cfg_attr(feature = "integer-max-i64", allow(unused_variables))]
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Ipld::Integer(v))
#[cfg(not(feature = "integer-max-i64"))]
{
Ok(Ipld::Integer(v))
}
#[cfg(feature = "integer-max-i64")]
{
Err(de::Error::custom("integer out of i64 bounds"))
}
}

#[inline]
Expand Down Expand Up @@ -266,7 +284,10 @@ impl<'de> de::Deserializer<'de> for Ipld {
match self {
Self::Null => visitor.visit_none(),
Self::Bool(bool) => visitor.visit_bool(bool),
#[cfg(not(feature = "integer-max-i64"))]
Self::Integer(i128) => visitor.visit_i128(i128),
#[cfg(feature = "integer-max-i64")]
Self::Integer(i64) => visitor.visit_i64(i64),
Self::Float(f64) => visitor.visit_f64(f64),
Self::String(string) => visitor.visit_str(&string),
Self::Bytes(bytes) => visitor.visit_bytes(&bytes),
Expand Down
39 changes: 33 additions & 6 deletions src/serde/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ impl ser::Serialize for Ipld {
match &self {
Self::Null => serializer.serialize_none(),
Self::Bool(value) => serializer.serialize_bool(*value),
#[cfg(not(feature = "integer-max-i64"))]
Self::Integer(value) => serializer.serialize_i128(*value),
#[cfg(feature = "integer-max-i64")]
Self::Integer(value) => serializer.serialize_i64(*value),
Self::Float(value) => serializer.serialize_f64(*value),
Self::String(value) => serializer.serialize_str(value),
Self::Bytes(value) => serializer.serialize_bytes(value),
Expand Down Expand Up @@ -135,31 +138,55 @@ impl serde::Serializer for Serializer {

#[inline]
fn serialize_i64(self, value: i64) -> Result<Self::Ok, Self::Error> {
self.serialize_i128(i128::from(value))
#[cfg(not(feature = "integer-max-i64"))]
{
self.serialize_i128(i128::from(value))
}
#[cfg(feature = "integer-max-i64")]
{
Ok(Self::Ok::Integer(value))
}
}

#[cfg_attr(feature = "integer-max-i64", allow(unused_variables))]
fn serialize_i128(self, value: i128) -> Result<Self::Ok, Self::Error> {
Ok(Self::Ok::Integer(value))
#[cfg(not(feature = "integer-max-i64"))]
{
Ok(Self::Ok::Integer(value))
}
#[cfg(feature = "integer-max-i64")]
{
Err(ser::Error::custom("integer out of i64 bounds"))
}
}

#[inline]
fn serialize_u8(self, value: u8) -> Result<Self::Ok, Self::Error> {
self.serialize_i128(value.into())
self.serialize_i64(value.into())
}

#[inline]
fn serialize_u16(self, value: u16) -> Result<Self::Ok, Self::Error> {
self.serialize_i128(value.into())
self.serialize_i64(value.into())
}

#[inline]
fn serialize_u32(self, value: u32) -> Result<Self::Ok, Self::Error> {
self.serialize_i128(value.into())
self.serialize_i64(value.into())
}

#[inline]
fn serialize_u64(self, value: u64) -> Result<Self::Ok, Self::Error> {
self.serialize_i128(value.into())
#[cfg(not(feature = "integer-max-i64"))]
{
self.serialize_i128(value.into())
}
#[cfg(feature = "integer-max-i64")]
{
Ok(Self::Ok::Integer(value.try_into().map_err(|_| {
ser::Error::custom("integer out of i64 bounds")
})?))
}
}

#[inline]
Expand Down
54 changes: 33 additions & 21 deletions tests/serde_deserializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ use serde_json::json;
use ipld_core::cid::Cid;
use ipld_core::ipld::Ipld;

#[cfg(not(feature = "integer-max-i64"))]
type Integer = i128;
#[cfg(feature = "integer-max-i64")]
type Integer = i64;

/// This function is to test that all IPLD kinds except the given one errors, when trying to
/// deserialize to the given Rust type.
fn error_except<'de, T>(_input: T, except: &Ipld)
Expand Down Expand Up @@ -98,9 +103,9 @@ fn ipld_deserializer_u8() {
"Correctly deserialize Ipld::Integer to u8."
);

let too_large = u8::deserialize(Ipld::Integer((u8::MAX as i128) + 10));
let too_large = u8::deserialize(Ipld::Integer((u8::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = u8::deserialize(Ipld::Integer((u8::MIN as i128) - 10));
let too_small = u8::deserialize(Ipld::Integer((u8::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

Expand All @@ -116,9 +121,9 @@ fn ipld_deserializer_u16() {
"Correctly deserialize Ipld::Integer to u16."
);

let too_large = u16::deserialize(Ipld::Integer((u16::MAX as i128) + 10));
let too_large = u16::deserialize(Ipld::Integer((u16::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = u16::deserialize(Ipld::Integer((u16::MIN as i128) - 10));
let too_small = u16::deserialize(Ipld::Integer((u16::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

Expand All @@ -134,16 +139,17 @@ fn ipld_deserializer_u32() {
"Correctly deserialize Ipld::Integer to u32."
);

let too_large = u32::deserialize(Ipld::Integer((u32::MAX as i128) + 10));
let too_large = u32::deserialize(Ipld::Integer((u32::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = u32::deserialize(Ipld::Integer((u32::MIN as i128) - 10));
let too_small = u32::deserialize(Ipld::Integer((u32::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

#[test]
#[allow(clippy::unnecessary_fallible_conversions)]
fn ipld_deserializer_u64() {
let integer = 34567890123u64;
let ipld = Ipld::Integer(integer.into());
let ipld = Ipld::Integer(integer.try_into().unwrap());
error_except(integer, &ipld);

let deserialized = u64::deserialize(ipld).unwrap();
Expand All @@ -152,10 +158,13 @@ fn ipld_deserializer_u64() {
"Correctly deserialize Ipld::Integer to u64."
);

let too_large = u64::deserialize(Ipld::Integer((u64::MAX as i128) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = u64::deserialize(Ipld::Integer((u64::MIN as i128) - 10));
assert!(too_small.is_err(), "Number must be within range.");
#[cfg(not(feature = "integer-max-i64"))]
{
let too_large = u64::deserialize(Ipld::Integer((u64::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = u64::deserialize(Ipld::Integer((u64::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}
}

#[test]
Expand All @@ -170,9 +179,9 @@ fn ipld_deserializer_i8() {
"Correctly deserialize Ipld::Integer to i8."
);

let too_large = i8::deserialize(Ipld::Integer((i8::MAX as i128) + 10));
let too_large = i8::deserialize(Ipld::Integer((i8::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = i8::deserialize(Ipld::Integer((i8::MIN as i128) - 10));
let too_small = i8::deserialize(Ipld::Integer((i8::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

Expand All @@ -188,9 +197,9 @@ fn ipld_deserializer_i16() {
"Correctly deserialize Ipld::Integer to i16."
);

let too_large = i16::deserialize(Ipld::Integer((i16::MAX as i128) + 10));
let too_large = i16::deserialize(Ipld::Integer((i16::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = i16::deserialize(Ipld::Integer((i16::MIN as i128) - 10));
let too_small = i16::deserialize(Ipld::Integer((i16::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

Expand All @@ -206,9 +215,9 @@ fn ipld_deserializer_i32() {
"Correctly deserialize Ipld::Integer to i32."
);

let too_large = i32::deserialize(Ipld::Integer((i32::MAX as i128) + 10));
let too_large = i32::deserialize(Ipld::Integer((i32::MAX as Integer) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = i32::deserialize(Ipld::Integer((i32::MIN as i128) - 10));
let too_small = i32::deserialize(Ipld::Integer((i32::MIN as Integer) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}

Expand All @@ -224,10 +233,13 @@ fn ipld_deserializer_i64() {
"Correctly deserialize Ipld::Integer to i64."
);

let too_large = i64::deserialize(Ipld::Integer((i64::MAX as i128) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = i64::deserialize(Ipld::Integer((i64::MIN as i128) - 10));
assert!(too_small.is_err(), "Number must be within range.");
#[cfg(not(feature = "integer-max-i64"))]
{
let too_large = i64::deserialize(Ipld::Integer((i64::MAX as i128) + 10));
assert!(too_large.is_err(), "Number must be within range.");
let too_small = i64::deserialize(Ipld::Integer((i64::MIN as i128) - 10));
assert!(too_small.is_err(), "Number must be within range.");
}
}

#[test]
Expand Down
Loading

0 comments on commit 90adbf7

Please sign in to comment.