From 3d1b19ed906cb1c8cf4e2b4a45eea8f810115db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Bethune?= Date: Mon, 4 Mar 2024 18:56:15 +0100 Subject: [PATCH] Implement Ser+De for `Saturating` The serialization implementation is heavily inspired by the existing trait implentation for `std::num::Wrapping`. 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 --- serde/build.rs | 6 ++++ serde/src/de/impls.rs | 67 ++++++++++++++++++++++++++++++++++++ serde/src/lib.rs | 3 ++ serde/src/ser/impls.rs | 14 ++++++++ test_suite/tests/test_de.rs | 39 ++++++++++++++++++++- test_suite/tests/test_ser.rs | 7 +++- 6 files changed, 134 insertions(+), 2 deletions(-) diff --git a/serde/build.rs b/serde/build.rs index fe5486a7a..0074df63f 100644 --- a/serde/build.rs +++ b/serde/build.rs @@ -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 { diff --git a/serde/src/de/impls.rs b/serde/src/de/impls.rs index d89f1872b..9ccb3ce69 100644 --- a/serde/src/de/impls.rs +++ b/serde/src/de/impls.rs @@ -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(self, v: $ty) -> Result, 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(deserializer: D) -> Result + 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(self, v: $ty) -> Result diff --git a/serde/src/lib.rs b/serde/src/lib.rs index 5cf44c1c1..dc6d392bd 100644 --- a/serde/src/lib.rs +++ b/serde/src/lib.rs @@ -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 diff --git a/serde/src/ser/impls.rs b/serde/src/ser/impls.rs index 7aa11621c..ffc4c70f9 100644 --- a/serde/src/ser/impls.rs +++ b/serde/src/ser/impls.rs @@ -1026,6 +1026,20 @@ where } } +#[cfg(not(no_core_num_saturating))] +impl Serialize for Saturating +where + T: Serialize, +{ + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + impl Serialize for Reverse where T: Serialize, diff --git a/test_suite/tests/test_de.rs b/test_suite/tests/test_de.rs index 3ca0fde36..8a7310cd2 100644 --- a/test_suite/tests/test_de.rs +++ b/test_suite/tests/test_de.rs @@ -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}; @@ -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::::from("s"), &[Token::Str("s")]); diff --git a/test_suite/tests/test_ser.rs b/test_suite/tests/test_ser.rs index 71ec3bc57..b60d03ab2 100644 --- a/test_suite/tests/test_ser.rs +++ b/test_suite/tests/test_ser.rs @@ -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}; @@ -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::::from("s"), &[Token::Str("s")]);