#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn ascii_passthrough() {
assert_eq!(encode_utf7("INBOX"), "INBOX");
assert_eq!(decode_utf7(b"INBOX"), "INBOX");
}
#[test]
fn ampersand_encoding() {
assert_eq!(encode_utf7("A&B"), "A&-B");
assert_eq!(decode_utf7(b"A&-B"), "A&B");
}
#[test]
fn non_ascii_roundtrip() {
let names = [
"Langstrumpf", "Strstrumpf", "Français", "日本語", "Strstrumpf/Langstrumpf", "Папка", "مجلد", "フォルダ", ];
for name in &names {
let encoded = encode_utf7(name);
let decoded = decode_utf7(encoded.as_bytes());
assert_eq!(
&decoded, name,
"round-trip failed for {name:?}: encoded={encoded:?}"
);
}
}
#[test]
fn known_encodings() {
assert_eq!(encode_utf7("Langstrumpf"), "Langstrumpf");
let encoded = encode_utf7("Français");
assert!(encoded.starts_with("Fran"));
assert!(encoded.contains('&'));
assert_eq!(decode_utf7(encoded.as_bytes()), "Français");
}
#[test]
fn mixed_ascii_non_ascii() {
let name = "INBOX/日本語/test";
let encoded = encode_utf7(name);
let decoded = decode_utf7(encoded.as_bytes());
assert_eq!(decoded, name);
}
#[test]
fn supplementary_plane() {
let name = "Emoji\u{1F4E7}Folder"; let encoded = encode_utf7(name);
let decoded = decode_utf7(encoded.as_bytes());
assert_eq!(decoded, name);
}
#[test]
fn empty_string() {
assert_eq!(encode_utf7(""), "");
assert_eq!(decode_utf7(b""), "");
}
#[test]
fn multiple_ampersands() {
assert_eq!(encode_utf7("A&B&C"), "A&-B&-C");
assert_eq!(decode_utf7(b"A&-B&-C"), "A&B&C");
}
#[test]
fn malformed_base64_graceful() {
let result = decode_utf7(b"&INVALID");
assert!(!result.is_empty());
}
#[test]
fn non_ascii_passthrough_outside_base64() {
let result = decode_utf7(&[b'A', 0xC3, 0xA9, b'B']);
assert!(result.contains('A'));
assert!(result.contains('B'));
}
#[test]
fn unterminated_base64_segment() {
let result = decode_utf7(b"test&AE4");
assert!(result.starts_with("test"));
}
#[test]
fn spec_audit_raw_utf8_outside_base64() {
let input = b"INBOX/caf\xc3\xa9";
let result = decode_utf7(input);
assert_eq!(
result, "INBOX/café",
"raw UTF-8 bytes should be decoded as UTF-8, not Latin-1"
);
}
#[test]
fn spec_audit_raw_utf8_cjk_outside_base64() {
let input = b"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e";
let result = decode_utf7(input);
assert_eq!(
result, "日本語",
"raw UTF-8 CJK should be decoded correctly"
);
}
#[test]
fn accepts_base64_encoded_printable_ascii_per_postels_law() {
let result = decode_utf7(b"&AEE-");
assert_eq!(
result, "A",
"Decoder must accept Base64-encoded printable ASCII per Postel's law"
);
}
#[test]
fn spec_audit_utf16be_trailing_odd_byte_produces_replacement() {
let mut out = String::new();
decode_utf16be(&[0x4E, 0x16, 0xFF], &mut out);
assert_eq!(
out, "世\u{FFFD}",
"orphan trailing byte must produce U+FFFD, not be silently dropped"
);
let result = decode_utf7(b"&Thb,-");
assert!(
result.contains('\u{FFFD}'),
"decode_utf7 must emit U+FFFD for orphan byte in Base64 segment"
);
assert!(
result.contains('世'),
"valid code unit before orphan byte must still decode"
);
}
#[test]
fn regression_mixed_ascii_nonascii_base64_segment() {
let result = decode_utf7(b"&AEFl5Q-");
assert_eq!(
result, "A\u{65E5}",
"Decoder must accept Base64-encoded printable ASCII mixed with \
non-ASCII per Postel's law (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn decode_utf7_mixed_ascii_nonascii_accepts_per_postels_law() {
let result = decode_utf7(b"&AEFl5Q-");
assert_eq!(
result, "A\u{65E5}",
"Decoder must accept Base64 with mixed ASCII/non-ASCII per Postel's law"
);
}
#[test]
fn decode_utf7_pure_ascii_in_base64_accepts_per_postels_law() {
let result = decode_utf7(b"&AEE-");
assert_eq!(
result, "A",
"Decoder must accept Base64-encoded printable ASCII per Postel's law"
);
}
#[test]
fn decode_utf7_base64_encoded_ampersand_accepts_per_postels_law() {
let result = decode_utf7(b"&ACY-");
assert_eq!(
result, "&",
"Decoder must accept Base64-encoded '&' per Postel's law"
);
}
#[test]
fn regression_nonzero_trailing_bits_accepted() {
assert_eq!(
decode_utf7(b"&AOn-"),
"\u{00E9}",
"Non-zero trailing bits in Base64 must be accepted (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn regression_base64_padding_tolerated() {
assert_eq!(
decode_utf7(b"&AOk=-"),
"\u{00E9}",
"Base64 padding must be tolerated (RFC 3501 Section 5.1.3, Postel's law)"
);
}
#[test]
fn accepts_base64_encoded_ampersand_per_postels_law() {
let result = decode_utf7(b"&ACY-");
assert_eq!(
result, "&",
"Decoder must accept Base64-encoded '&' per Postel's law (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn regression_control_chars_replaced() {
let input = b"\x00hello\x07world\x7F";
let result = decode_utf7(input);
assert!(
!result.contains('\0'),
"NUL (0x00) must not pass through verbatim (RFC 3501 Section 5.1.3)"
);
assert!(
!result.contains('\x07'),
"BEL (0x07) must not pass through verbatim (RFC 3501 Section 5.1.3)"
);
assert!(
!result.contains('\x7F'),
"DEL (0x7F) must not pass through verbatim (RFC 3501 Section 5.1.3)"
);
assert!(
result.contains("hello"),
"printable ASCII must be preserved"
);
assert!(
result.contains("world"),
"printable ASCII must be preserved"
);
assert!(
result.contains('\u{FFFD}'),
"control characters must be replaced with U+FFFD"
);
}
#[test]
fn invalid_base64_in_shift_falls_back_to_raw() {
let result = decode_utf7(b"test&!!!-end");
assert_eq!(
result, "test&!!!-end",
"Invalid Base64 within shift must emit raw fallback (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn invalid_base64_chars_in_shift_falls_back() {
let result = decode_utf7(b"&@#$-");
assert_eq!(
result, "&@#$-",
"Base64 with invalid alphabet chars must produce raw fallback"
);
}
#[test]
fn invalid_utf8_high_bytes_outside_base64() {
let result = decode_utf7(&[b'A', 0xFF, 0xFE, b'B']);
assert!(
result.starts_with('A'),
"printable ASCII before invalid bytes must be preserved"
);
assert!(
result.ends_with('B'),
"printable ASCII after invalid bytes must be preserved"
);
assert!(
result.contains('\u{FFFD}'),
"invalid UTF-8 high bytes must produce U+FFFD (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn lone_continuation_byte_outside_base64() {
let result = decode_utf7(&[0x80]);
assert_eq!(
result, "\u{FFFD}",
"lone continuation byte must produce U+FFFD"
);
}
#[test]
fn unpaired_high_surrogate_produces_replacement() {
let mut out = String::new();
decode_utf16be(&[0xD8, 0x00], &mut out);
assert_eq!(
out, "\u{FFFD}",
"unpaired high surrogate must produce U+FFFD (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn unpaired_low_surrogate_produces_replacement() {
let mut out = String::new();
decode_utf16be(&[0xDC, 0x00], &mut out);
assert_eq!(
out, "\u{FFFD}",
"unpaired low surrogate must produce U+FFFD (RFC 3501 Section 5.1.3)"
);
}
#[test]
fn trailing_ampersand_preserved_as_literal() {
assert_eq!(
decode_utf7(b"test&"),
"test&",
"trailing `&` with no closing `-` must be preserved as literal (Postel's law)"
);
assert_eq!(
decode_utf7(b"&"),
"&",
"lone `&` with no closing `-` must be preserved as literal (Postel's law)"
);
}
#[test]
fn two_high_surrogates_produce_two_replacements() {
let mut out = String::new();
decode_utf16be(&[0xD8, 0x00, 0xD8, 0x00], &mut out);
assert_eq!(
out, "\u{FFFD}\u{FFFD}",
"two unpaired high surrogates must each produce U+FFFD"
);
}
#[test]
fn unpaired_surrogate_in_base64_segment_produces_replacement() {
let engine = &*IMAP_B64_ENGINE;
let encoded_b64 = engine.encode([0xD8, 0x00]);
let mut input = Vec::new();
input.push(b'&');
input.extend_from_slice(encoded_b64.as_bytes());
input.push(b'-');
let result = decode_utf7(&input);
assert!(
result.contains('\u{FFFD}'),
"unpaired surrogate in Base64 segment must produce U+FFFD (RFC 3501 Section 5.1.3)"
);
}
mod prop_invariants {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn i15_roundtrip_encode_decode_utf7(name in "[^\0\r\n]*") {
let encoded = encode_utf7(&name);
let decoded = decode_utf7(encoded.as_bytes());
prop_assert_eq!(
&decoded, &name,
"round-trip failed: encode_utf7({:?}) = {:?}, decode_utf7({:?}) = {:?}",
name, encoded, encoded, decoded
);
}
}
}
#[test]
fn imap_b64_alphabet_is_valid() {
assert_eq!(
IMAP_B64_ALPHABET_STR.len(),
64,
"alphabet must be exactly 64 characters"
);
assert!(
!IMAP_B64_ALPHABET_STR.contains('/'),
"IMAP modified Base64 must replace / with ,"
);
assert!(
IMAP_B64_ALPHABET_STR.contains(','),
"IMAP modified Base64 must contain , in place of /"
);
let mut chars: Vec<char> = IMAP_B64_ALPHABET_STR.chars().collect();
chars.sort_unstable();
chars.dedup();
assert_eq!(chars.len(), 64, "alphabet must have 64 unique characters");
base64::alphabet::Alphabet::new(IMAP_B64_ALPHABET_STR)
.expect("alphabet must be accepted by the base64 crate");
}