1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! UTF-8 well-formedness in graceful mode.
//!
//! Graceful mode accepts raw multi-byte UTF-8 in path / query / fragment
//! / userinfo (browsers and curl do too), but every typed accessor
//! derives `&str` via `from_utf8_unchecked`. So the bytes must in fact
//! *be* well-formed UTF-8 — RFC 3629 §4 + Unicode 13 Table 3-7, which
//! also excludes overlong encodings, surrogate code points
//! (U+D800..=U+DFFF), and code points beyond U+10FFFF.
//!
//! Strict mode's byte-set check rejects non-ASCII outright, so these
//! tests target graceful mode (with one strict-mode crossover at the
//! end to pin the layering).
use super::{parse_graceful, parse_graceful_bytes, parse_strict_bytes};
use crate::uri::{ParseError, Uri};
/// Assert a parse fails with `ParseError::NonUtf8` at the expected byte
/// offset. Pinning the offset is stronger than `.is_err()` — a future
/// change that swaps the variant out doesn't silently regress.
#[track_caller]
fn assert_non_utf8(input: &[u8], expected_at: usize) {
match Uri::parse(input) {
Err(ParseError::NonUtf8 { at }) => assert_eq!(
at, expected_at,
"wrong offset for {input:?}: want {expected_at}, got {at}"
),
other => panic!("expected NonUtf8 for {input:?}, got {other:?}"),
}
}
#[test]
fn lone_high_byte_in_path_rejected() {
// `\xFF` is not a valid UTF-8 byte in any position.
assert_non_utf8(b"/\xff", 1);
}
#[test]
fn lone_continuation_byte_in_path_rejected() {
// 0x80..=0xBF are continuation bytes; they may not start a sequence.
assert_non_utf8(b"/\x80", 1);
assert_non_utf8(b"/\xbf", 1);
}
#[test]
fn truncated_multibyte_sequence_in_path_rejected() {
// Three-byte lead with only two bytes available.
assert_non_utf8(b"/\xe2\x82", 1);
// Four-byte lead with three bytes available.
assert_non_utf8(b"/\xf0\x9f\x98", 1);
}
#[test]
fn invalid_continuation_byte_rejected() {
// Lead 0xE2 expects two continuation bytes in 0x80..=0xBF.
// Second byte 0x7F is below 0x80 — invalid.
assert_non_utf8(b"/\xe2\x7f\xac", 2);
// Third byte 0xC0 is above 0xBF — invalid.
assert_non_utf8(b"/\xe2\x82\xc0", 3);
}
#[test]
fn overlong_encodings_rejected() {
// U+0000 encoded as the two-byte form `0xC0 0x80` — overlong.
// 0xC0 isn't a valid lead at all (lookup starts at 0xC2).
assert_non_utf8(b"/\xc0\x80", 1);
// U+007F encoded as `0xC1 0xBF` — overlong, 0xC1 also invalid lead.
assert_non_utf8(b"/\xc1\xbf", 1);
// U+007F encoded as `0xE0 0x81 0xBF` — overlong 3-byte. Lead 0xE0
// requires its first continuation in 0xA0..=0xBF; 0x81 fails.
assert_non_utf8(b"/\xe0\x81\xbf", 2);
// U+FFFF encoded as `0xF0 0x8F 0xBF 0xBF` — overlong 4-byte. Lead
// 0xF0 requires its first continuation in 0x90..=0xBF; 0x8F fails.
assert_non_utf8(b"/\xf0\x8f\xbf\xbf", 2);
}
#[test]
fn surrogate_codepoints_rejected() {
// U+D800 (low surrogate) encoded as `0xED 0xA0 0x80`. Lead 0xED
// requires its first continuation in 0x80..=0x9F (not 0xA0..=0xBF),
// which precisely excludes the surrogate block.
assert_non_utf8(b"/\xed\xa0\x80", 2);
// U+DFFF (high surrogate) — `0xED 0xBF 0xBF`. Same exclusion.
assert_non_utf8(b"/\xed\xbf\xbf", 2);
}
#[test]
fn codepoints_above_u10ffff_rejected() {
// U+110000 would be `0xF4 0x90 0x80 0x80`. Lead 0xF4 requires its
// first continuation in 0x80..=0x8F; 0x90 fails.
assert_non_utf8(b"/\xf4\x90\x80\x80", 2);
// 0xF5..=0xFF are invalid leads — no code point lives there.
assert_non_utf8(b"/\xf5\x80\x80\x80", 1);
assert_non_utf8(b"/\xff\x80\x80\x80", 1);
}
#[test]
fn non_utf8_in_query_and_fragment_rejected() {
assert_non_utf8(b"/?\xff", 2);
assert_non_utf8(b"/?k=\xff", 4);
assert_non_utf8(b"/#\xff", 2);
}
#[test]
fn non_utf8_in_userinfo_rejected() {
// Userinfo in graceful mode previously had no validation — any
// userinfo accessor then did `from_utf8_unchecked`. UB.
parse_graceful_bytes(b"http://\xff@example.com/").unwrap_err();
parse_graceful_bytes(b"http://user:\xff@example.com/").unwrap_err();
}
#[test]
fn valid_multibyte_sequences_accepted() {
// Two-byte (ñ = U+00F1).
parse_graceful_bytes("/ñ".as_bytes()).unwrap();
// Three-byte (€ = U+20AC; CJK).
parse_graceful_bytes("/€".as_bytes()).unwrap();
parse_graceful_bytes("/?key=日本".as_bytes()).unwrap();
parse_graceful_bytes("/#fragmöng".as_bytes()).unwrap();
// Four-byte (😀 = U+1F600).
parse_graceful_bytes("/😀".as_bytes()).unwrap();
// Mixed ASCII + multi-byte across sections.
parse_graceful_bytes("/path/€?q=日本#日本".as_bytes()).unwrap();
// Userinfo with valid UTF-8.
parse_graceful_bytes("http://üser@example.com/".as_bytes()).unwrap();
}
#[test]
fn strict_mode_rejects_non_ascii_via_byte_set() {
// Layering pin: strict mode's byte-set check rejects non-ASCII
// before the UTF-8 validator could ever fire, so the variant is
// `StrictViolation` rather than `NonUtf8`.
match parse_strict_bytes("/€".as_bytes()) {
Err(ParseError::StrictViolation) => {}
other => panic!("expected StrictViolation, got {other:?}"),
}
match parse_strict_bytes(b"/\xff") {
Err(ParseError::StrictViolation) => {}
other => panic!("expected StrictViolation, got {other:?}"),
}
}
#[test]
fn accepted_uris_render_without_ub() {
// Round-trip sanity: every accepted graceful-mode URI must be safe
// to call `to_string()` on. If invalid UTF-8 ever slips through,
// this test panics with a Utf8Error (or worse) rather than silent
// UB — which is exactly the regression we want to catch.
let inputs = [
"/€",
"/?q=日本",
"/#frag",
"http://example.com/€/€?q=日本#日本",
];
for s in &inputs {
let u: Uri = parse_graceful(s).unwrap();
let rendered = u.to_string();
assert!(rendered.contains(s.split('?').next().unwrap_or(s)));
}
}