305 lines
9.7 KiB
Rust
305 lines
9.7 KiB
Rust
use super::*;
|
|
use crate::wire::document::{MAX_BYTES, MAX_VALUES};
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Arguments {
|
|
name: String,
|
|
count: i64,
|
|
enabled: bool,
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "explicit release performance probe"]
|
|
fn form_decode_cost() {
|
|
use std::{hint::black_box, time::Instant};
|
|
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
|
for bytes in [64, 4096, 65536] {
|
|
let text = "a".repeat(bytes);
|
|
for sample in 0..15 {
|
|
for offset in 0..2 {
|
|
let native = (sample + offset) % 2 == 1;
|
|
let started = Instant::now();
|
|
let mut checksum = 0;
|
|
for _ in 0..1000 {
|
|
let fields = [
|
|
("name".into(), black_box(&text).clone()),
|
|
("count".into(), "-7".into()),
|
|
("enabled".into(), "true".into()),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
let result: Arguments = if native {
|
|
native_form(fields).unwrap()
|
|
} else {
|
|
Input::fields(fields).decode(crate::wire::decode).unwrap()
|
|
};
|
|
assert_eq!(result.count, -7);
|
|
assert!(result.enabled);
|
|
checksum += black_box(result.name.len());
|
|
}
|
|
let elapsed = started.elapsed().as_nanos();
|
|
let implementation = if native { "rust" } else { "dever" };
|
|
println!("form_{bytes},{sample},{implementation},1000,{elapsed},{checksum}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn native_form(mut fields: BTreeMap<String, String>) -> Option<Arguments> {
|
|
if fields.len() != 3 {
|
|
return None;
|
|
}
|
|
let result = Arguments {
|
|
name: fields.remove("name")?,
|
|
count: fields.remove("count")?.parse().ok()?,
|
|
enabled: fields.remove("enabled")?.parse().ok()?,
|
|
};
|
|
// 字段名固定;Int、Bool 和标点合计不超过 60 字节。
|
|
if result.name.len().saturating_mul(6).saturating_add(60) > MAX_BYTES
|
|
&& serde_json::to_vec(&result).ok()?.len() > MAX_BYTES
|
|
{
|
|
return None;
|
|
}
|
|
Some(result)
|
|
}
|
|
|
|
#[test]
|
|
fn form_fields_use_target_types_without_url_decoding() {
|
|
let fields = BTreeMap::from([
|
|
("name".into(), "%FF+雪".into()),
|
|
("count".into(), "-7".into()),
|
|
("enabled".into(), "true".into()),
|
|
]);
|
|
let parsed: Arguments = Input::fields(fields.clone())
|
|
.decode(crate::wire::decode)
|
|
.unwrap();
|
|
assert_eq!(parsed, native_form(fields.clone()).unwrap());
|
|
assert_eq!(parsed.name, "%FF+雪");
|
|
for (name, value) in [("count", "1.5"), ("enabled", "1"), ("extra", "x")] {
|
|
let mut invalid = fields.clone();
|
|
invalid.insert(name.into(), value.into());
|
|
assert!(
|
|
Input::fields(invalid.clone())
|
|
.decode::<Arguments>(crate::wire::decode)
|
|
.is_err()
|
|
);
|
|
assert!(native_form(invalid).is_none());
|
|
}
|
|
let mut missing = fields;
|
|
missing.remove("enabled");
|
|
assert!(
|
|
Input::fields(missing)
|
|
.decode::<Arguments>(crate::wire::decode)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
fn query_request(query: &str) -> Request {
|
|
hyper::Request::builder()
|
|
.uri(format!("/test?{query}"))
|
|
.body(Default::default())
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_fields_decode_without_changing_argument_rules() {
|
|
let request = query_request("name=%E9%9B%AA+test&count=-7&enabled=true");
|
|
assert_eq!(
|
|
input(&request)
|
|
.unwrap()
|
|
.decode::<Arguments>(crate::wire::decode)
|
|
.unwrap(),
|
|
Arguments {
|
|
name: "雪 test".into(),
|
|
count: -7,
|
|
enabled: true
|
|
}
|
|
);
|
|
for query in [
|
|
"name=x&name=y",
|
|
"unknown=x",
|
|
"count=1.5",
|
|
"enabled=1",
|
|
"name=%FF",
|
|
"name=%",
|
|
] {
|
|
assert!(
|
|
input(&query_request(query))
|
|
.unwrap()
|
|
.decode::<Arguments>(crate::wire::decode)
|
|
.is_err(),
|
|
"{query}"
|
|
);
|
|
}
|
|
assert!(
|
|
input(&query_request("name=x"))
|
|
.unwrap()
|
|
.decode::<Arguments>(crate::wire::decode)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn form_budgets_count_json_escaping_and_root_value() {
|
|
let mut fields = BTreeMap::new();
|
|
fields.insert("text".into(), "\0".repeat(MAX_BYTES / 6));
|
|
assert!(
|
|
Input::fields(fields)
|
|
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
|
.is_err()
|
|
);
|
|
let fields = (0..MAX_VALUES - 1)
|
|
.map(|index| (index.to_string(), "false".to_owned()))
|
|
.collect::<BTreeMap<_, _>>();
|
|
assert!(
|
|
Input::fields(fields.clone())
|
|
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
|
.is_ok()
|
|
);
|
|
let mut fields = fields;
|
|
fields.insert("extra".into(), "true".to_owned());
|
|
assert!(
|
|
Input::fields(fields)
|
|
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_rejects_bad_trailing_optional_fields_and_non_finite_numbers() {
|
|
#[derive(Debug, serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Scalars {
|
|
ratio: f64,
|
|
tag: Option<String>,
|
|
}
|
|
for query in [
|
|
"ratio=1&tag=%",
|
|
"ratio=1&tag=%FF",
|
|
"ratio=1&tag",
|
|
"ratio=1&tag=a&tag=b",
|
|
"ratio=1&extra=x",
|
|
"ratio=NaN",
|
|
"ratio=inf",
|
|
"ratio=1e999",
|
|
] {
|
|
assert!(
|
|
input(&query_request(query))
|
|
.unwrap()
|
|
.decode::<Scalars>(crate::wire::decode)
|
|
.is_err(),
|
|
"{query}"
|
|
);
|
|
}
|
|
let result: Scalars = input(&query_request("ratio=-0&tag=%E9%9B%AA+test"))
|
|
.unwrap()
|
|
.decode(crate::wire::decode)
|
|
.unwrap();
|
|
assert_eq!(result.ratio.to_bits(), (-0.0f64).to_bits());
|
|
assert_eq!(result.tag.as_deref(), Some("雪 test"));
|
|
}
|
|
|
|
#[test]
|
|
fn large_query_uses_exact_existing_json_byte_budget() {
|
|
#[derive(Debug, serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct TextArgument {
|
|
text: String,
|
|
}
|
|
let overhead = serde_json::to_vec(&serde_json::json!({"text":""}))
|
|
.unwrap()
|
|
.len();
|
|
for (plain, count) in [
|
|
(true, MAX_BYTES / 6 + 1),
|
|
(true, MAX_BYTES - overhead),
|
|
(true, MAX_BYTES - overhead + 1),
|
|
(false, (MAX_BYTES - overhead) / 6),
|
|
(false, (MAX_BYTES - overhead) / 6 + 1),
|
|
] {
|
|
let text = if plain { "a" } else { "\0" }.repeat(count);
|
|
let encoded = if plain { "a" } else { "%00" }.repeat(count);
|
|
let source = serde_json::to_string(&serde_json::json!({"text": text})).unwrap();
|
|
let expected = crate::wire::decode::<TextArgument>(&source);
|
|
let fields = BTreeMap::from([("text".into(), text)]);
|
|
let form = Input::fields(fields).decode::<TextArgument>(crate::wire::decode);
|
|
assert_eq!(form.is_ok(), expected.is_ok());
|
|
if let (Ok(form), Ok(expected)) = (&form, &expected) {
|
|
assert_eq!(form.text, expected.text);
|
|
}
|
|
// 此处独立验证字段 codec 的 JSON 预算;超长 URI 会先被 HTTP 库拒绝。
|
|
let query = format!("text={encoded}");
|
|
let actual = Input(Data::Query(&query)).decode::<TextArgument>(crate::wire::decode);
|
|
assert_eq!(
|
|
actual.is_ok(),
|
|
expected.is_ok(),
|
|
"plain={plain}, count={count}"
|
|
);
|
|
if let (Ok(actual), Ok(expected)) = (actual, expected) {
|
|
assert_eq!(actual.text, expected.text);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "calendar", feature = "uuid"))]
|
|
#[test]
|
|
fn query_uses_existing_scalar_codecs_and_optional_types() {
|
|
use crate::{
|
|
time::calendar::{Date, DateTime, Duration, Time},
|
|
value::{decimal::Decimal, id::Id, uuid::Uuid},
|
|
};
|
|
|
|
#[derive(Debug, PartialEq, serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Scalars {
|
|
decimal: Decimal,
|
|
id: Id,
|
|
uuid: Uuid,
|
|
date: Date,
|
|
time: Time,
|
|
datetime: DateTime,
|
|
duration: Duration,
|
|
count: Option<i64>,
|
|
enabled: Option<bool>,
|
|
ratio: Option<f64>,
|
|
}
|
|
|
|
let fields = serde_json::json!({
|
|
"decimal": "123456789.0123456789",
|
|
"id": "雪",
|
|
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
"date": "2026-10-26",
|
|
"time": "12:34:56.789",
|
|
"datetime": "2026-10-26T12:34:56Z",
|
|
"duration": -1500,
|
|
"count": 7,
|
|
"enabled": true,
|
|
"ratio": 1.25
|
|
});
|
|
let expected: Scalars = crate::wire::decode(&fields.to_string()).unwrap();
|
|
let query = "decimal=123456789.0123456789&id=%E9%9B%AA&uuid=550e8400-e29b-41d4-a716-446655440000&date=2026-10-26&time=12:34:56.789&datetime=2026-10-26T12:34:56Z&duration=-1500&count=7&enabled=true&ratio=1.25";
|
|
let actual: Scalars = input(&query_request(query))
|
|
.unwrap()
|
|
.decode(crate::wire::decode)
|
|
.unwrap();
|
|
assert_eq!(actual, expected);
|
|
for (valid, invalid) in [
|
|
("123456789.0123456789", "NaN"),
|
|
("550e8400-e29b-41d4-a716-446655440000", "bad-uuid"),
|
|
("date=2026-10-26", "date=2026-02-30"),
|
|
("time=12:34:56.789", "time=25:34:56.789"),
|
|
("duration=-1500", "duration=1.5"),
|
|
("count=7", "count=9223372036854775808"),
|
|
("enabled=true", "enabled=yes"),
|
|
("ratio=1.25", "ratio=NaN"),
|
|
] {
|
|
assert!(
|
|
input(&query_request(&query.replace(valid, invalid)))
|
|
.unwrap()
|
|
.decode::<Scalars>(crate::wire::decode)
|
|
.is_err(),
|
|
"{invalid}"
|
|
);
|
|
}
|
|
}
|