Files
core/test/rust/runtime/http/input.rs
T
2026-11-03 03:32:07 +00:00

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}"
);
}
}