Skip to content

Commit 45ca021

Browse files
authored
Merge pull request #167 from wingo/http-fields
Add tests for HTTP fields
2 parents bcd3687 + 3c9d0cf commit 45ca021

2 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"proposals": ["http"],
3+
"operations": [
4+
{
5+
"type": "run"
6+
},
7+
{
8+
"type": "wait"
9+
}
10+
]
11+
}
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
extern crate wit_bindgen;
2+
3+
wit_bindgen::generate!({
4+
inline: r"
5+
package test:test;
6+
7+
world test {
8+
import wasi:http/types@0.3.0-rc-2026-02-09;
9+
include wasi:cli/command@0.3.0-rc-2026-02-09;
10+
}
11+
",
12+
additional_derives: [PartialEq, Eq, Hash, Clone],
13+
features:["clocks-timezone"],
14+
generate_all
15+
});
16+
17+
use wasi::http::types::Fields;
18+
use wasi::http::types::HeaderError;
19+
20+
fn test_empty_fields_inner(fields: Fields) {
21+
assert!(!fields.has("foo"));
22+
assert!(fields.get("foo").is_empty());
23+
assert!(fields.get_and_delete("foo").unwrap().is_empty());
24+
fields.delete("foo").unwrap();
25+
fields.delete("other").unwrap();
26+
assert!(fields.copy_all().is_empty());
27+
}
28+
29+
fn test_empty_fields() {
30+
let fields = Fields::new();
31+
let clone = fields.clone();
32+
test_empty_fields_inner(fields);
33+
test_empty_fields_inner(clone);
34+
test_empty_fields_inner(Fields::from_list(&[]).unwrap());
35+
}
36+
37+
fn test_fields_with_foo_inner(fields: Fields) {
38+
assert!(fields.has("foo"));
39+
assert_eq!(fields.get("foo"), [b"bar".to_vec()]);
40+
fields.delete("foo").unwrap();
41+
assert!(!fields.has("foo"));
42+
assert!(fields.get("foo").is_empty());
43+
44+
fields.set("foo", &[]).unwrap();
45+
assert!(!fields.has("foo"));
46+
assert!(fields.get("foo").is_empty());
47+
fields
48+
.set("foo", &[b"bar".to_vec(), b"baz".to_vec()])
49+
.unwrap();
50+
assert!(fields.has("foo"));
51+
assert_eq!(fields.get("foo"), [b"bar".to_vec(), b"baz".to_vec()]);
52+
assert_eq!(
53+
fields.get_and_delete("foo").unwrap(),
54+
[b"bar".to_vec(), b"baz".to_vec()]
55+
);
56+
assert!(fields.get_and_delete("foo").unwrap().is_empty());
57+
58+
fields
59+
.set("foo", &[b"bar".to_vec(), b"baz".to_vec()])
60+
.unwrap();
61+
assert!(fields.has("foo"));
62+
assert_eq!(fields.get("foo"), [b"bar".to_vec(), b"baz".to_vec()]);
63+
assert_eq!(
64+
fields.get_and_delete("foo").unwrap(),
65+
[b"bar".to_vec(), b"baz".to_vec()]
66+
);
67+
assert!(fields.get_and_delete("foo").unwrap().is_empty());
68+
assert!(!fields.has("foo"));
69+
70+
fields.append("foo", b"bar").unwrap();
71+
fields.append("foo", b"baz").unwrap();
72+
assert!(fields.has("foo"));
73+
assert_eq!(fields.get("foo"), [b"bar".to_vec(), b"baz".to_vec()]);
74+
assert_eq!(
75+
fields.get_and_delete("foo").unwrap(),
76+
[b"bar".to_vec(), b"baz".to_vec()]
77+
);
78+
assert!(fields.get_and_delete("foo").unwrap().is_empty());
79+
assert!(!fields.has("foo"));
80+
}
81+
82+
fn test_fields_with_foo() {
83+
let fields = Fields::from_list(&[("foo".to_string(), b"bar".to_vec())]).unwrap();
84+
let clone = fields.clone();
85+
test_fields_with_foo_inner(fields);
86+
test_fields_with_foo_inner(clone);
87+
}
88+
89+
fn test_invalid_field_name(field: &str) {
90+
let fields = Fields::new();
91+
assert!(!fields.has(field));
92+
assert!(fields.get(field).is_empty());
93+
assert_eq!(fields.delete(field), Err(HeaderError::InvalidSyntax));
94+
assert_eq!(
95+
fields.get_and_delete(field),
96+
Err(HeaderError::InvalidSyntax)
97+
);
98+
assert_eq!(
99+
fields.set(field, &[b"val".to_vec()]),
100+
Err(HeaderError::InvalidSyntax)
101+
);
102+
assert_eq!(
103+
fields.append(field, b"val"),
104+
Err(HeaderError::InvalidSyntax)
105+
);
106+
assert!(fields.copy_all().is_empty());
107+
assert!(!fields.has(field));
108+
assert!(fields.get(field).is_empty());
109+
110+
assert_eq!(
111+
Fields::from_list(&[(field.to_string(), b"val".to_vec())]).unwrap_err(),
112+
HeaderError::InvalidSyntax
113+
);
114+
}
115+
116+
fn test_valid_field_name(field: &str) {
117+
let fields = Fields::new();
118+
assert!(!fields.has(field));
119+
assert!(fields.get(field).is_empty());
120+
fields.delete(field).unwrap();
121+
assert!(fields.get_and_delete(field).unwrap().is_empty());
122+
fields.set(field, &[b"val".to_vec()]).unwrap();
123+
fields.append(field, b"val2").unwrap();
124+
assert_eq!(
125+
fields.copy_all(),
126+
[
127+
(field.to_string(), b"val".to_vec()),
128+
(field.to_string(), b"val2".to_vec())
129+
]
130+
);
131+
assert_eq!(
132+
Fields::from_list(&[
133+
(field.to_string(), b"val".to_vec()),
134+
(field.to_string(), b"val2".to_vec())
135+
])
136+
.unwrap()
137+
.copy_all(),
138+
fields.clone().copy_all()
139+
);
140+
}
141+
142+
fn test_invalid_field_value(val: &[u8]) {
143+
let fields = Fields::new();
144+
assert_eq!(
145+
fields.set("foo", &[val.to_vec()]),
146+
Err(HeaderError::InvalidSyntax)
147+
);
148+
assert_eq!(fields.append("foo", val), Err(HeaderError::InvalidSyntax));
149+
assert_eq!(
150+
Fields::from_list(&[("foo".to_string(), val.to_vec())]).unwrap_err(),
151+
HeaderError::InvalidSyntax
152+
);
153+
}
154+
155+
fn test_valid_field_value(val: &[u8]) {
156+
let fields = Fields::new();
157+
fields.set("foo", &[val.to_vec()]).unwrap();
158+
fields.append("foo", val).unwrap();
159+
assert_eq!(
160+
fields.copy_all(),
161+
[
162+
("foo".to_string(), val.to_vec()),
163+
("foo".to_string(), val.to_vec())
164+
]
165+
);
166+
assert_eq!(
167+
Fields::from_list(&[
168+
("foo".to_string(), val.to_vec()),
169+
("foo".to_string(), val.to_vec())
170+
])
171+
.unwrap()
172+
.copy_all(),
173+
fields.clone().copy_all()
174+
);
175+
}
176+
177+
fn compute_valid_field_name_chars(len: usize) -> Vec<bool> {
178+
// https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2
179+
// field-name = token
180+
// token = 1*tchar
181+
//
182+
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
183+
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
184+
// / DIGIT / ALPHA
185+
// ; any VCHAR, except delimiters
186+
let mut ret = Vec::<bool>::new();
187+
ret.resize(len, false);
188+
for ch in "!#$%&'*+-.^_`|~".chars() {
189+
ret[ch as usize] = true;
190+
}
191+
for ch in 'a'..='z' {
192+
ret[ch as usize] = true;
193+
}
194+
for ch in 'A'..='Z' {
195+
ret[ch as usize] = true;
196+
}
197+
for ch in '0'..='9' {
198+
ret[ch as usize] = true;
199+
}
200+
ret
201+
}
202+
203+
fn test_invalid_field_names() {
204+
test_invalid_field_name("");
205+
test_invalid_field_name("voilà");
206+
test_invalid_field_name("hey ho");
207+
test_invalid_field_name(" ");
208+
test_invalid_field_name(" hey");
209+
test_invalid_field_name("hey ");
210+
test_invalid_field_name("(what)");
211+
test_invalid_field_name("[what]");
212+
test_invalid_field_name("{what}");
213+
// https://github.com/bytecodealliance/wasmtime/issues/11771
214+
// test_invalid_field_name("\"foo\"");
215+
let max_codepoint_to_test = 1024;
216+
let valid = compute_valid_field_name_chars(max_codepoint_to_test);
217+
for ch in 0..max_codepoint_to_test {
218+
if !valid[ch] {
219+
let ch = char::from_u32(ch as u32).unwrap();
220+
if ch == '"' {
221+
// https://github.com/bytecodealliance/wasmtime/issues/11771
222+
continue;
223+
}
224+
test_invalid_field_name(&String::from(ch));
225+
}
226+
}
227+
}
228+
229+
fn test_valid_field_names() {
230+
let max_codepoint_to_test = 1024;
231+
let valid = compute_valid_field_name_chars(max_codepoint_to_test);
232+
for ch in 0..max_codepoint_to_test {
233+
if valid[ch] {
234+
let ch = char::from_u32(ch as u32).unwrap();
235+
if ch.is_uppercase() {
236+
// https://github.com/bytecodealliance/wasmtime/issues/11770
237+
continue;
238+
}
239+
test_valid_field_name(&String::from(ch));
240+
}
241+
}
242+
243+
test_valid_field_name("1");
244+
test_valid_field_name("142");
245+
// https://github.com/bytecodealliance/wasmtime/issues/11770
246+
// test_valid_field_name("Foo");
247+
// test_valid_field_name("ConnectionLevel142");
248+
test_valid_field_name("kebab-data-100");
249+
test_valid_field_name(str::from_utf8(&[b"f"[0]; 1024]).unwrap());
250+
}
251+
252+
fn compute_valid_field_value_bytes() -> Vec<bool> {
253+
// https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2
254+
// field-value = *field-content
255+
// field-content = field-vchar
256+
// [ 1*( SP / HTAB / field-vchar ) field-vchar ]
257+
// field-vchar = VCHAR / obs-text
258+
// VCHAR = %x21-7E
259+
// obs-text = %x80-FF
260+
let mut ret = Vec::<bool>::new();
261+
ret.resize(256, false);
262+
ret[' ' as usize] = true;
263+
ret['\t' as usize] = true;
264+
for ch in 0x21..=0x7e {
265+
ret[ch] = true;
266+
}
267+
for ch in 0x80..=0xff {
268+
ret[ch] = true;
269+
}
270+
ret
271+
}
272+
273+
fn test_invalid_field_values() {
274+
let valid = compute_valid_field_value_bytes();
275+
for byte in 0u8..=0xff {
276+
if !valid[byte as usize] {
277+
test_invalid_field_value(&[byte]);
278+
}
279+
}
280+
281+
test_invalid_field_value(b"\n");
282+
test_invalid_field_value(b"\r");
283+
test_invalid_field_value(b"\0");
284+
}
285+
286+
fn test_valid_field_values() {
287+
let valid = compute_valid_field_value_bytes();
288+
for byte in 0u8..=0xff {
289+
if valid[byte as usize] {
290+
test_valid_field_value(&[byte])
291+
}
292+
}
293+
294+
test_valid_field_value(b"");
295+
test_valid_field_value(b" \t \t \t \t \t ");
296+
test_valid_field_value(b"Foo");
297+
test_valid_field_value(b"ConnectionLevel142");
298+
test_valid_field_value(b"kebab-data-100");
299+
test_valid_field_value(&[b"f"[0]; 1024]);
300+
}
301+
302+
fn test_field_name_case_insensitivity() {
303+
let lower = "foo";
304+
let upper = "FOO";
305+
306+
let fields = Fields::new();
307+
fields.append(lower, b"val1").unwrap();
308+
assert!(fields.has(lower));
309+
assert!(fields.has(upper));
310+
assert_eq!(fields.get(lower), fields.get(upper));
311+
fields.delete(upper).unwrap();
312+
assert!(!fields.has(lower));
313+
assert!(!fields.has(upper));
314+
315+
fields.append(upper, b"val1").unwrap();
316+
assert!(fields.has(upper));
317+
assert!(fields.has(lower));
318+
assert_eq!(fields.get(lower), fields.get(upper));
319+
fields.delete(lower).unwrap();
320+
assert!(!fields.has(upper));
321+
assert!(!fields.has(lower));
322+
323+
fields.append(lower, b"val1").unwrap();
324+
fields.append(upper, b"val2").unwrap();
325+
assert_eq!(
326+
fields.copy_all(),
327+
[
328+
(lower.to_string(), b"val1".to_vec()),
329+
(lower.to_string(), b"val2".to_vec())
330+
]
331+
);
332+
assert_eq!(
333+
fields.get_and_delete(upper).unwrap(),
334+
[b"val1".to_vec(), b"val2".to_vec()]
335+
);
336+
337+
fields.append(upper, b"val2").unwrap();
338+
fields.append(lower, b"val1").unwrap();
339+
// https://github.com/bytecodealliance/wasmtime/issues/11770
340+
// assert_eq!(fields.copy_all(),
341+
// [(upper.to_string(), b"val2".to_vec()),
342+
// (upper.to_string(), b"val1".to_vec())]);
343+
assert_eq!(
344+
fields.get_and_delete(lower).unwrap(),
345+
[b"val2".to_vec(), b"val1".to_vec()]
346+
);
347+
}
348+
349+
struct Component;
350+
export!(Component);
351+
impl exports::wasi::cli::run::Guest for Component {
352+
async fn run() -> Result<(), ()> {
353+
test_empty_fields();
354+
test_fields_with_foo();
355+
test_invalid_field_names();
356+
test_valid_field_names();
357+
test_invalid_field_values();
358+
test_valid_field_values();
359+
test_field_name_case_insensitivity();
360+
Ok(())
361+
}
362+
}
363+
364+
fn main() {
365+
unreachable!("main is a stub");
366+
}

0 commit comments

Comments
 (0)