mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
239 lines
7.3 KiB
Rust
239 lines
7.3 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn token_and_separator_consumers_skip_optional_whitespace() {
|
|
let mut tokenizer = HeaderFieldTokenizer::new(" \tattachment \t; \tfilename \t= \tfile.txt \t");
|
|
|
|
assert_eq!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("attachment")
|
|
);
|
|
assert!(tokenizer.consume(';'));
|
|
assert_eq!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("filename")
|
|
);
|
|
assert!(tokenizer.consume('='));
|
|
assert_eq!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("file.txt")
|
|
);
|
|
assert!(tokenizer.is_consumed());
|
|
}
|
|
|
|
#[test]
|
|
fn normal_and_relaxed_modes_match_blink_tspecial_handling() {
|
|
let mut normal = HeaderFieldTokenizer::new("type/subtype");
|
|
assert_eq!(
|
|
normal.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("type")
|
|
);
|
|
assert!(!normal.is_consumed());
|
|
|
|
let mut relaxed = HeaderFieldTokenizer::new("type/subtype");
|
|
assert_eq!(
|
|
relaxed.consume_token(HeaderFieldTokenMode::Relaxed),
|
|
Some("type/subtype")
|
|
);
|
|
assert!(relaxed.is_consumed());
|
|
|
|
for value in ["two words", "token;next", "token\"quoted"] {
|
|
let mut tokenizer = HeaderFieldTokenizer::new(value);
|
|
assert_ne!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Relaxed),
|
|
Some(value),
|
|
"separator must remain outside relaxed tokens: {value:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn quoted_strings_preserve_semicolons_unicode_and_quoted_pairs() {
|
|
let mut tokenizer = HeaderFieldTokenizer::new(r#" "x=y;y=\"\pz; ;;你好" ; next=value"#);
|
|
|
|
assert_eq!(
|
|
tokenizer
|
|
.consume_token_or_quoted_string(HeaderFieldTokenMode::Normal)
|
|
.as_deref(),
|
|
Some("x=y;y=\"pz; ;;你好")
|
|
);
|
|
assert!(tokenizer.consume(';'));
|
|
assert_eq!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("next")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quoted_empty_string_is_distinct_from_an_empty_token() {
|
|
let mut quoted = HeaderFieldTokenizer::new(r#""""#);
|
|
assert_eq!(quoted.consume_quoted_string().as_deref(), Some(""));
|
|
assert!(quoted.is_consumed());
|
|
|
|
let mut empty = HeaderFieldTokenizer::new("");
|
|
assert_eq!(empty.consume_token(HeaderFieldTokenMode::Normal), None);
|
|
assert_eq!(
|
|
empty.consume_token_or_quoted_string(HeaderFieldTokenMode::Normal),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_quoted_strings_fail() {
|
|
for value in [r#""unterminated"#, r#""terminal escape\"#] {
|
|
let mut tokenizer = HeaderFieldTokenizer::new(value);
|
|
assert_eq!(tokenizer.consume_quoted_string(), None, "{value:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn non_ascii_requires_quoting_but_del_matches_blink_token_behavior() {
|
|
let mut unquoted_unicode = HeaderFieldTokenizer::new("你好");
|
|
assert_eq!(
|
|
unquoted_unicode.consume_token(HeaderFieldTokenMode::Normal),
|
|
None
|
|
);
|
|
|
|
let mut quoted_unicode = HeaderFieldTokenizer::new(r#""你好""#);
|
|
assert_eq!(
|
|
quoted_unicode.consume_quoted_string().as_deref(),
|
|
Some("你好")
|
|
);
|
|
|
|
// RFC MIME token grammar excludes DEL as a control. Blink's effective
|
|
// implementation accepts it, so the shared compatibility primitive
|
|
// deliberately does too.
|
|
let mut del = HeaderFieldTokenizer::new("\u{7f}");
|
|
assert_eq!(
|
|
del.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("\u{7f}")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn consume_before_any_match_stops_without_consuming_the_separator() {
|
|
let mut tokenizer = HeaderFieldTokenizer::new("alpha,beta;gamma");
|
|
|
|
tokenizer.consume_before_any_char_match(&[',', ';']);
|
|
assert_eq!(tokenizer.byte_index(), "alpha".len());
|
|
assert!(tokenizer.consume(','));
|
|
assert_eq!(
|
|
tokenizer.consume_token(HeaderFieldTokenMode::Normal),
|
|
Some("beta")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn separators_inside_a_quoted_string_do_not_split() {
|
|
assert_eq!(
|
|
split_outside_quoted_strings("text/html; boundary=\"; charset=gbk\"", ';'),
|
|
vec!["text/html", " boundary=\"; charset=gbk\""]
|
|
);
|
|
assert_eq!(
|
|
split_outside_quoted_strings("private=\"Set-Cookie, X-Auth\", max-age=60", ','),
|
|
vec!["private=\"Set-Cookie, X-Auth\"", " max-age=60"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_escaped_quote_does_not_close_a_quoted_string() {
|
|
assert_eq!(
|
|
split_outside_quoted_strings("attachment; name=\"a\\\"; x=1\"; y=2", ';'),
|
|
vec!["attachment", " name=\"a\\\"; x=1\"", " y=2"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn splitting_without_quotes_matches_a_plain_split() {
|
|
for value in [
|
|
"text/html; charset=utf-8",
|
|
"a;b;c",
|
|
"",
|
|
";",
|
|
"trailing;",
|
|
";leading",
|
|
] {
|
|
assert_eq!(
|
|
split_outside_quoted_strings(value, ';'),
|
|
value.split(';').collect::<Vec<_>>(),
|
|
"value={value}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_unterminated_quoted_string_runs_to_the_end() {
|
|
assert_eq!(
|
|
split_outside_quoted_strings("text/html; charset=\"gbk; q=1", ';'),
|
|
vec!["text/html", " charset=\"gbk; q=1"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quoted_values_lose_their_delimiters_and_backslashes() {
|
|
assert_eq!(unquote_parameter_value("\"utf-8\""), "utf-8");
|
|
assert_eq!(unquote_parameter_value("\"utf\\-8\""), "utf-8");
|
|
assert_eq!(unquote_parameter_value("\"a\\\"b\""), "a\"b");
|
|
// Not a quoted string, so it is returned untouched.
|
|
assert_eq!(unquote_parameter_value("utf-8"), "utf-8");
|
|
assert_eq!(unquote_parameter_value("'utf-8'"), "'utf-8'");
|
|
// Unterminated keeps what was read.
|
|
assert_eq!(unquote_parameter_value("\"gbk"), "gbk");
|
|
}
|
|
|
|
#[test]
|
|
fn multibyte_values_keep_character_boundaries() {
|
|
let value = "text/plain; name=\"café; x\"; charset=utf-8";
|
|
assert_eq!(
|
|
split_outside_quoted_strings(value, ';'),
|
|
vec!["text/plain", " name=\"café; x\"", " charset=utf-8"]
|
|
);
|
|
assert_eq!(unquote_parameter_value("\"caf\\é\""), "café");
|
|
}
|
|
|
|
#[test]
|
|
fn a_quote_outside_a_parameter_value_is_ordinary_data() {
|
|
// A `"` that does not open a parameter value must not swallow the rest of
|
|
// the field, or the parameters behind it become invisible.
|
|
assert_eq!(
|
|
split_outside_quoted_strings("text/html;\";charset=gbk", ';'),
|
|
vec!["text/html", "\"", "charset=gbk"]
|
|
);
|
|
assert_eq!(
|
|
split_outside_quoted_strings("a\"b;c", ';'),
|
|
vec!["a\"b", "c"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn whitespace_between_equals_and_a_quoted_value_still_opens_it() {
|
|
assert_eq!(
|
|
split_outside_quoted_strings("text/html; boundary = \"; x\"; charset=utf-8", ';'),
|
|
vec!["text/html", " boundary = \"; x\"", " charset=utf-8"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_trailing_backslash_is_kept_rather_than_dropped() {
|
|
// Dropping it would turn a malformed value into a well-formed one.
|
|
assert_eq!(unquote_parameter_value("\"31536000\\"), "31536000\\");
|
|
assert_eq!(unquote_parameter_value("\"a\\"), "a\\");
|
|
}
|
|
|
|
#[test]
|
|
fn an_escaped_quote_survives_as_data() {
|
|
assert_eq!(unquote_parameter_value("\"utf-8\\\"\""), "utf-8\"");
|
|
}
|
|
|
|
#[test]
|
|
fn equals_inside_a_value_does_not_reopen_quoted_string_mode() {
|
|
assert_eq!(
|
|
split_outside_quoted_strings("token=a=\"unterminated, next=value", ','),
|
|
vec!["token=a=\"unterminated", " next=value"]
|
|
);
|
|
assert_eq!(
|
|
split_outside_quoted_strings("token=\"ok\"junk=\"unterminated, next=value", ','),
|
|
vec!["token=\"ok\"junk=\"unterminated", " next=value"]
|
|
);
|
|
}
|