Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/serde/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
35 changes: 35 additions & 0 deletions src/serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<BTreeMap<i64, u8>>(json).is_err(),
"i64 key: {json:?}"
);
assert!(
crate::from_str::<BTreeMap<u32, u8>>(json).is_err(),
"u32 key: {json:?}"
);
assert!(
crate::from_str::<BTreeMap<i8, u8>>(json).is_err(),
"i8 key: {json:?}"
);
assert!(
serde_json::from_str::<BTreeMap<i64, u8>>(json).is_err(),
"serde_json: {json:?}"
);
}
// as a string key it is fine, and plain numeric keys are unchanged
let m: BTreeMap<String, u8> = crate::from_str(r#"{" 1":2}"#).unwrap();
assert_eq!(m.get(" 1"), Some(&2));
let m: BTreeMap<i64, u8> = 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""#];
Expand Down
38 changes: 38 additions & 0 deletions src/value/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
};
Expand Down Expand Up @@ -746,4 +751,37 @@ mod test {
assert!(from_value::<E>(&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::<BTreeMap<i64, u8>>(&v).is_err(),
"i64 key: {json}"
);
assert!(
from_value::<BTreeMap<u32, u8>>(&v).is_err(),
"u32 key: {json}"
);
assert!(from_str::<BTreeMap<i64, u8>>(json).is_err(), "text: {json}");
}
let v: Value = from_str(r#"{"12":1,"-3":2}"#).unwrap();
assert_eq!(
from_value::<BTreeMap<i64, u8>>(&v).unwrap(),
BTreeMap::from([(12, 1), (-3, 2)])
);
}
}