#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn p(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|&(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
#[test]
fn plain_passthrough() {
let params = p(&[("charset", "utf-8"), ("name", "file.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result, p(&[("charset", "utf-8"), ("name", "file.txt")]));
}
#[test]
fn standalone_charset_encoded() {
let params = p(&[("title*", "us-ascii'en-us'This%20is%20fun")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "This is fun");
}
#[test]
fn continuation_reassembly() {
let params = p(&[("name*0", "first"), ("name*1", "second")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "firstsecond");
}
#[test]
fn charset_continuation_combined() {
let params = p(&[
("title*0*", "us-ascii'en'This%20is"),
("title*1*", "%20fun"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "This is fun");
}
#[test]
fn out_of_order_indices() {
let params = p(&[("name*1", "second"), ("name*0", "first")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "firstsecond");
}
#[test]
fn non_utf8_charset_iso8859_1() {
let params = p(&[("title*", "iso-8859-1'en'caf%E9")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "café");
}
#[test]
fn unknown_charset_lossy_fallback() {
let params = p(&[("title*", "x-nonexistent'en'hello%20world")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "hello world");
}
#[test]
fn mixed_plain_and_encoded_ordering() {
let params = p(&[
("charset", "utf-8"),
("name*0", "long"),
("name*1", "file.txt"),
("disposition", "inline"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 3);
assert_eq!(result[0].0, "charset");
assert_eq!(result[0].1, "utf-8");
assert_eq!(result[1].0, "name");
assert_eq!(result[1].1, "longfile.txt");
assert_eq!(result[2].0, "disposition");
assert_eq!(result[2].1, "inline");
}
#[test]
fn charset_from_non_first_encoded_section() {
let params = p(&[("name*0", "hello "), ("name*1*", "UTF-8''w%C3%B6rld.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(
result[0].1, "hello wörld.txt",
"RFC 2231 Section 3: charset from the first encoded section (not section 0) \
must be used to decode percent-encoded bytes"
);
}
#[test]
fn empty_params() {
let params: Vec<(String, String)> = Vec::new();
let result = decode_rfc2231_params(¶ms);
assert!(result.is_empty());
}
#[test]
fn missing_language_tag() {
let params = p(&[("title*", "utf-8''hello%20world")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "hello world");
}
#[test]
fn malformed_value_no_quotes() {
let params = p(&[("title*", "just-some-value")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "just-some-value");
}
#[test]
fn case_insensitive_key_grouping() {
let params = p(&[("Name*0", "hello"), ("NAME*1", " world")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "Name");
assert_eq!(result[0].1, "hello world");
}
#[test]
fn percent_decode_basic() {
assert_eq!(percent_decode("hello%20world"), b"hello world");
assert_eq!(percent_decode("%2A%2A%2A"), b"***");
assert_eq!(percent_decode("no-encoding"), b"no-encoding");
}
#[test]
fn percent_decode_truncated_sequence() {
assert_eq!(percent_decode("abc%2"), b"abc%2");
assert_eq!(percent_decode("abc%"), b"abc%");
}
#[test]
fn percent_decode_invalid_hex() {
assert_eq!(percent_decode("%GG"), b"%GG");
}
#[test]
fn continuation_missing_segment_0() {
let params = p(&[("name*1", "world")]);
let result = decode_rfc2231_params(¶ms);
assert!(result.is_empty());
}
#[test]
fn continuation_requires_segment_zero() {
let params = p(&[("name*1", "world")]);
let result = decode_rfc2231_params(¶ms);
assert!(
result.iter().all(|(key, _)| key != "name"),
"RFC 2231 Section 3: continuation groups without section 0 must not be reassembled"
);
}
#[test]
fn continuation_stops_at_first_gap() {
let params = p(&[("name*0", "first"), ("name*2", "third")]);
let result = decode_rfc2231_params(¶ms);
let name_val = result
.iter()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.as_str());
assert_eq!(
name_val,
Some("first"),
"RFC 2231 Section 3: continuation reassembly must stop at the first missing index"
);
}
#[test]
fn continuation_mixed_encoded_plain() {
let params = p(&[("name*0*", "utf-8'en'caf%C3%A9"), ("name*1", ".txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "café.txt");
}
#[test]
fn standalone_empty_charset_empty_language() {
let params = p(&[("title*", "''hello%20world")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "title");
assert_eq!(result[0].1, "hello world");
}
#[test]
fn empty_base_name_key() {
let params = p(&[("*0", "value")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "");
assert_eq!(result[0].1, "value");
}
#[test]
fn continuation_gap_assembles_all_segments() {
let params = p(&[("name*0", "first"), ("name*2", "third")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "first");
}
#[test]
fn continuation_gap_mid_sequence_assembles_all() {
let params = p(&[("f*0", "A"), ("f*1", "B"), ("f*3", "D")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].1, "AB");
}
#[test]
fn continuation_no_gap() {
let params = p(&[("f*0", "A"), ("f*1", "B"), ("f*2", "C")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].1, "ABC");
}
#[test]
fn continuation_gap_tolerates_non_contiguous_indices() {
let params = p(&[("name*0", "first"), ("name*2", "third")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "first");
}
#[test]
fn continuation_gap_mid_sequence_tolerates() {
let params = p(&[("f*0", "A"), ("f*1", "B"), ("f*3", "D")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].1, "AB");
}
#[test]
fn rfc2047_encoded_word_fallback_base64() {
let params = p(&[("filename", "=?UTF-8?B?dGVzdC50eHQ=?=")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "filename");
assert_eq!(result[0].1, "test.txt");
}
#[test]
fn rfc2047_encoded_word_fallback_quoted_printable() {
let params = p(&[("filename", "=?UTF-8?Q?caf=C3=A9.txt?=")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "filename");
assert_eq!(result[0].1, "caf\u{e9}.txt");
}
#[test]
fn rfc2047_fallback_does_not_corrupt_plain_values() {
let params = p(&[("filename", "report.pdf")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result[0].1, "report.pdf");
}
#[test]
fn non_utf8_charset_preserves_leading_feff() {
let bom_bytes_hex = "%EF%BB%BF%C0"; let params = p(&[("title*", &format!("windows-1252''{bom_bytes_hex}"))]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].1, "À");
}
#[test]
fn rfc2231_decoded_value_not_double_decoded_as_rfc2047() {
let params = p(&[("name*", "utf-8''%3D%3FUTF-8%3FB%3FdGVzdA%3D%3D%3F%3D")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "=?UTF-8?B?dGVzdA==?=");
}
#[test]
fn duplicate_continuation_index_keeps_first() {
let params = p(&[("name*0", "correct"), ("name*0", "wrong")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "name");
assert_eq!(result[0].1, "correct"); }
#[test]
fn rfc2231_section5_encoded_overrides_plain_duplicate() {
let params = p(&[
("name", "fallback.txt"),
("name*", "utf-8''encoded%2Dname.txt"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result,
p(&[("name", "encoded-name.txt")]),
"RFC 2231 Section 5: encoded form should override plain duplicate"
);
}
#[test]
fn rfc2231_section5_plain_without_encoded_kept() {
let params = p(&[("name", "plain.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result, p(&[("name", "plain.txt")]));
}
#[test]
fn rfc2231_section5_encoded_without_plain_kept() {
let params = p(&[("name*", "utf-8''encoded.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result, p(&[("name", "encoded.txt")]));
}
#[test]
fn rfc2231_section5_case_insensitive_dedup() {
let params = p(&[("Name", "fallback.txt"), ("NAME*", "utf-8''encoded.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result.len(),
1,
"RFC 2231 Section 5: case-insensitive dedup should produce one entry; got {result:?}"
);
assert_eq!(result[0].1, "encoded.txt");
}
#[test]
fn rfc2231_section5_continuation_overrides_plain() {
let params = p(&[
("name", "fallback.txt"),
("name*0*", "utf-8''encoded"),
("name*1", ".txt"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result.len(),
1,
"RFC 2231 Section 5: continuation group should override plain; got {result:?}"
);
assert_eq!(result[0].1, "encoded.txt");
}
#[test]
fn rfc2231_section5_plain_before_encoded_preserves_order() {
let params = p(&[
("charset", "utf-8"),
("name", "fallback.txt"),
("name*", "utf-8''real%2Dname.txt"),
("disposition", "attachment"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 3);
assert_eq!(result[0], ("charset".to_owned(), "utf-8".to_owned()));
assert_eq!(
result[2],
("disposition".to_owned(), "attachment".to_owned())
);
let name_entries: Vec<_> = result.iter().filter(|(k, _)| k == "name").collect();
assert_eq!(name_entries.len(), 1);
assert_eq!(name_entries[0].1, "real-name.txt");
}
#[test]
fn split_charset_value_one_quote_only() {
let (cs, bytes) = split_charset_value("utf-8'hello");
assert!(cs.is_none());
assert_eq!(bytes, b"utf-8'hello");
}
#[test]
fn percent_decode_lowercase_hex() {
assert_eq!(percent_decode("%2a%2b%2c"), b"*+,");
assert_eq!(percent_decode("caf%c3%a9"), "café".as_bytes());
}
#[test]
fn find_original_base_name_fallback_to_lowercase() {
let params = vec![
("plain_key".to_owned(), "value".to_owned()),
("another".to_owned(), "value2".to_owned()),
];
let result = find_original_base_name(¶ms, "nonexistent");
assert_eq!(result, "nonexistent");
}
#[test]
fn find_original_base_name_no_star_keys() {
let params = vec![("charset".to_owned(), "utf-8".to_owned())];
let result = find_original_base_name(¶ms, "charset");
assert_eq!(result, "charset");
}
#[test]
fn standalone_charset_no_encoded_bytes() {
let params = p(&[("filename*", "us-ascii'en'plain-text-file.txt")]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "filename");
assert_eq!(result[0].1, "plain-text-file.txt");
}
#[test]
fn continuation_gap_tolerance() {
let params = vec![
("name*0".to_string(), "part0".to_string()),
("name*2".to_string(), "part2".to_string()),
];
let result = decode_rfc2231_params(¶ms);
let name_val = result
.iter()
.find(|(k, _)| k == "name")
.map(|(_, v)| v.as_str());
assert_eq!(
name_val,
Some("part0"),
"RFC 2231 Section 3: reassembly must stop at the first gap"
);
}
#[test]
fn continuation_leading_zeros_rejected() {
let params = vec![
("name*00".to_string(), "part0".to_string()),
("name*01".to_string(), "part1".to_string()),
];
let result = decode_rfc2231_params(¶ms);
let reassembled = result.iter().any(|(k, v)| k == "name" && v == "part0part1");
assert!(
!reassembled,
"leading zeros in continuation indices (*00, *01) must be rejected \
per RFC 2231 Section 3, not silently normalized; got {result:?}"
);
}
#[test]
fn spec_audit_m6_leading_zeroes_in_continuation_rejected() {
let params = p(&[("name*00", "first"), ("name*01", "second")]);
let result = decode_rfc2231_params(¶ms);
let has_reassembled = result
.iter()
.any(|(k, v)| k == "name" && v == "firstsecond");
assert!(
!has_reassembled,
"Leading zeroes in continuation indices (*00, *01) should be rejected \
per RFC 2231 Section 3, not silently normalized; got {result:?}"
);
}
#[test]
fn spec_audit_m6_leading_zero_collision() {
let params = p(&[("name*0", "correct"), ("name*00", "wrong")]);
let result = decode_rfc2231_params(¶ms);
let name_value = result.iter().find(|(k, _)| k == "name");
assert!(
name_value.is_some(),
"Expected a 'name' parameter in the result"
);
assert_eq!(
name_value.unwrap().1,
"correct",
"name*0 (valid) should take precedence over name*00 (leading zero); \
got {result:?}"
);
}
#[test]
fn edge_continuation_three_segments_reassembled() {
let params = p(&[
("name*0", "first_"),
("name*1", "second_"),
("name*2", "third"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result.len(),
1,
"three continuation segments must reassemble into one parameter; got {result:?}"
);
assert_eq!(
result[0].0, "name",
"reassembled param key must be the base name (RFC 2231 Section 3)"
);
assert_eq!(
result[0].1, "first_second_third",
"continuation segments must be concatenated in index order \
(RFC 2231 Section 3)"
);
}
#[test]
fn edge_continuation_with_charset_encoding() {
let params = p(&[
("filename*0*", "UTF-8''caf%C3%A9_"),
("filename*1", "report.pdf"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result.len(),
1,
"charset-encoded continuation must reassemble into one parameter; got {result:?}"
);
assert_eq!(result[0].0, "filename");
assert_eq!(
result[0].1, "caf\u{e9}_report.pdf",
"first segment must be charset-decoded, then concatenated with \
subsequent plain segments (RFC 2231 Sections 3-4)"
);
}
#[test]
fn edge_continuation_mixed_with_plain_params() {
let params = p(&[
("charset", "utf-8"),
("name*0", "long_"),
("name*1", "value.txt"),
("format", "flowed"),
]);
let result = decode_rfc2231_params(¶ms);
assert_eq!(
result.len(),
3,
"one plain + one reassembled + one plain = 3 params; got {result:?}"
);
assert_eq!(result[0], ("charset".to_string(), "utf-8".to_string()));
assert_eq!(
result[1],
("name".to_string(), "long_value.txt".to_string()),
"continuation must be reassembled at the position of its first segment"
);
assert_eq!(result[2], ("format".to_string(), "flowed".to_string()));
}