diff --git a/src/serde/de.rs b/src/serde/de.rs index db71213..f25ed02 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1152,6 +1152,13 @@ macro_rules! deserialize_numeric_key { where V: de::Visitor<'de>, { + // The key text must be the number itself: `deserialize_number` skips leading + // whitespace, which is not part of a numeric key (`{" 1": 0}` is not key 1). + match self.de.parser.read.peek() { + Some(b'0'..=b'9' | b'-') => {} + _ => return Err(self.de.parser.error(ErrorCode::ExpectedNumericKey)), + } + let value = tri!(self.de.deserialize_number(visitor)); if self.de.parser.read.next() != Some(b'"') { return Err(self.de.parser.error(ErrorCode::ExpectedQuote)); diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 1a90144..ec9e1e8 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -767,6 +767,41 @@ mod test { assert_eq!(collect("[1] [2"), vec![Ok(crate::json!([1])), Err(())]); } + #[test] + fn test_numeric_key_rejects_leading_whitespace() { + // A numeric map key is the number itself; whitespace inside the quotes is not skipped + // (serde_json rejects these too). It used to be: `{" 1": 2}` gave the key 1. + for json in [ + r#"{" 1":2}"#, + r#"{" -1":2}"#, + "{\"\t1\":2}", + "{\"\n1\":2}", + r#"{" 1.5":2}"#, + ] { + assert!( + crate::from_str::>(json).is_err(), + "i64 key: {json:?}" + ); + assert!( + crate::from_str::>(json).is_err(), + "u32 key: {json:?}" + ); + assert!( + crate::from_str::>(json).is_err(), + "i8 key: {json:?}" + ); + assert!( + serde_json::from_str::>(json).is_err(), + "serde_json: {json:?}" + ); + } + // as a string key it is fine, and plain numeric keys are unchanged + let m: BTreeMap = crate::from_str(r#"{" 1":2}"#).unwrap(); + assert_eq!(m.get(" 1"), Some(&2)); + let m: BTreeMap = crate::from_str(r#"{"1":2,"-3":4}"#).unwrap(); + assert_eq!(m, BTreeMap::from([(1, 2), (-3, 4)])); + } + #[test] fn test_utf8_lossy() { let data = [&[b'\"', 0xff, b'\"'][..], br#""\uD800""#, br#""\udc00""#]; diff --git a/src/value/de.rs b/src/value/de.rs index 5694482..c891d26 100644 --- a/src/value/de.rs +++ b/src/value/de.rs @@ -213,6 +213,11 @@ macro_rules! deserialize_numeric_key { _ => return Err(Error::syntax(ErrorCode::ExpectedNumericKey, b"", 0)), } let number = tri!(de.$using(visitor)); + // the whole key must be the number: `{"1x": 0}` is not key 1 (as in serde_json, and + // as when deserializing from text, where the closing quote must follow the number) + if de.parser.read.peek().is_some() { + return Err(Error::syntax(ErrorCode::ExpectedNumericKey, b"", 0)); + } Ok(number) } }; @@ -746,4 +751,37 @@ mod test { assert!(from_value::(&v).is_err(), "{bad}"); } } + + #[test] + fn test_numeric_map_key_from_value_must_be_whole_number() { + // A numeric key is the whole key text. It used to be parsed as a prefix, so + // `{"12abc": 1}` gave the key 12 through `from_value` (the text path rejects it). + use std::collections::BTreeMap; + + use crate::{from_str, from_value, Value}; + + for json in [ + r#"{"12abc":1}"#, + r#"{"1 ":1}"#, + r#"{"1,2":1}"#, + r#"{"0x1":1}"#, + r#"{"08":1}"#, + ] { + let v: Value = from_str(json).unwrap(); + assert!( + from_value::>(&v).is_err(), + "i64 key: {json}" + ); + assert!( + from_value::>(&v).is_err(), + "u32 key: {json}" + ); + assert!(from_str::>(json).is_err(), "text: {json}"); + } + let v: Value = from_str(r#"{"12":1,"-3":2}"#).unwrap(); + assert_eq!( + from_value::>(&v).unwrap(), + BTreeMap::from([(12, 1), (-3, 2)]) + ); + } }