483 lines
16 KiB
Rust
483 lines
16 KiB
Rust
use dever_runtime::wire::{document, json::Json};
|
|
|
|
#[path = "json/encode.rs"]
|
|
mod encode;
|
|
#[path = "json/input.rs"]
|
|
mod input;
|
|
|
|
#[test]
|
|
fn fixed_wire_keeps_syntax_and_byte_limits_without_a_second_parse() {
|
|
use dever_runtime::wire;
|
|
for source in [r#""\ud800""#, r#""\udc00""#, r#""x" null"#] {
|
|
assert!(wire::decode_fixed::<String>(source).is_err(), "{source}");
|
|
}
|
|
let source = format!("\"{}\"", "x".repeat(document::MAX_BYTES - 2));
|
|
assert_eq!(
|
|
wire::decode_fixed::<String>(&source).unwrap().len(),
|
|
document::MAX_BYTES - 2
|
|
);
|
|
assert!(wire::decode_fixed::<String>(&(source + " ")).is_err());
|
|
let overhead = wire::response_fixed(&"").unwrap().len();
|
|
let value = "x".repeat(document::MAX_BYTES - overhead);
|
|
assert_eq!(
|
|
wire::response_fixed(&value).unwrap().len(),
|
|
document::MAX_BYTES
|
|
);
|
|
assert_eq!(
|
|
wire::response_fixed_bytes(&value).unwrap().len(),
|
|
document::MAX_BYTES
|
|
);
|
|
let oversized = value + "x";
|
|
assert!(wire::response_fixed(&oversized).is_err());
|
|
assert!(wire::response_fixed_bytes(&oversized).is_err());
|
|
let value = "雪\"\\\n";
|
|
assert_eq!(wire::response_fixed(&value), wire::response(&value));
|
|
assert_eq!(
|
|
wire::response_fixed_bytes(&value).unwrap(),
|
|
wire::response(&value).unwrap().into_bytes()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_keeps_numbers_order_and_node_ids() {
|
|
let source = r#"{"b":1e+03,"a":[null,"雪",-0]}"#;
|
|
let document = document::parse(source).unwrap();
|
|
assert_eq!(document.root, 5);
|
|
assert_eq!(document::stringify(&document).unwrap(), source);
|
|
assert_eq!(document::property(&document, document.root, "b"), Some(0));
|
|
assert_eq!(document::element(&document, 4, 1), Some(2));
|
|
for source in ["{}", "[{},[]]", r#"{"":{}}"#] {
|
|
let parsed = document::parse(source).unwrap();
|
|
assert_eq!(document::stringify(&parsed).unwrap(), source);
|
|
assert_eq!(document::property(&parsed, parsed.root, "missing"), None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_rejects_duplicates_surrogates_and_trailing_content() {
|
|
for source in [
|
|
r#"{"name":1,"name":2}"#,
|
|
r#""\ud800""#,
|
|
r#"[1,]"#,
|
|
"null false",
|
|
r#""\udc00""#,
|
|
r#""\ud800\u0061""#,
|
|
r#""\u00""#,
|
|
"\"雪\n\"",
|
|
] {
|
|
assert!(document::parse(source).is_err(), "{source}");
|
|
}
|
|
let duplicate = r#"{"雪":0,"\u96ea":[}"#;
|
|
let error = document::parse(duplicate).unwrap_err();
|
|
assert_eq!(error.message, "duplicate JSON object key");
|
|
assert_eq!(
|
|
error.position,
|
|
duplicate[..duplicate.rfind(':').unwrap()].chars().count()
|
|
);
|
|
assert!(
|
|
Json::parse(duplicate)
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("duplicate JSON object key")
|
|
);
|
|
assert_eq!(
|
|
document::parse(r#""\ud83d\ude42""#).unwrap().nodes,
|
|
vec![document::Value::String("🙂".into())]
|
|
);
|
|
for text in ["", "plain text", "雪🙂", "雪\"\\\n\0🙂", "\u{08}\u{0c}\r\t"] {
|
|
let source = serde_json::to_string(text).unwrap();
|
|
let parsed = document::parse(&source).unwrap();
|
|
assert_eq!(parsed.nodes, vec![document::Value::String(text.into())]);
|
|
assert_eq!(document::stringify(&parsed).unwrap(), source);
|
|
assert_eq!(Json::parse(&source).unwrap().as_text().unwrap(), text);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn document_string_escaping_matches_serde_for_unicode_and_control_boundaries() {
|
|
let encode = |text: String| {
|
|
let expected = serde_json::to_string(&text).unwrap();
|
|
let value = document::Document {
|
|
root: 0,
|
|
nodes: vec![document::Value::String(text)],
|
|
};
|
|
assert_eq!(document::stringify(&value).unwrap(), expected);
|
|
assert_eq!(document::parse(&expected).unwrap().nodes, value.nodes);
|
|
};
|
|
encode(
|
|
(0..=u32::from(char::MAX))
|
|
.filter_map(char::from_u32)
|
|
.collect(),
|
|
);
|
|
encode(
|
|
(128..=u32::from(char::MAX))
|
|
.filter_map(char::from_u32)
|
|
.collect(),
|
|
);
|
|
for character in (0..32).map(char::from).chain(['"', '\\']) {
|
|
for text in [
|
|
format!("雪🙂{character}𐀀a{character}"),
|
|
format!("{character}{character}雪🙂"),
|
|
] {
|
|
encode(text);
|
|
}
|
|
}
|
|
encode(String::new());
|
|
}
|
|
|
|
#[test]
|
|
fn json_property_update_and_scalar_reads_preserve_wire_text() {
|
|
let value = Json::parse(r#"{"a":1,"b":[true]}"#).unwrap();
|
|
assert_eq!(value.property("a").unwrap().unwrap().as_integer(), Ok(1));
|
|
assert_eq!(value.property("missing").unwrap(), None);
|
|
let updated = value
|
|
.set_property("a", &Json::parse("1.000e2").unwrap())
|
|
.unwrap();
|
|
assert_eq!(updated.as_str(), r#"{"a":1.000e2,"b":[true]}"#);
|
|
assert_eq!(
|
|
Json::parse(r#""hello""#).unwrap().as_text(),
|
|
Ok("hello".into())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_enforces_depth_and_value_budgets() {
|
|
let nested = "[".repeat(document::MAX_DEPTH) + &"]".repeat(document::MAX_DEPTH);
|
|
assert!(document::parse(&nested).is_ok());
|
|
let too_deep = "[".repeat(document::MAX_DEPTH + 1) + &"]".repeat(document::MAX_DEPTH + 1);
|
|
assert!(document::parse(&too_deep).is_err());
|
|
let allowed = format!("[{}0]", "0,".repeat(document::MAX_VALUES - 2));
|
|
assert!(document::parse(&allowed).is_ok());
|
|
let many = format!("[{}0]", "0,".repeat(document::MAX_VALUES - 1));
|
|
assert!(document::parse(&many).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_utf8_escape_reports_a_codepoint_position_without_panicking() {
|
|
for source in [r#""\雪""#, r#""\u0雪雪""#] {
|
|
assert!(document::parse(source).is_err());
|
|
}
|
|
assert_eq!(document::parse("[\"雪\",]").unwrap_err().position, 5);
|
|
for source in ["\"plain", "\"雪🙂", "{\"雪🙂"] {
|
|
let error = document::parse(source).unwrap_err();
|
|
assert_eq!(error.message, "unterminated JSON string");
|
|
assert_eq!(error.position, source.chars().count());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn manual_documents_reject_forward_references() {
|
|
let document = document::Document {
|
|
root: 1,
|
|
nodes: vec![document::Value::Null, document::Value::Array(vec![1])],
|
|
};
|
|
assert!(document::stringify(&document).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn manual_document_numbers_preserve_json_lexical_rules() {
|
|
let encode = |raw: &str| {
|
|
document::stringify(&document::Document {
|
|
root: 0,
|
|
nodes: vec![document::Value::Number(raw.into())],
|
|
})
|
|
};
|
|
for raw in [
|
|
"0",
|
|
"-0",
|
|
"1.00",
|
|
"-0.125E+03",
|
|
"1e9999",
|
|
"1234567890123456789012345678901234e-12",
|
|
] {
|
|
assert_eq!(encode(raw).unwrap(), raw);
|
|
}
|
|
for raw in [
|
|
"",
|
|
"-",
|
|
"+1",
|
|
"01",
|
|
"-01",
|
|
"0x1",
|
|
"NaN",
|
|
"1.",
|
|
".1",
|
|
"1e",
|
|
"1e+",
|
|
"1_0",
|
|
" 1",
|
|
"1 ",
|
|
"1\n",
|
|
"null",
|
|
"[]",
|
|
"1true",
|
|
"1雪",
|
|
"0],\"x\":true",
|
|
] {
|
|
let error = encode(raw).unwrap_err();
|
|
assert_eq!(error.node, 0, "{raw}");
|
|
assert_eq!(error.message, "invalid JSON number", "{raw}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_and_array_encoding_reuses_the_same_budgeted_parser() {
|
|
assert_eq!(Json::boolean(true).unwrap().as_str(), "true");
|
|
assert_eq!(Json::integer(42).unwrap().as_str(), "42");
|
|
assert_eq!(Json::string("雪").unwrap().as_str(), "\"雪\"");
|
|
assert!(Json::float(f64::NAN).is_err());
|
|
let array = Json::array(&[Json::integer(1).unwrap(), Json::boolean(false).unwrap()]).unwrap();
|
|
assert_eq!(array.as_str(), "[1,false]");
|
|
}
|
|
|
|
#[test]
|
|
fn source_document_keeps_the_256_container_contract() {
|
|
let nested = "[".repeat(256) + &"]".repeat(256);
|
|
let parsed = document::parse_with_depth(&nested, document::DOCUMENT_MAX_DEPTH).unwrap();
|
|
assert_eq!(
|
|
document::stringify_with_depth(&parsed, document::DOCUMENT_MAX_DEPTH).unwrap(),
|
|
nested
|
|
);
|
|
assert!(document::stringify_with_depth(&parsed, 255).is_err());
|
|
let too_deep = "[".repeat(257) + &"]".repeat(257);
|
|
assert!(document::parse_with_depth(&too_deep, document::DOCUMENT_MAX_DEPTH).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn raw_properties_preserve_subtree_bytes_and_distinguish_null_from_missing() {
|
|
let value = Json::parse(r#"{"payload": [ 1e9999, -0, "\u96ea" ], "empty": null}"#).unwrap();
|
|
assert_eq!(
|
|
value.property("payload").unwrap().unwrap().as_str(),
|
|
r#"[ 1e9999, -0, "\u96ea" ]"#
|
|
);
|
|
assert_eq!(value.property("empty").unwrap().unwrap().as_str(), "null");
|
|
assert_eq!(value.property("missing").unwrap(), None);
|
|
assert!(Json::integer(1).unwrap().property("key").is_err());
|
|
assert!(
|
|
Json::parse("[]")
|
|
.unwrap()
|
|
.set_property("key", &value)
|
|
.is_err()
|
|
);
|
|
let updated = value
|
|
.set_property("empty", &Json::boolean(true).unwrap())
|
|
.unwrap();
|
|
assert!(updated.as_str().contains(r#"[ 1e9999, -0, "\u96ea" ]"#));
|
|
assert_eq!(value.property("empty").unwrap().unwrap().as_str(), "null");
|
|
}
|
|
|
|
#[test]
|
|
fn property_keys_compare_decoded_text_without_changing_subtree_spelling() {
|
|
let source =
|
|
r#" {"\u0061": [ 1e9999, -0 ], "雪\"\\\n": "\u96ea", "\ud83d\ude42": null, "last": true} "#;
|
|
let value = Json::parse(source).unwrap();
|
|
for (key, expected) in [
|
|
("a", Some("[ 1e9999, -0 ]")),
|
|
("雪\"\\\n", Some(r#""\u96ea""#)),
|
|
("🙂", Some("null")),
|
|
("last", Some("true")),
|
|
(r"\u0061", None),
|
|
("missing", None),
|
|
] {
|
|
assert_eq!(
|
|
value.property(key).unwrap().as_ref().map(Json::as_str),
|
|
expected,
|
|
"{key}"
|
|
);
|
|
}
|
|
assert_eq!(value.as_str(), source);
|
|
for source in [r#"{"a":1,"\u0061":2}"#, r#"{"a":1,"tail":[}"#] {
|
|
assert!(Json::parse(source).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn shared_wire_checks_duplicate_names_and_all_three_budgets() {
|
|
use dever_runtime::wire;
|
|
for source in [r#"{"a":1,"\u0061":2}"#, r#"[{"a":1,"a":2}]"#, r#""\ud800""#] {
|
|
assert!(Json::parse(source).is_err(), "{source}");
|
|
assert!(
|
|
wire::decode::<serde_json::Value>(source).is_err(),
|
|
"{source}"
|
|
);
|
|
}
|
|
let maximum = format!("[{}0]", "0,".repeat(document::MAX_VALUES - 2));
|
|
assert!(Json::parse(&maximum).is_ok());
|
|
assert!(wire::decode::<Vec<i64>>(&maximum).is_ok());
|
|
let excessive = format!("[{}0]", "0,".repeat(document::MAX_VALUES - 1));
|
|
assert!(Json::parse(&excessive).is_err());
|
|
assert!(wire::decode::<Vec<i64>>(&excessive).is_err());
|
|
let nested = "[".repeat(document::MAX_DEPTH) + &"]".repeat(document::MAX_DEPTH);
|
|
assert!(Json::parse(&nested).is_ok());
|
|
assert!(Json::parse(&format!("[{nested}]")).is_err());
|
|
assert!(wire::response(&serde_json::from_str::<serde_json::Value>(&nested).unwrap()).is_err());
|
|
assert!(Json::string(&"x".repeat(document::MAX_BYTES - 2)).is_ok());
|
|
assert!(Json::string(&"x".repeat(document::MAX_BYTES - 1)).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn manual_document_shared_subgraphs_are_bounded_before_expansion() {
|
|
let mut graph = document::Document {
|
|
root: 0,
|
|
nodes: vec![document::Value::Null],
|
|
};
|
|
for index in 1..40 {
|
|
graph.nodes.push(document::Value::Array(vec![
|
|
index as i64 - 1,
|
|
index as i64 - 1,
|
|
]));
|
|
graph.root = index;
|
|
}
|
|
let error = document::stringify(&graph).unwrap_err();
|
|
assert!(error.node < 20, "{error}");
|
|
assert!(error.message.contains("65536"), "{error}");
|
|
let oversized = document::Document {
|
|
root: 1,
|
|
nodes: vec![
|
|
document::Value::String("x".repeat(document::MAX_BYTES / 2)),
|
|
document::Value::Array(vec![0, 0]),
|
|
],
|
|
};
|
|
assert!(
|
|
document::stringify(&oversized)
|
|
.unwrap_err()
|
|
.message
|
|
.contains("16 MiB")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn replacing_a_field_does_not_accumulate_obsolete_document_nodes() {
|
|
let source = format!(
|
|
r#"{{"count":0,"values":[{}0]}}"#,
|
|
"0,".repeat(document::MAX_VALUES - 4)
|
|
);
|
|
let original = Json::parse(&source).unwrap();
|
|
let updated = original
|
|
.set_property("count", &Json::integer(1).unwrap())
|
|
.unwrap();
|
|
assert_eq!(
|
|
updated.property("count").unwrap().unwrap().as_integer(),
|
|
Ok(1)
|
|
);
|
|
assert_eq!(
|
|
original.property("count").unwrap().unwrap().as_integer(),
|
|
Ok(0)
|
|
);
|
|
assert!(
|
|
updated
|
|
.set_property("extra", &Json::null().unwrap())
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_record_encoding_keeps_byte_limits_and_escaping() {
|
|
#[derive(serde::Serialize)]
|
|
struct Record<'value> {
|
|
text: &'value str,
|
|
}
|
|
for text in ["", "雪\"\\\n"] {
|
|
let value = Record { text };
|
|
assert_eq!(Json::encode_proven(&value, 2, 1), Json::encode(&value));
|
|
}
|
|
let text = "x".repeat(document::MAX_BYTES - r#"{"text":""}"#.len());
|
|
assert_eq!(
|
|
Json::encode(&Record { text: &text })
|
|
.unwrap()
|
|
.as_str()
|
|
.len(),
|
|
document::MAX_BYTES
|
|
);
|
|
assert!(
|
|
Json::encode(&Record {
|
|
text: &(text + "x")
|
|
})
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decimal_wire_is_an_exact_string_not_a_floating_point_value() {
|
|
use dever_runtime::{value::decimal::Decimal, wire};
|
|
let value = wire::decode::<Decimal>(r#""1234567890123456789012345678901234""#).unwrap();
|
|
assert_eq!(
|
|
Json::encode(&value).unwrap().as_str(),
|
|
r#""1234567890123456789012345678901234""#
|
|
);
|
|
assert!(wire::decode::<Decimal>("1.25").is_err());
|
|
assert!(wire::decode::<Decimal>(r#""NaN""#).is_err());
|
|
assert!(wire::decode::<Decimal>(r#""12345678901234567890123456789012345""#).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn decimal_nullable_lists_preserve_extremes_and_normalized_output() {
|
|
use dever_runtime::{value::decimal::Decimal, wire};
|
|
let values = [
|
|
"-0.00",
|
|
"1.2300",
|
|
"1E-6176",
|
|
"9.999999999999999999999999999999999E6144",
|
|
];
|
|
let mut numbers = values
|
|
.map(|value| Some(Decimal::parse(value).unwrap()))
|
|
.to_vec();
|
|
numbers.push(None);
|
|
let encoded = Json::encode(&numbers).unwrap();
|
|
let expected = [
|
|
Some("0"),
|
|
Some("1.23"),
|
|
Some("1E-6176"),
|
|
Some("9.999999999999999999999999999999999E+6144"),
|
|
None,
|
|
];
|
|
assert_eq!(encoded.as_str(), serde_json::to_string(&expected).unwrap());
|
|
assert_eq!(
|
|
wire::decode::<Vec<Option<Decimal>>>(encoded.as_str()).unwrap(),
|
|
numbers
|
|
);
|
|
assert!(wire::decode::<Vec<Decimal>>(encoded.as_str()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn raw_deserialization_preserves_subtree_text_and_enforces_bounds() {
|
|
let source = r#" {"huge": 1e9999, "text": "\u96ea", "array": [ 1, 2 ]} "#;
|
|
let value: Json = serde_json::from_str(source).unwrap();
|
|
assert_eq!(value.as_str(), source.trim());
|
|
assert_eq!(Json::parse(source).unwrap().as_str(), source);
|
|
let parsed = Json::parse(source.trim()).unwrap();
|
|
assert_eq!(value, parsed);
|
|
for value in [value, parsed] {
|
|
assert_eq!(format!("{value:?}"), format!("Json({:?})", source.trim()));
|
|
assert_eq!(
|
|
Json::encode_with(|output| output.json(&value))
|
|
.unwrap()
|
|
.as_str(),
|
|
source.trim()
|
|
);
|
|
assert_eq!(value.clone().into_text(), source.trim());
|
|
let child = value.property("array").unwrap().unwrap();
|
|
assert_eq!(
|
|
Json::encode_with(|output| output.json(&child))
|
|
.unwrap()
|
|
.as_str(),
|
|
"[ 1, 2 ]"
|
|
);
|
|
let updated = value.set_property("array", &child).unwrap();
|
|
assert_eq!(updated.property("array").unwrap().as_ref(), Some(&child));
|
|
assert_eq!(
|
|
updated.property("huge").unwrap().unwrap().as_str(),
|
|
"1e9999"
|
|
);
|
|
}
|
|
for source in [
|
|
r#"{"a":1,"\u0061":2}"#.to_owned(),
|
|
r#"["\ud800"]"#.to_owned(),
|
|
"[".repeat(65) + &"]".repeat(65),
|
|
format!("[{}0]", "0,".repeat(document::MAX_VALUES - 1)),
|
|
format!("\"{}\"", "x".repeat(document::MAX_BYTES - 1)),
|
|
] {
|
|
assert!(serde_json::from_str::<Json>(&source).is_err());
|
|
}
|
|
}
|