351 lines
12 KiB
Rust
351 lines
12 KiB
Rust
use dever_runtime::wire::{self, document};
|
|
|
|
#[test]
|
|
#[ignore = "explicit strict-Unicode versus syntax-only performance diagnostic"]
|
|
fn escaped_string_validation_cost() {
|
|
use serde::{Deserializer, de::IgnoredAny};
|
|
use std::{hint::black_box, time::Instant};
|
|
let source = serde_json::to_string(&"雪\"\\\n".repeat(1024)).unwrap();
|
|
println!("case,sample,implementation,iterations,nanoseconds,checksum");
|
|
for sample in 0..15 {
|
|
for strict in if sample % 2 == 0 {
|
|
[true, false]
|
|
} else {
|
|
[false, true]
|
|
} {
|
|
let started = Instant::now();
|
|
for _ in 0..1000 {
|
|
let source = black_box(source.as_str());
|
|
if strict {
|
|
let mut decoder = serde_json::Deserializer::from_str(source);
|
|
decoder.deserialize_str(IgnoredAny).unwrap();
|
|
decoder.end().unwrap();
|
|
} else {
|
|
black_box(
|
|
serde_json::from_str::<&serde_json::value::RawValue>(source).unwrap(),
|
|
);
|
|
}
|
|
}
|
|
let elapsed = started.elapsed().as_nanos();
|
|
let label = if strict {
|
|
"rust_unicode"
|
|
} else {
|
|
"rust_syntax_only"
|
|
};
|
|
println!(
|
|
"escaped_string,{sample},{label},1000,{elapsed},{}",
|
|
source.len() * 1000
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn from_text_preserves_unicode_validation_and_original_spelling() {
|
|
use wire::json::Json;
|
|
for unit in 0..=u16::MAX {
|
|
let source = format!("\"\\u{unit:04x}\"");
|
|
assert_eq!(
|
|
Json::parse(&source).is_ok(),
|
|
serde_json::from_str::<String>(&source).is_ok(),
|
|
"{source}"
|
|
);
|
|
}
|
|
for text in [
|
|
"",
|
|
"雪🙂",
|
|
"\\u1234",
|
|
"\\\"\\\\",
|
|
"\\\\\"\\\"",
|
|
"\"\"雪\"",
|
|
"\u{0}\n\t",
|
|
] {
|
|
let source = serde_json::to_string(text).unwrap();
|
|
for source in [source.clone(), format!(" \n{source}\t ")] {
|
|
let parsed = Json::parse(&source).unwrap();
|
|
assert_eq!(parsed.as_str(), source);
|
|
assert_eq!(parsed.as_text().unwrap(), text);
|
|
assert_eq!(
|
|
Json::array(&[parsed]).unwrap().into_text(),
|
|
format!("[{}]", source.trim())
|
|
);
|
|
}
|
|
}
|
|
for source in [
|
|
r#"{"雪":0,"\u96ea":1}"#,
|
|
r#"["\ud800"]"#,
|
|
r#"{"field":"\udc00"}"#,
|
|
r#"{"\ud800":0}"#,
|
|
r#""\ud800\u0061""#,
|
|
] {
|
|
assert!(Json::parse(source).is_err(), "{source}");
|
|
}
|
|
for source in [
|
|
r#""\ud83d\ude42""#,
|
|
r#"{"value":[true,false,null,-0,1e9999]}"#,
|
|
] {
|
|
assert_eq!(Json::parse(source).unwrap().as_str(), source);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn shaped_input_keeps_array_syntax_and_limits() {
|
|
for count in [0, 16, document::MAX_VALUES - 1] {
|
|
let source = format!("[{}]", vec!["0"; count].join(","));
|
|
assert_eq!(
|
|
wire::decode_shaped::<Vec<i64>>(&source),
|
|
wire::decode(&source)
|
|
);
|
|
assert_eq!(
|
|
wire::decode_shaped::<Vec<i64>>(&source).unwrap().len(),
|
|
count
|
|
);
|
|
}
|
|
for source in [
|
|
"[1,]",
|
|
"[null]",
|
|
"[true]",
|
|
"[{}]",
|
|
"[[1]]",
|
|
"[1.5]",
|
|
"[1] null",
|
|
"[9223372036854775808]",
|
|
"[1e9999]",
|
|
] {
|
|
assert!(wire::decode_shaped::<Vec<i64>>(source).is_err(), "{source}");
|
|
}
|
|
let source = format!("[{}]", vec!["0"; document::MAX_VALUES].join(","));
|
|
assert!(
|
|
wire::decode_shaped::<Vec<i64>>(&source)
|
|
.unwrap_err()
|
|
.contains("value")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shaped_input_has_identical_acceptance_across_the_proven_byte_budget() {
|
|
for size in [
|
|
document::MAX_VALUES - 1,
|
|
document::MAX_VALUES,
|
|
document::MAX_VALUES + 1,
|
|
] {
|
|
let source = format!("[\"{}\"]", "x".repeat(size - 4));
|
|
assert_eq!(source.len(), size);
|
|
assert_eq!(
|
|
wire::decode_shaped::<Vec<String>>(&source),
|
|
wire::decode(&source)
|
|
);
|
|
let source = format!("[\"{}\\ud800\"]", "x".repeat(size - 10));
|
|
assert!(wire::decode_shaped::<Vec<String>>(&source).is_err());
|
|
}
|
|
let source = format!("[\"{}\"]", "x".repeat(document::MAX_BYTES - 4));
|
|
assert!(wire::decode_shaped::<Vec<String>>(&source).is_ok());
|
|
assert!(wire::decode_shaped::<Vec<String>>(&(source + " ")).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn single_pass_budget_counts_nested_containers_and_optional_values_once() {
|
|
use serde::Deserialize;
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Fields {
|
|
first: Vec<Option<i64>>,
|
|
second: Option<Vec<Option<i64>>>,
|
|
}
|
|
for extra in [0, 1] {
|
|
let source = format!(
|
|
"{{\"first\":[{}],\"second\":[null]}}",
|
|
vec!["null"; document::MAX_VALUES - 4 + extra].join(",")
|
|
);
|
|
assert_eq!(wire::decode::<Fields>(&source).is_ok(), extra == 0);
|
|
assert_eq!(wire::decode_shaped::<Fields>(&source).is_ok(), extra == 0);
|
|
}
|
|
let padding = " ".repeat(document::MAX_VALUES);
|
|
for source in [
|
|
r#"{"first":[],"second":null}"#,
|
|
r#"{"first":[null,1],"second":[2,null]}"#,
|
|
r#"{"first":[],"second":null,"second":[]}"#,
|
|
r#"{"first":[],"fi\u0072st":[]}"#,
|
|
r#"{"first":[],"unknown":{}}"#,
|
|
] {
|
|
let source = format!("{padding}{source}");
|
|
let normal = wire::decode::<Fields>(&source);
|
|
let shaped = wire::decode_shaped::<Fields>(&source);
|
|
assert_eq!(normal.is_ok(), shaped.is_ok());
|
|
assert_eq!(normal.ok(), shaped.ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn single_pass_budget_preserves_serde_numeric_decoding() {
|
|
let padding = " ".repeat(document::MAX_VALUES);
|
|
for source in [
|
|
"[0,-0,-0.0,1,9223372036854775807,18446744073709551615]",
|
|
"[0.1,1.2345678901234567,1e-308,5e-324,1.7976931348623157e308]",
|
|
] {
|
|
let source = format!("{padding}{source}");
|
|
let normal = wire::decode::<Vec<f64>>(&source).unwrap();
|
|
let shaped = wire::decode_shaped::<Vec<f64>>(&source).unwrap();
|
|
assert_eq!(
|
|
normal
|
|
.iter()
|
|
.map(|value| value.to_bits())
|
|
.collect::<Vec<_>>(),
|
|
shaped
|
|
.iter()
|
|
.map(|value| value.to_bits())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
for source in ["[1e9999]", "[null]", "[\"1\"]", "[NaN]"] {
|
|
assert!(wire::decode_shaped::<Vec<f64>>(&format!("{padding}{source}")).is_err());
|
|
}
|
|
let source = format!("{padding}[\"1.2300\",null,\"-1E-6176\"]");
|
|
assert_eq!(
|
|
wire::decode_shaped::<Vec<Option<dever_runtime::value::decimal::Decimal>>>(&source),
|
|
wire::decode(&source)
|
|
);
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct JsonFields {
|
|
payload: wire::json::Json,
|
|
note: Option<String>,
|
|
nested: Option<JsonRecord>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct JsonRecord {
|
|
payload: Option<wire::json::Json>,
|
|
count: i64,
|
|
}
|
|
|
|
fn decode_json_fields(source: &str) -> Result<JsonFields, String> {
|
|
wire::decode_json(source, |value: &JsonFields, budget| {
|
|
budget.open()?;
|
|
budget.value(3)?;
|
|
budget.json(&value.payload)?;
|
|
if let Some(nested) = &value.nested {
|
|
budget.open()?;
|
|
budget.value(2)?;
|
|
if let Some(payload) = &nested.payload {
|
|
budget.json(payload)?;
|
|
}
|
|
budget.close();
|
|
}
|
|
budget.close();
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn json_input_budget_distinguishes_omitted_fields_from_explicit_null_at_limit() {
|
|
for count in [document::MAX_VALUES - 4, document::MAX_VALUES - 2] {
|
|
let payload = vec!["0"; count].join(",");
|
|
for suffix in ["", ",\"note\":null", ",\"note\":null,\"nested\":null"] {
|
|
let source = format!("{{\"payload\":[{payload}]{suffix}}}");
|
|
let expected = wire::decode::<JsonFields>(&source);
|
|
let actual = decode_json_fields(&source);
|
|
assert_eq!(actual.is_ok(), expected.is_ok(), "{count}/{suffix}");
|
|
assert_eq!(actual.ok(), expected.ok());
|
|
}
|
|
}
|
|
let payload = vec!["0"; document::MAX_VALUES - 2].join(",");
|
|
assert!(decode_json_fields(&format!("{{\"payload\":[{payload}]}}")).is_ok());
|
|
assert!(decode_json_fields(&format!("{{\"payload\":[{payload}],\"note\":null}}")).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn json_input_budget_combines_fixed_records_and_multiple_json_subtrees() {
|
|
for extra in [0, 1] {
|
|
let payload = vec!["0"; document::MAX_VALUES - 6 + extra].join(",");
|
|
let source =
|
|
format!("{{\"payload\":[{payload}],\"nested\":{{\"payload\":[0],\"count\":0}}}}");
|
|
let actual = decode_json_fields(&source);
|
|
assert_eq!(actual.is_ok(), extra == 0);
|
|
assert_eq!(actual.ok(), wire::decode::<JsonFields>(&source).ok());
|
|
}
|
|
let payload = vec!["0"; document::MAX_VALUES - 4].join(",");
|
|
for (field, accepted) in [("", true), (",\"payload\":null", false)] {
|
|
let source = format!("{{\"payload\":[{payload}],\"nested\":{{\"count\":0{field}}}}}");
|
|
assert_eq!(decode_json_fields(&source).is_ok(), accepted);
|
|
assert_eq!(wire::decode::<JsonFields>(&source).is_ok(), accepted);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_input_budget_counts_empty_containers_and_relative_subtree_depth() {
|
|
for (nested, parent_depth) in [(false, 1), (true, 2)] {
|
|
for extra in [0, 1] {
|
|
let depth = document::MAX_DEPTH - parent_depth + extra;
|
|
let payload = "[".repeat(depth) + &"]".repeat(depth);
|
|
let source = if nested {
|
|
format!("{{\"payload\":null,\"nested\":{{\"payload\":{payload},\"count\":0}}}}")
|
|
} else {
|
|
format!("{{\"payload\":{payload}}}")
|
|
};
|
|
assert_eq!(decode_json_fields(&source).is_ok(), extra == 0);
|
|
assert_eq!(wire::decode::<JsonFields>(&source).is_ok(), extra == 0);
|
|
}
|
|
}
|
|
for source in [
|
|
r#"{"payload":null,"nested":null}"#,
|
|
r#"{"payload":{},"nested":{"count":0}}"#,
|
|
r#"{"payload":[],"nested":{"payload":null,"count":0}}"#,
|
|
] {
|
|
assert_eq!(
|
|
decode_json_fields(source).unwrap(),
|
|
wire::decode(source).unwrap()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_input_keeps_raw_spelling_duplicate_keys_and_strict_unicode() {
|
|
let payload = r#"{ "value": [1e9999,-0,1.00e+2,"\u96ea","\ud83d\ude42"] }"#;
|
|
let source = format!("{{\"payload\":{payload}}}");
|
|
let decoded = decode_json_fields(&source).unwrap();
|
|
assert_eq!(decoded.payload.as_str(), payload);
|
|
assert_eq!(
|
|
wire::json::Json::array(&[decoded.payload])
|
|
.unwrap()
|
|
.as_str(),
|
|
format!("[{payload}]")
|
|
);
|
|
for source in [
|
|
r#"{"payload":null,"payl\u006fad":0}"#,
|
|
r#"{"payload":null,"note":null,"note":null}"#,
|
|
r#"{"payload":{"雪":0,"\u96ea":1}}"#,
|
|
r#"{"payload":"\ud800"}"#,
|
|
r#"{"payload":null,"note":"\udc00"}"#,
|
|
r#"{"payload":null,"nested":{"count":0,"count":1}}"#,
|
|
r#"{"payload":null,"extra":0}"#,
|
|
r#"{"payload":null} {}"#,
|
|
r#"{"payload":null,}"#,
|
|
] {
|
|
assert!(decode_json_fields(source).is_err(), "{source}");
|
|
assert!(wire::decode::<JsonFields>(source).is_err(), "{source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_input_limits_original_bytes_before_deserializing() {
|
|
let prefix = r#"{"payload":null,"note":""#;
|
|
let source = format!(
|
|
"{prefix}雪{}\"}}",
|
|
" ".repeat(document::MAX_BYTES - prefix.len() - 5)
|
|
);
|
|
assert_eq!(source.len(), document::MAX_BYTES);
|
|
assert!(decode_json_fields(&source).is_ok());
|
|
let oversized = source + " ";
|
|
let mut checked = false;
|
|
let result = wire::decode_json::<JsonFields>(&oversized, |_, _| {
|
|
checked = true;
|
|
Ok(())
|
|
});
|
|
assert!(result.unwrap_err().contains("16 MiB"));
|
|
assert!(!checked);
|
|
}
|