Skip to content

Commit

Permalink
Implement Ser+De for Saturating<T>
Browse files Browse the repository at this point in the history
The serialization implementation is heavily
inspired by the existing trait implentation for
`std::num::Wrapping<T>`.

The deserializing implementation maps input values
that lie outside of the numerical range of the
output type to the `MIN` or `MAX` value of the
output type, depending on the sign of the input
value. This behaviour follows to the `Saturating`
semantics of the output type.

fix #2708
  • Loading branch information
Jörn Bethune committed Apr 6, 2024
1 parent 5b24f88 commit 3d1b19e
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 2 deletions.
6 changes: 6 additions & 0 deletions serde/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ fn main() {
if minor < 64 {
println!("cargo:rustc-cfg=no_core_cstr");
}

// Support for core::num::Saturating and std::num::Saturating stabilized in Rust 1.74
// https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html#stabilized-apis
if minor < 74 {
println!("cargo:rustc-cfg=no_core_num_saturating");
}
}

fn rustc_minor_version() -> Option<u32> {
Expand Down
67 changes: 67 additions & 0 deletions serde/src/de/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,73 @@ impl_deserialize_num! {
num_as_self!(u8:visit_u8 u16:visit_u16 u32:visit_u32 u64:visit_u64);
}

#[cfg(not(no_core_num_saturating))]
macro_rules! visit_saturating {
($primitive:ident, $ty:ident : $visit:ident) => {
#[inline]
fn $visit<E>(self, v: $ty) -> Result<Saturating<$primitive>, E>
where
E: Error,
{
let out: $primitive = core::convert::TryFrom::<$ty>::try_from(v).unwrap_or_else(|_| {
#[allow(unused_comparisons)]
if v < 0 {
// never true for unsigned values
$primitive::MIN
} else {
$primitive::MAX
}
});
Ok(Saturating(out))
}
};
}

macro_rules! impl_deserialize_saturating_num {
($primitive:ident, $deserialize:ident ) => {
#[cfg(not(no_core_num_saturating))]
impl<'de> Deserialize<'de> for Saturating<$primitive> {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct SaturatingVisitor;

impl<'de> Visitor<'de> for SaturatingVisitor {
type Value = Saturating<$primitive>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("An integer with support for saturating semantics")
}

visit_saturating!($primitive, u8:visit_u8);
visit_saturating!($primitive, u16:visit_u16);
visit_saturating!($primitive, u32:visit_u32);
visit_saturating!($primitive, u64:visit_u64);
visit_saturating!($primitive, i8:visit_i8);
visit_saturating!($primitive, i16:visit_i16);
visit_saturating!($primitive, i32:visit_i32);
visit_saturating!($primitive, i64:visit_i64);
}

deserializer.$deserialize(SaturatingVisitor)
}
}
};
}

impl_deserialize_saturating_num!(u8, deserialize_u8);
impl_deserialize_saturating_num!(u16, deserialize_u16);
impl_deserialize_saturating_num!(u32, deserialize_u32);
impl_deserialize_saturating_num!(u64, deserialize_u64);
impl_deserialize_saturating_num!(usize, deserialize_u64);
impl_deserialize_saturating_num!(i8, deserialize_i8);
impl_deserialize_saturating_num!(i16, deserialize_i16);
impl_deserialize_saturating_num!(i32, deserialize_i32);
impl_deserialize_saturating_num!(i64, deserialize_i64);
impl_deserialize_saturating_num!(isize, deserialize_i64);

macro_rules! num_128 {
($ty:ident : $visit:ident) => {
fn $visit<E>(self, v: $ty) -> Result<Self::Value, E>
Expand Down
3 changes: 3 additions & 0 deletions serde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ mod lib {
pub use std::sync::atomic::{AtomicI64, AtomicU64};
#[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "ptr"))]
pub use std::sync::atomic::{AtomicIsize, AtomicUsize};

#[cfg(not(no_core_num_saturating))]
pub use self::core::num::Saturating;
}

// None of this crate's error handling needs the `From::from` error conversion
Expand Down
14 changes: 14 additions & 0 deletions serde/src/ser/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,20 @@ where
}
}

#[cfg(not(no_core_num_saturating))]
impl<T> Serialize for Saturating<T>
where
T: Serialize,
{
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}

impl<T> Serialize for Reverse<T>
where
T: Serialize,
Expand Down
39 changes: 38 additions & 1 deletion test_suite/tests/test_de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::iter;
use std::net;
use std::num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, Wrapping,
NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, Saturating, Wrapping,
};
use std::ops::Bound;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -2065,6 +2065,43 @@ fn test_wrapping() {
test(Wrapping(1usize), &[Token::U64(1)]);
}

#[test]
fn test_saturating() {
test(Saturating(1usize), &[Token::U32(1)]);
test(Saturating(1usize), &[Token::U64(1)]);
test(Saturating(0u8), &[Token::I8(0)]);
test(Saturating(0u16), &[Token::I16(0)]);

// saturate input values at the minimum or maximum value
test(Saturating(u8::MAX), &[Token::U16(u16::MAX)]);
test(Saturating(u8::MAX), &[Token::U16(u8::MAX as u16 + 1)]);
test(Saturating(u16::MAX), &[Token::U32(u32::MAX)]);
test(Saturating(u32::MAX), &[Token::U64(u64::MAX)]);
test(Saturating(u8::MIN), &[Token::I8(i8::MIN)]);
test(Saturating(u16::MIN), &[Token::I16(i16::MIN)]);
test(Saturating(u32::MIN), &[Token::I32(i32::MIN)]);
test(Saturating(i8::MIN), &[Token::I16(i16::MIN)]);
test(Saturating(i16::MIN), &[Token::I32(i32::MIN)]);
test(Saturating(i32::MIN), &[Token::I64(i64::MIN)]);

test(Saturating(u8::MIN), &[Token::I8(-1)]);
test(Saturating(u16::MIN), &[Token::I16(-1)]);

#[cfg(target_pointer_width = "64")]
{
test(Saturating(usize::MIN), &[Token::U64(u64::MIN)]);
test(Saturating(usize::MAX), &[Token::U64(u64::MAX)]);
test(Saturating(isize::MIN), &[Token::I64(i64::MIN)]);
test(Saturating(isize::MAX), &[Token::I64(i64::MAX)]);
test(Saturating(0usize), &[Token::I64(i64::MIN)]);

test(
Saturating(9_223_372_036_854_775_807usize),
&[Token::I64(i64::MAX)],
);
}
}

#[test]
fn test_rc_dst() {
test(Rc::<str>::from("s"), &[Token::Str("s")]);
Expand Down
7 changes: 6 additions & 1 deletion test_suite/tests/test_ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::ffi::CString;
use std::net;
use std::num::Wrapping;
use std::num::{Saturating, Wrapping};
use std::ops::Bound;
use std::path::{Path, PathBuf};
use std::rc::{Rc, Weak as RcWeak};
Expand Down Expand Up @@ -624,6 +624,11 @@ fn test_wrapping() {
assert_ser_tokens(&Wrapping(1usize), &[Token::U64(1)]);
}

#[test]
fn test_saturating() {
assert_ser_tokens(&Saturating(1usize), &[Token::U64(1)]);
}

#[test]
fn test_rc_dst() {
assert_ser_tokens(&Rc::<str>::from("s"), &[Token::Str("s")]);
Expand Down

0 comments on commit 3d1b19e

Please sign in to comment.