diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c7533e1..801a1294b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for parsing Rust-style string continuation escapes ([#585](https://github.com/ron-rs/ron/issues/585)) +### Bug Fixes + +- Fixed round-tripping of compact ranges whose start bound is `inf` or `NaN` ([#605](https://github.com/ron-rs/ron/pull/605)) + ## [0.12.2] - 2026-06-22 ### Format Changes diff --git a/src/de/mod.rs b/src/de/mod.rs index 7a7432829..a7b6d43a2 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -783,56 +783,43 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> { || (name == "RangeInclusive" && matches!(fields, ["start", "end" | "last"])) { let end_field = fields[1]; - if let Some(c) = self.parser.peek_char() { - if self.parser.is_number_start(c) { - let start = self.parser.any_number()?; - - let inclusive = if self.parser.consume_str("..=") { - true - } else if self.parser.consume_str("..") { - false - } else { - return Err(Error::ExpectedRangeSyntax); - }; - - if inclusive && name == "Range" { - return Err(Error::Message(String::from( - "expected `..` for `Range`, found `..=`", - ))); - } - if !inclusive && name == "RangeInclusive" { - return Err(Error::Message(String::from( - "expected `..=` for `RangeInclusive`, found `..`", - ))); - } + if self.parser.check_any_number_start() { + let start = self.parser.any_number()?; - let end = self.parser.any_number()?; - return visitor.visit_map(RangeMapAccess::new(start, end, end_field)); + let inclusive = if self.parser.consume_str("..=") { + true + } else if self.parser.consume_str("..") { + false + } else { + return Err(Error::ExpectedRangeSyntax); + }; + + if inclusive && name == "Range" { + return Err(Error::Message(String::from( + "expected `..` for `Range`, found `..=`", + ))); + } + if !inclusive && name == "RangeInclusive" { + return Err(Error::Message(String::from( + "expected `..=` for `RangeInclusive`, found `..`", + ))); } + + let end = self.parser.any_number()?; + return visitor.visit_map(RangeMapAccess::new(start, end, end_field)); } } - if fields == ["start"] && name == "RangeFrom" { - if let Some(c) = self.parser.peek_char() { - if self.parser.is_number_start(c) - || self.parser.check_ident("inf") - || self.parser.check_ident("inff32") - || self.parser.check_ident("inff64") - || self.parser.check_ident("NaN") - || self.parser.check_ident("NaNf32") - || self.parser.check_ident("NaNf64") - { - let start = self.parser.any_number()?; - if self.parser.consume_str("..=") { - return Err(Error::Message(String::from( - "expected `..` for `RangeFrom`, found `..=`", - ))); - } else if !self.parser.consume_str("..") { - return Err(Error::ExpectedRangeSyntax); - } - return visitor.visit_map(RangeFromMapAccess::new(start)); - } + if fields == ["start"] && name == "RangeFrom" && self.parser.check_any_number_start() { + let start = self.parser.any_number()?; + if self.parser.consume_str("..=") { + return Err(Error::Message(String::from( + "expected `..` for `RangeFrom`, found `..=`", + ))); + } else if !self.parser.consume_str("..") { + return Err(Error::ExpectedRangeSyntax); } + return visitor.visit_map(RangeFromMapAccess::new(start)); } if fields == ["end"] diff --git a/src/parse.rs b/src/parse.rs index 73f4b3dae..c37d70366 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -468,6 +468,24 @@ impl<'a> Parser<'a> { T::parse(self, sign, base) } + /// Check whether [`Parser::any_number`] has a number literal to parse at + /// the cursor. The `inf` and `NaN` literals start with a letter, so they + /// are not covered by [`Parser::is_number_start`]. + pub fn check_any_number_start(&mut self) -> bool { + match self.peek_char() { + None => false, + Some(c) => { + self.is_number_start(c) + || self.check_ident("inf") + || self.check_ident("inff32") + || self.check_ident("inff64") + || self.check_ident("NaN") + || self.check_ident("NaNf32") + || self.check_ident("NaNf64") + } + } + } + pub fn any_number(&mut self) -> Result { if self.consume_ident("inf") || self.consume_ident("inff32") { return Ok(Number::F32(crate::value::F32(core::f32::INFINITY))); diff --git a/tests/601_support_for_number_ranges.rs b/tests/601_support_for_number_ranges.rs index c2d2acc2a..4393b29c9 100644 --- a/tests/601_support_for_number_ranges.rs +++ b/tests/601_support_for_number_ranges.rs @@ -256,3 +256,53 @@ fn test_range_full_whitespace_lookahead() { MaybeRange::RangeFull(std::ops::RangeFull) ); } + +#[test] +fn test_range_inf_nan_start_bound_roundtrips() { + // Regression: the compact-range serializer emits `inf`/`NaN` start bounds + // (e.g. `inf..2.0`, `NaN..=2.0`), but the `Range`/`RangeInclusive` + // deserializers only entered the compact-range path when the first byte + // was recognised by `is_number_start` (digit / `+` / `-` / `.` / `b`). + // Since `inf` and `NaN` start with a letter, those inputs failed to + // round-trip with `ExpectedNamedStructLike`, even though `RangeFrom` + // (e.g. `inf..`) round-tripped fine. + let compact = ron::ser::PrettyConfig::new().compact_ranges(true); + + // Range with +inf start + let r = f64::INFINITY..2.0; + let s = ron::ser::to_string_pretty(&r, compact.clone()).unwrap(); + assert_eq!(s, "inf..2.0"); + let de: std::ops::Range = ron::from_str(&s).unwrap(); + assert!(de.start.is_infinite() && de.start.is_sign_positive()); + assert_eq!(de.end, 2.0); + + // Range with NaN start + let r = f64::NAN..2.0; + let s = ron::ser::to_string_pretty(&r, compact.clone()).unwrap(); + assert_eq!(s, "NaN..2.0"); + let de: std::ops::Range = ron::from_str(&s).unwrap(); + assert!(de.start.is_nan()); + assert_eq!(de.end, 2.0); + + // RangeInclusive with +inf start + let r = f64::INFINITY..=2.0; + let s = ron::ser::to_string_pretty(&r, compact.clone()).unwrap(); + assert_eq!(s, "inf..=2.0"); + let de: std::ops::RangeInclusive = ron::from_str(&s).unwrap(); + assert!(de.start().is_infinite() && de.start().is_sign_positive()); + assert_eq!(*de.end(), 2.0); + + // RangeInclusive with NaN start + let r = f64::NAN..=2.0; + let s = ron::ser::to_string_pretty(&r, compact.clone()).unwrap(); + assert_eq!(s, "NaN..=2.0"); + let de: std::ops::RangeInclusive = ron::from_str(&s).unwrap(); + assert!(de.start().is_nan()); + assert_eq!(*de.end(), 2.0); + + // Direct parse of the serializer output (independent of the serializer). + let de: std::ops::Range = ron::from_str("inf..2.0").unwrap(); + assert!(de.start.is_infinite()); + let de: std::ops::Range = ron::from_str("NaN..2.0").unwrap(); + assert!(de.start.is_nan()); +}