use crate::{CanonicalRequest, Method, Protocol, Transport};
#[derive(Debug, Clone, Copy)]
pub struct NormalizeConfig {
pub decode_path: bool,
pub merge_headers: bool,
pub normalize_case: bool,
}
impl Default for NormalizeConfig {
fn default() -> Self {
Self {
decode_path: true,
merge_headers: true,
normalize_case: true,
}
}
}
#[derive(Debug, Clone)]
pub struct NormalizeError {
pub message: &'static str,
}
impl core::fmt::Display for NormalizeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "NormalizeError: {}", self.message)
}
}
#[allow(clippy::too_many_arguments)]
pub fn normalize_request(
method: Method,
scheme: &str,
authority: &str,
path: &str,
query: &str,
headers: &[(&str, &str)],
protocol: Protocol,
transport: Transport,
) -> Result<CanonicalRequest, NormalizeError> {
let config = NormalizeConfig::default();
normalize_request_with_config(
method, scheme, authority, path, query, headers, protocol, transport, config,
)
}
#[allow(clippy::too_many_arguments)]
pub fn normalize_request_with_config(
method: Method,
scheme: &str,
authority: &str,
path: &str,
query: &str,
headers: &[(&str, &str)],
protocol: Protocol,
transport: Transport,
config: NormalizeConfig,
) -> Result<CanonicalRequest, NormalizeError> {
let mut request = CanonicalRequest::empty();
request.method = method;
request.protocol = protocol;
request.transport = transport;
let normalized_scheme = if config.normalize_case {
scheme.to_lowercase()
} else {
scheme.to_string()
};
if !request.set_scheme(&normalized_scheme) {
return Err(NormalizeError {
message: "scheme too long",
});
}
let normalized_authority = normalize_authority(authority, &normalized_scheme);
if !request.set_authority(&normalized_authority) {
return Err(NormalizeError {
message: "authority too long",
});
}
let normalized_path = normalize_path(path, config.decode_path)?;
if !request.set_path(&normalized_path) {
return Err(NormalizeError {
message: "path too long",
});
}
let normalized_query = normalize_query(query, config.decode_path);
if !request.set_query(&normalized_query) {
return Err(NormalizeError {
message: "query too long",
});
}
normalize_headers(&mut request, headers, config)?;
Ok(request)
}
pub fn normalize_path(path: &str, decode: bool) -> Result<String, NormalizeError> {
if path.is_empty() {
return Ok("/".to_string());
}
let bytes = path.as_bytes();
let mut result = Vec::with_capacity(path.len());
let has_trailing_slash = bytes.ends_with(b"/");
let starts_with_slash = bytes[0] == b'/';
if starts_with_slash {
result.push(b'/');
}
let segments: Vec<&[u8]> = bytes
.split(|&b| b == b'/')
.filter(|s| !s.is_empty())
.collect();
let mut stack: Vec<Vec<u8>> = Vec::with_capacity(segments.len());
for segment in &segments {
let decoded: Vec<u8> = if decode {
safe_percent_decode(segment)
} else {
segment.to_vec()
};
match decoded.as_slice() {
b"." => {
}
b".." => {
stack.pop();
}
_ => {
stack.push(decoded);
}
}
}
for (i, segment) in stack.iter().enumerate() {
if i > 0 {
result.push(b'/');
}
result.extend_from_slice(segment);
}
if result.is_empty() {
result.push(b'/');
}
if has_trailing_slash && !result.ends_with(b"/") {
result.push(b'/');
}
String::from_utf8(result).map_err(|_| NormalizeError {
message: "invalid UTF-8 in path",
})
}
pub fn normalize_query(query: &str, decode: bool) -> String {
if query.is_empty() {
return String::new();
}
let trimmed = query.trim_start_matches('?');
let normalized: String = trimmed
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.collect();
if !decode {
return normalized;
}
match String::from_utf8(safe_percent_decode(normalized.as_bytes())) {
Ok(decoded) => decoded,
Err(_) => normalized,
}
}
pub fn normalize_authority(authority: &str, scheme: &str) -> String {
if authority.is_empty() {
return String::new();
}
let (host, port) = if let Some(bracket_start) = authority.find('[') {
match authority.find(']') {
Some(bracket_end) if bracket_end > bracket_start => {
let host = &authority[bracket_start..=bracket_end];
let port_part = authority.get(bracket_end + 1..).unwrap_or("");
let port = port_part.trim_start_matches(':');
if port.is_empty() || port.parse::<u16>().is_ok() {
(host.to_string(), port.to_string())
} else {
(host.to_string(), String::new())
}
}
_ => (authority.to_string(), String::new()),
}
} else if let Some(colon_pos) = authority.rfind(':') {
let host = &authority[..colon_pos];
let port = &authority[colon_pos + 1..];
if port.parse::<u16>().is_ok() {
(host.to_string(), port.to_string())
} else {
(authority.to_string(), String::new())
}
} else {
(authority.to_string(), String::new())
};
let normalized_host = host.to_lowercase();
let default_port = match scheme {
"http" => Some("80"),
"https" => Some("443"),
_ => None,
};
if !port.is_empty() {
if let Some(default) = default_port
&& port == default
{
return normalized_host;
}
format!("{}:{}", normalized_host, port)
} else {
normalized_host
}
}
pub fn normalize_host_key(host: &str) -> String {
let h = host.trim();
if h.is_empty() {
return String::new();
}
let (name, port) = split_host_port(h);
let mut name = name.to_lowercase();
while name.ends_with('.') {
name.pop();
}
match port {
Some("80") | Some("443") => name,
Some(p) => format!("{name}:{p}"),
None => name,
}
}
pub fn split_host_port(h: &str) -> (&str, Option<&str>) {
if let Some(end) = h.strip_prefix('[').and_then(|s| s.find(']')) {
let addr = &h[..=end + 1]; let rest = &h[end + 2..];
match rest.strip_prefix(':') {
Some(p) if !p.is_empty() => (addr, Some(p)),
_ => (addr, None),
}
} else if let Some(idx) = h.rfind(':') {
let (name, p) = h.split_at(idx);
let p = &p[1..];
if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) {
(name, Some(p))
} else {
(h, None)
}
} else {
(h, None)
}
}
#[inline]
pub fn unbracket_ipv6(h: &str) -> &str {
h.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(h)
}
pub fn normalize_headers(
request: &mut CanonicalRequest,
headers: &[(&str, &str)],
config: NormalizeConfig,
) -> Result<(), NormalizeError> {
if headers.is_empty() {
return Ok(());
}
if config.merge_headers {
let mut merged: Vec<(String, Vec<String>)> = Vec::new();
for (name, value) in headers {
if name.starts_with(':') {
continue;
}
let normalized_name = if config.normalize_case {
name.to_lowercase()
} else {
name.to_string()
};
if !is_valid_header_name(&normalized_name) {
return Err(NormalizeError {
message: "invalid header name",
});
}
let normalized_value = value.trim().to_string();
if let Some(entry) = merged.iter_mut().find(|(n, _)| n == &normalized_name) {
entry.1.push(normalized_value);
} else {
merged.push((normalized_name, vec![normalized_value]));
}
}
for (name, values) in &merged {
let combined_value = values.join(", ");
if combined_value.len() > crate::MAX_HEADER_VALUE_LEN {
return Err(NormalizeError {
message: "header value too long",
});
}
request
.add_header(name.as_bytes(), combined_value.as_bytes())
.map_err(|msg| NormalizeError { message: msg })?;
}
} else {
for (name, value) in headers {
if name.starts_with(':') {
continue;
}
let normalized_name = if config.normalize_case {
name.to_lowercase()
} else {
name.to_string()
};
if !is_valid_header_name(&normalized_name) {
return Err(NormalizeError {
message: "invalid header name",
});
}
let normalized_value = value.trim();
request
.add_header(normalized_name.as_bytes(), normalized_value.as_bytes())
.map_err(|msg| NormalizeError { message: msg })?;
}
}
Ok(())
}
pub fn is_valid_header_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
name.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'
)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidSequencePolicy {
Reject,
Preserve,
}
pub fn percent_decode(input: &str, plus_as_space: bool) -> Option<String> {
percent_decode_with_policy(input, plus_as_space, InvalidSequencePolicy::Reject)
}
pub fn percent_decode_with_policy(
input: &str,
plus_as_space: bool,
policy: InvalidSequencePolicy,
) -> Option<String> {
let mut out = Vec::with_capacity(input.len());
decode_percent_vec(input.as_bytes(), plus_as_space, policy, &mut out)?;
String::from_utf8(out).ok()
}
pub fn percent_decode_bytes(input: &[u8], plus_as_space: bool) -> Vec<u8> {
let mut out = Vec::with_capacity(input.len());
let _ = decode_percent_vec(input, plus_as_space, InvalidSequencePolicy::Preserve, &mut out);
out
}
pub fn percent_decode_bytes_into<'a>(
input: &[u8],
buf: &'a mut [u8],
plus_as_space: bool,
) -> Option<&'a [u8]> {
let mut i = 0usize;
let mut j = 0usize;
while i < input.len() {
if j >= buf.len() {
return None;
}
match input[i] {
b'%' => {
let hi = input.get(i + 1).copied().and_then(hex_value);
let lo = input.get(i + 2).copied().and_then(hex_value);
match (hi, lo) {
(Some(h), Some(l)) => {
buf[j] = (h << 4) | l;
j += 1;
i += 3;
}
_ => {
buf[j] = b'%';
j += 1;
i += 1;
}
}
}
b'+' if plus_as_space => {
buf[j] = b' ';
j += 1;
i += 1;
}
b => {
buf[j] = b;
j += 1;
i += 1;
}
}
}
Some(&buf[..j])
}
fn decode_percent_vec(
input: &[u8],
plus_as_space: bool,
policy: InvalidSequencePolicy,
out: &mut Vec<u8>,
) -> Option<()> {
let mut i = 0usize;
while i < input.len() {
match input[i] {
b'%' => {
let hi = input.get(i + 1).copied().and_then(hex_value);
let lo = input.get(i + 2).copied().and_then(hex_value);
match (hi, lo) {
(Some(h), Some(l)) => {
out.push((h << 4) | l);
i += 3;
}
_ => match policy {
InvalidSequencePolicy::Reject => return None,
InvalidSequencePolicy::Preserve => {
out.push(b'%');
i += 1;
}
},
}
}
b'+' if plus_as_space => {
out.push(b' ');
i += 1;
}
b => {
out.push(b);
i += 1;
}
}
}
Some(())
}
const fn hex_value(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn safe_percent_decode(input: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
if input[i] == b'%' && i + 2 < input.len() {
let hex = &input[i + 1..i + 3];
if let Ok(byte) = u8::from_str_radix(
core::str::from_utf8(hex).unwrap_or(""),
16,
) {
if is_unreserved(byte) {
result.push(byte);
i += 3;
continue;
}
}
}
result.push(input[i]);
i += 1;
}
result
}
fn is_unreserved(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~' | b'%')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_host_port_and_unbracket_ipv6_matrix() {
assert_eq!(split_host_port("example.com:8443"), ("example.com", Some("8443")));
assert_eq!(split_host_port("example.com"), ("example.com", None));
assert_eq!(split_host_port("example.com:abc"), ("example.com:abc", None));
assert_eq!(split_host_port("[::1]"), ("[::1]", None));
assert_eq!(split_host_port("[::1]:8443"), ("[::1]", Some("8443")));
assert_eq!(split_host_port("[fe80::ff]:443"), ("[fe80::ff]", Some("443")));
assert_eq!(split_host_port("[::ffff:1.2.3.4]"), ("[::ffff:1.2.3.4]", None));
assert_eq!(unbracket_ipv6("[::1]"), "::1");
assert_eq!(unbracket_ipv6("[a]"), "a");
assert_eq!(unbracket_ipv6("::1"), "::1");
assert_eq!(unbracket_ipv6("example.com"), "example.com");
assert_eq!(unbracket_ipv6("[::"), "[::");
assert_eq!(unbracket_ipv6("a]"), "a]");
assert_eq!(unbracket_ipv6(""), "");
}
#[test]
fn test_normalize_path_simple() {
let result = normalize_path("/simple/path", true).unwrap();
assert_eq!(result, "/simple/path");
}
#[test]
fn test_normalize_path_dot_segments() {
let result = normalize_path("/a/./b/../c", true).unwrap();
assert_eq!(result, "/a/c");
}
#[test]
fn test_normalize_path_double_dot() {
let result = normalize_path("/a/b/c/../../d", true).unwrap();
assert_eq!(result, "/a/d");
}
#[test]
fn test_normalize_path_root() {
let result = normalize_path("/", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_empty() {
let result = normalize_path("", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_multiple_slashes() {
let result = normalize_path("//a///b", true).unwrap();
assert_eq!(result, "/a/b");
}
#[test]
fn test_normalize_path_percent_decode() {
let result = normalize_path("/hello%7Eworld", true).unwrap();
assert_eq!(result, "/hello~world");
}
#[test]
fn test_normalize_path_no_decode() {
let result = normalize_path("/hello%20world", false).unwrap();
assert_eq!(result, "/hello%20world");
}
#[test]
fn test_normalize_path_encoded_dot_segments_folded() {
assert_eq!(normalize_path("/a/%2e%2e/b", true).unwrap(), "/b");
assert_eq!(normalize_path("/a/%2e/b", true).unwrap(), "/a/b");
assert_eq!(
normalize_path("/%2e%2e/etc/passwd", true).unwrap(),
"/etc/passwd"
);
assert_eq!(
normalize_path("/a/%252e%252e/b", true).unwrap(),
"/a/%2e%2e/b"
);
}
#[test]
fn test_normalize_path_encoded_dot_segments_mixed_case() {
assert_eq!(normalize_path("/a/%2E%2e/b", true).unwrap(), "/b");
assert_eq!(normalize_path("/a/%2e%2E/b", true).unwrap(), "/b");
assert_eq!(normalize_path("/a/%2E/b", true).unwrap(), "/a/b");
}
#[test]
fn test_normalize_path_encoded_dot_segments_reserved_kept() {
assert_eq!(
normalize_path("/a%2f..%2fb", true).unwrap(),
"/a%2f..%2fb"
);
assert_eq!(normalize_path("/a/..%2e/b", true).unwrap(), "/a/.../b");
}
#[test]
fn test_normalize_path_encoded_dot_segments_no_decode() {
assert_eq!(
normalize_path("/a/%2e%2e/b", false).unwrap(),
"/a/%2e%2e/b"
);
assert_eq!(
normalize_path("/%2e%2e/etc/passwd", false).unwrap(),
"/%2e%2e/etc/passwd"
);
}
#[test]
fn test_normalize_query() {
let result = normalize_query("key=value&foo=bar", true);
assert_eq!(result, "key=value&foo=bar");
}
#[test]
fn test_normalize_query_with_leading_question() {
let result = normalize_query("?key=value", true);
assert_eq!(result, "key=value");
}
#[test]
fn test_normalize_query_empty() {
let result = normalize_query("", true);
assert!(result.is_empty());
}
#[test]
fn test_normalize_host_key_cache_semantics() {
assert_eq!(normalize_host_key(" ExAmPLE.com "), "example.com");
assert_eq!(normalize_host_key("example.com."), "example.com");
assert_eq!(normalize_host_key("example.com.."), "example.com");
assert_eq!(normalize_host_key("example.com:80"), "example.com");
assert_eq!(normalize_host_key("example.com:443"), "example.com");
assert_eq!(normalize_host_key("example.com:8080"), "example.com:8080");
assert_eq!(normalize_host_key("[::1]:8080"), "[::1]:8080");
assert_eq!(normalize_host_key("[::1]:443"), "[::1]");
assert_eq!(normalize_host_key("[2001:db8::a]"), "[2001:db8::a]");
assert_eq!(normalize_host_key("example.com:abc"), "example.com:abc");
assert_eq!(normalize_host_key(""), "");
}
#[test]
fn test_normalize_authority_reverse_brackets_no_panic() {
let _ = normalize_authority("]x[", "https");
let _ = normalize_authority("]:80[abc", "http");
let _ = normalize_authority("[]", "https");
assert_eq!(normalize_authority("[::1]:4444", "https"), "[::1]:4444");
assert_eq!(normalize_authority("[::1]:443", "https"), "[::1]");
}
#[test]
fn test_normalize_authority() {
let result = normalize_authority("Example.COM:443", "https");
assert_eq!(result, "example.com");
}
#[test]
fn test_normalize_authority_non_default_port() {
let result = normalize_authority("example.com:8080", "http");
assert_eq!(result, "example.com:8080");
}
#[test]
fn test_normalize_authority_http_default() {
let result = normalize_authority("example.com:80", "http");
assert_eq!(result, "example.com");
}
#[test]
fn test_normalize_authority_ipv6() {
let result = normalize_authority("[::1]:8080", "http");
assert_eq!(result, "[::1]:8080");
}
#[test]
fn test_normalize_headers_basic() {
let headers = [("Content-Type", "application/json"), ("Accept", "text/html")];
let result = normalize_request(
Method::Get,
"https",
"example.com",
"/test",
"",
&headers,
Protocol::Http1,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.find_header("content-type").unwrap().value_str(), "application/json");
assert_eq!(result.find_header("accept").unwrap().value_str(), "text/html");
}
#[test]
fn test_normalize_headers_merge() {
let headers = [
("X-Custom", "value1"),
("x-custom", "value2"),
("Accept", "text/html"),
];
let result = normalize_request(
Method::Get,
"https",
"example.com",
"/test",
"",
&headers,
Protocol::Http1,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.header_count(), 2);
let custom = result.find_header("x-custom").unwrap();
assert!(custom.value_str().contains("value1"));
assert!(custom.value_str().contains("value2"));
}
#[test]
fn test_normalize_headers_merged_value_overlong_precise_error() {
let v100 = "a".repeat(100);
let headers = [
("X-Big", v100.as_str()),
("x-big", v100.as_str()),
("X-BIG", v100.as_str()),
];
let result = normalize_request(
Method::Get,
"http",
"example.com",
"/",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
);
let err = result.expect_err("合并总长超限必须报错");
assert_eq!(
err.message, "header value too long",
"合并超长必须返回精确错误而非 header count exceeded"
);
}
#[test]
fn test_normalize_headers_invalid_name() {
let headers = [("Invalid Header!", "value"), ("Valid", "ok")];
let result = normalize_request(
Method::Get,
"https",
"example.com",
"/test",
"",
&headers,
Protocol::Http1,
Transport::Tls13,
);
let err = result.expect_err("invalid header name should be rejected");
assert_eq!(err.message, "invalid header name");
}
#[test]
fn test_is_valid_header_name() {
assert!(is_valid_header_name("content-type"));
assert!(is_valid_header_name("x-custom-header"));
assert!(is_valid_header_name("Authorization"));
assert!(!is_valid_header_name("invalid header"));
assert!(!is_valid_header_name("header\r\n"));
}
#[test]
fn test_full_normalize() {
let headers = [
("Content-Type", "application/json"),
("Host", "Example.COM"),
];
let result = normalize_request(
Method::Post,
"HTTPS",
"Example.COM:443",
"/Api/Test/./..",
"?key=value",
&headers,
Protocol::Http2,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.method, Method::Post);
assert_eq!(result.scheme_str(), "https");
assert_eq!(result.authority_str(), "example.com");
assert_eq!(result.path_str(), "/Api");
assert_eq!(result.query_str(), "key=value");
assert_eq!(result.find_header("content-type").unwrap().value_str(), "application/json");
assert_eq!(result.find_header("host").unwrap().value_str(), "Example.COM");
}
#[test]
fn test_safe_percent_decode() {
let result = safe_percent_decode(b"hello%7Eworld");
assert_eq!(result, b"hello~world");
let result = safe_percent_decode(b"test%2Fpath");
assert_eq!(result, b"test%2Fpath"); }
#[test]
fn test_normalize_path_trailing_slash() {
let result = normalize_path("/a/b/", true).unwrap();
assert_eq!(result, "/a/b/");
}
#[test]
fn test_normalize_path_only_dots() {
let result = normalize_path("/../../../..", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_single_dot_root() {
let result = normalize_path("/.", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_double_dot_root() {
let result = normalize_path("/..", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_complex_dots() {
let result = normalize_path("/a/b/c/./d/../e/../../f", true).unwrap();
assert_eq!(result, "/a/b/f");
}
#[test]
fn test_normalize_path_multiple_dots_in_segment() {
let result = normalize_path("/..hidden/.bashrc/test..", true).unwrap();
assert_eq!(result, "/..hidden/.bashrc/test..");
}
#[test]
fn test_normalize_path_no_leading_slash() {
let result = normalize_path("a/b/c", true).unwrap();
assert_eq!(result, "a/b/c");
}
#[test]
fn test_normalize_path_single_segment() {
let result = normalize_path("/test", true).unwrap();
assert_eq!(result, "/test");
}
#[test]
fn test_normalize_path_only_slashes() {
let result = normalize_path("///", true).unwrap();
assert_eq!(result, "/");
}
#[test]
fn test_normalize_path_many_slashes() {
let result = normalize_path("/a///b//c////d", true).unwrap();
assert_eq!(result, "/a/b/c/d");
}
#[test]
fn test_percent_decode_mixed_case_hex() {
let result = safe_percent_decode(b"test%2fdata");
assert_eq!(result, b"test%2fdata");
let result = safe_percent_decode(b"hello%7eworld");
assert_eq!(result, b"hello~world"); }
#[test]
fn test_percent_decode_uppercase_hex() {
let result = safe_percent_decode(b"hello%7Eworld");
assert_eq!(result, b"hello~world");
}
#[test]
fn test_percent_decode_incomplete_sequence() {
let result = safe_percent_decode(b"test%2");
assert_eq!(result, b"test%2");
let result = safe_percent_decode(b"test%");
assert_eq!(result, b"test%");
}
#[test]
fn test_percent_decode_invalid_hex() {
let result = safe_percent_decode(b"test%ZZdata");
assert_eq!(result, b"test%ZZdata");
}
#[test]
fn test_percent_decode_unreserved_chars() {
let result = safe_percent_decode(b"%41%5A%61%7A%30%39%2D%5F%2E%7E");
assert_eq!(result, b"AZaz09-_.~");
}
#[test]
fn test_percent_decode_reserved_chars_kept() {
let result = safe_percent_decode(b"%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D");
assert_eq!(result, b"%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D");
}
#[test]
fn test_percent_decode_multiple_sequences() {
let result = safe_percent_decode(b"%7E%7Ehello%7Eworld%7E%7E");
assert_eq!(result, b"~~hello~world~~");
}
#[test]
fn test_percent_decode_empty_input() {
let result = safe_percent_decode(b"");
assert_eq!(result, b"");
}
#[test]
fn test_percent_decode_pub_basic() {
assert_eq!(percent_decode("hello%20world", false).unwrap(), "hello world");
assert_eq!(percent_decode("%41%42%43", false).unwrap(), "ABC");
assert_eq!(percent_decode("plain", false).unwrap(), "plain");
assert_eq!(percent_decode("", false).unwrap(), "");
}
#[test]
fn test_percent_decode_pub_plus_as_space() {
assert_eq!(percent_decode("a+b+c", true).unwrap(), "a b c");
assert_eq!(percent_decode("a+b", false).unwrap(), "a+b");
assert_eq!(percent_decode("a%2Bb", true).unwrap(), "a+b");
}
#[test]
fn test_percent_decode_pub_invalid_sequences() {
assert!(percent_decode("test%", false).is_none());
assert!(percent_decode("test%2", false).is_none());
assert!(percent_decode("test%ZZdata", false).is_none());
assert!(percent_decode("%2g", false).is_none());
assert!(percent_decode("%g2", false).is_none());
assert!(percent_decode("%FF%FE", false).is_none());
}
#[test]
fn test_percent_decode_pub_mixed_case_hex() {
assert_eq!(percent_decode("%7e", false).unwrap(), "~");
assert_eq!(percent_decode("%7E", false).unwrap(), "~");
assert_eq!(percent_decode("%2f", false).unwrap(), "/");
assert_eq!(percent_decode("%2F", false).unwrap(), "/");
}
#[test]
fn test_policy_reject_delegates_percent_decode() {
let cases = [
"hello%20world",
"a+b",
"test%",
"test%2",
"test%ZZdata",
"%FF%FE",
"%41%42%43",
"",
];
for case in cases {
for plus in [false, true] {
assert_eq!(
percent_decode_with_policy(case, plus, InvalidSequencePolicy::Reject),
percent_decode(case, plus),
"Reject 策略与 percent_decode 不一致: {:?} (plus={})",
case,
plus
);
}
}
}
#[test]
fn test_policy_preserve_invalid_sequences() {
assert_eq!(
percent_decode_with_policy("test%ZZdata", false, InvalidSequencePolicy::Preserve).unwrap(),
"test%ZZdata"
);
assert_eq!(
percent_decode_with_policy("%", false, InvalidSequencePolicy::Preserve).unwrap(),
"%"
);
assert_eq!(
percent_decode_with_policy("%2", false, InvalidSequencePolicy::Preserve).unwrap(),
"%2"
);
assert_eq!(
percent_decode_with_policy("100%+pure", true, InvalidSequencePolicy::Preserve).unwrap(),
"100% pure"
);
assert_eq!(
percent_decode_with_policy("%2Z%41", false, InvalidSequencePolicy::Preserve).unwrap(),
"%2ZA"
);
}
#[test]
fn test_policy_preserve_invalid_utf8_rejected() {
assert!(percent_decode_with_policy("%FF", false, InvalidSequencePolicy::Preserve).is_none());
assert!(percent_decode_with_policy("%FF%FE", true, InvalidSequencePolicy::Preserve).is_none());
}
#[test]
fn test_policy_preserve_plus_as_space() {
assert_eq!(
percent_decode_with_policy("a+b+c", true, InvalidSequencePolicy::Preserve).unwrap(),
"a b c"
);
assert_eq!(
percent_decode_with_policy("a+b", false, InvalidSequencePolicy::Preserve).unwrap(),
"a+b"
);
}
#[test]
fn test_percent_decode_bytes_basic() {
assert_eq!(percent_decode_bytes(b"hello%20world", false), b"hello world");
assert_eq!(percent_decode_bytes(b"a+b", true), b"a b");
assert_eq!(percent_decode_bytes(b"a+b", false), b"a+b");
assert_eq!(percent_decode_bytes(b"%ZZ", false), b"%ZZ");
assert_eq!(percent_decode_bytes(b"%", false), b"%");
assert_eq!(percent_decode_bytes(b"%2", false), b"%2");
assert_eq!(percent_decode_bytes(b"%FF", false), b"\xFF");
assert_eq!(percent_decode_bytes(b"", false), b"");
}
#[test]
fn test_percent_decode_bytes_into_basic() {
let mut buf = [0u8; 64];
assert_eq!(
percent_decode_bytes_into(b"id=%27+OR+%271%27%3D%271", &mut buf, true),
Some(&b"id=' OR '1'='1"[..])
);
assert_eq!(
percent_decode_bytes_into(b"%FF", &mut buf, true),
Some(&b"\xFF"[..])
);
assert_eq!(
percent_decode_bytes_into(b"%ZZ", &mut buf, true),
Some(&b"%ZZ"[..])
);
}
#[test]
fn test_percent_decode_bytes_into_buffer_overflow_fail_closed() {
let mut tiny = [0u8; 2];
assert_eq!(percent_decode_bytes_into(b"abcdef", &mut tiny, true), None);
let mut exact = [0u8; 6];
assert_eq!(
percent_decode_bytes_into(b"abcdef", &mut exact, true),
Some(&b"abcdef"[..])
);
let mut empty: [u8; 0] = [];
assert_eq!(
percent_decode_bytes_into(b"", &mut empty, true),
Some(&b""[..])
);
}
#[test]
fn test_hex_value_all() {
assert_eq!(hex_value(b'0'), Some(0));
assert_eq!(hex_value(b'9'), Some(9));
assert_eq!(hex_value(b'a'), Some(10));
assert_eq!(hex_value(b'f'), Some(15));
assert_eq!(hex_value(b'A'), Some(10));
assert_eq!(hex_value(b'F'), Some(15));
assert_eq!(hex_value(b'g'), None);
assert_eq!(hex_value(b'G'), None);
assert_eq!(hex_value(b' '), None);
assert_eq!(hex_value(b'%'), None);
}
#[test]
fn test_normalize_query_multiple_question_marks() {
let result = normalize_query("??key=value??foo=bar", true);
assert_eq!(result, "key=value??foo=bar");
}
#[test]
fn test_normalize_query_whitespace_normalization() {
let result = normalize_query("key=hello%20world\t\n", true);
assert_eq!(result, "key=hello%20world ");
}
#[test]
fn test_normalize_query_decode_true_unreserved_only() {
assert_eq!(normalize_query("a=%41%2e%2e&b=%7E", true), "a=A..&b=~");
assert_eq!(normalize_query("p=%2f%3F%23", true), "p=%2f%3F%23");
assert_eq!(normalize_query("q=100%+pure", true), "q=100%+pure");
}
#[test]
fn test_normalize_query_decode_false_passthrough() {
assert_eq!(normalize_query("a=%41%2e&b=%2f", false), "a=%41%2e&b=%2f");
assert_eq!(normalize_query("a=b\tc", false), "a=b c");
}
#[test]
fn test_normalize_query_only_question_marks() {
let result = normalize_query("???", true);
assert_eq!(result, "");
}
#[test]
fn test_normalize_query_complex() {
let result = normalize_query("?a=1&b=2&c=3", true);
assert_eq!(result, "a=1&b=2&c=3");
}
#[test]
fn test_normalize_authority_empty() {
let result = normalize_authority("", "http");
assert_eq!(result, "");
}
#[test]
fn test_normalize_authority_no_port_http() {
let result = normalize_authority("example.com", "http");
assert_eq!(result, "example.com");
}
#[test]
fn test_normalize_authority_no_port_https() {
let result = normalize_authority("example.com", "https");
assert_eq!(result, "example.com");
}
#[test]
fn test_normalize_authority_host_lowercase() {
let result = normalize_authority("EXAMPLE.COM", "http");
assert_eq!(result, "example.com");
}
#[test]
fn test_normalize_authority_mixed_case() {
let result = normalize_authority("My-Host.Example.COM:8080", "http");
assert_eq!(result, "my-host.example.com:8080");
}
#[test]
fn test_normalize_authority_non_numeric_port() {
let result = normalize_authority("example.com:http", "http");
assert_eq!(result, "example.com:http");
}
#[test]
fn test_normalize_authority_ipv6_no_port() {
let result = normalize_authority("[::1]", "http");
assert_eq!(result, "[::1]");
}
#[test]
fn test_normalize_authority_ipv6_default_port() {
let result = normalize_authority("[::1]:443", "https");
assert_eq!(result, "[::1]");
}
#[test]
fn test_normalize_authority_ipv6_unclosed_bracket() {
let result = normalize_authority("[::1:8080", "http");
assert_eq!(result, "[::1:8080");
}
#[test]
fn test_normalize_authority_unknown_scheme() {
let result = normalize_authority("example.com:1234", "ftp");
assert_eq!(result, "example.com:1234");
}
#[test]
fn test_normalize_headers_pseudo_headers_filtered() {
let headers = [
(":method", "GET"),
(":path", "/test"),
(":scheme", "https"),
("content-type", "application/json"),
];
let result = normalize_request(
Method::Get,
"https",
"example.com",
"/test",
"",
&headers,
Protocol::Http2,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.header_count(), 1);
assert!(result.find_header("content-type").is_some());
}
#[test]
fn test_normalize_headers_value_trim() {
let headers = [("X-Test", " hello world ")];
let result = normalize_request(
Method::Get,
"http",
"example.com",
"/",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
)
.unwrap();
let hdr = result.find_header("x-test").unwrap();
assert_eq!(hdr.value_str(), "hello world");
}
#[test]
fn test_normalize_headers_empty_value() {
let headers = [("X-Empty", ""), ("X-Normal", "value")];
let result = normalize_request(
Method::Get,
"http",
"example.com",
"/",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
)
.unwrap();
assert_eq!(result.header_count(), 2);
assert_eq!(result.find_header("x-empty").unwrap().value_str(), "");
}
#[test]
fn test_normalize_headers_no_merge_mode() {
let config = NormalizeConfig {
merge_headers: false,
..NormalizeConfig::default()
};
let headers = [("X-Custom", "v1"), ("x-custom", "v2")];
let result = normalize_request_with_config(
Method::Get,
"http",
"example.com",
"/",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
config,
)
.unwrap();
assert_eq!(result.header_count(), 2);
}
#[test]
fn test_normalize_headers_no_normalize_case() {
let config = NormalizeConfig {
normalize_case: false,
..NormalizeConfig::default()
};
let headers = [("Content-Type", "application/json")];
let result = normalize_request_with_config(
Method::Get,
"http",
"example.com",
"/",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
config,
)
.unwrap();
assert!(result.find_header("Content-Type").is_some());
assert!(result.find_header("content-type").is_some());
let hdr = result.find_header("content-type").unwrap();
assert_eq!(hdr.name_str(), "Content-Type");
}
#[test]
fn test_normalize_headers_all_config_off() {
let config = NormalizeConfig {
decode_path: false,
merge_headers: false,
normalize_case: false,
};
let headers = [("X-Test", "A"), ("x-test", "B")];
let result = normalize_request_with_config(
Method::Get,
"HTTP",
"Example.COM",
"/hello%20world",
"",
&headers,
Protocol::Http1,
Transport::Plaintext,
config,
)
.unwrap();
assert_eq!(result.scheme_str(), "HTTP");
assert_eq!(result.path_str(), "/hello%20world");
assert_eq!(result.header_count(), 2);
}
#[test]
fn test_normalize_headers_header_count_exceeded() {
let mut header_names: Vec<String> = Vec::new();
for i in 0..100 {
header_names.push(format!("X-Header-{}", i));
}
let header_refs: Vec<(&str, &str)> = header_names.iter().map(|k| (k.as_str(), "value")).collect();
let result = normalize_request(
Method::Get,
"http",
"example.com",
"/",
"",
&header_refs,
Protocol::Http1,
Transport::Plaintext,
);
assert!(result.is_err());
}
#[test]
fn test_is_valid_header_name_all_special_chars() {
assert!(is_valid_header_name("!#$%&'*+-.^_`|~"));
}
#[test]
fn test_is_valid_header_name_empty() {
assert!(!is_valid_header_name(""));
}
#[test]
fn test_is_valid_header_name_with_spaces() {
assert!(!is_valid_header_name("content type"));
}
#[test]
fn test_is_valid_header_name_with_colon() {
assert!(!is_valid_header_name("content-type:"));
}
#[test]
fn test_is_valid_header_name_with_newline() {
assert!(!is_valid_header_name("content\r\ntype"));
}
#[test]
fn test_is_valid_header_name_with_null() {
assert!(!is_valid_header_name("content\0type"));
}
#[test]
fn test_normalize_error_display() {
let err = NormalizeError {
message: "test error message",
};
assert_eq!(
format!("{}", err),
"NormalizeError: test error message"
);
}
#[test]
fn test_normalize_config_default() {
let config = NormalizeConfig::default();
assert!(config.decode_path);
assert!(config.merge_headers);
assert!(config.normalize_case);
}
#[test]
fn test_full_normalize_http1_plaintext() {
let result = normalize_request(
Method::Get,
"http",
"example.com:80",
"/path/../to/./resource",
"?q=test",
&[("Host", "example.com"), ("Accept", "text/html")],
Protocol::Http1,
Transport::Plaintext,
)
.unwrap();
assert_eq!(result.method, Method::Get);
assert_eq!(result.protocol, Protocol::Http1);
assert_eq!(result.transport, Transport::Plaintext);
assert_eq!(result.scheme_str(), "http");
assert_eq!(result.authority_str(), "example.com");
assert_eq!(result.path_str(), "/to/resource");
assert_eq!(result.query_str(), "q=test");
}
#[test]
fn test_full_normalize_http2_tls() {
let result = normalize_request(
Method::Post,
"https",
"api.example.com:443",
"/api/v1/data",
"verbose=true",
&[
(":method", "POST"),
(":scheme", "https"),
(":path", "/api/v1/data"),
("content-type", "application/json"),
("content-type", "text/plain"),
],
Protocol::Http2,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.protocol, Protocol::Http2);
assert_eq!(result.transport, Transport::Tls13);
assert_eq!(result.header_count(), 1);
let ct = result.find_header("content-type").unwrap();
assert!(ct.value_str().contains("application/json"));
assert!(ct.value_str().contains("text/plain"));
}
#[test]
fn test_full_normalize_http3() {
let result = normalize_request(
Method::Get,
"https",
"quic.example.com",
"/",
"",
&[("user-agent", "test-agent")],
Protocol::Http3,
Transport::Tls13,
)
.unwrap();
assert_eq!(result.protocol, Protocol::Http3);
assert_eq!(result.transport, Transport::Tls13);
assert_eq!(result.path_str(), "/");
}
#[test]
fn test_normalize_all_methods() {
let methods = [
Method::Get,
Method::Post,
Method::Put,
Method::Delete,
Method::Patch,
Method::Head,
Method::Options,
Method::Connect,
Method::Trace,
];
for method in methods.iter() {
let result = normalize_request(
*method,
"http",
"example.com",
"/",
"",
&[],
Protocol::Http1,
Transport::Plaintext,
)
.unwrap();
assert_eq!(result.method, *method);
}
}
#[test]
fn test_normalize_request_overlong_path_rejected() {
let long_path = format!("/{}", "a".repeat(crate::MAX_PATH_LEN + 1));
let result = normalize_request(
Method::Get,
"http",
"example.com",
&long_path,
"",
&[],
Protocol::Http1,
Transport::Plaintext,
);
let err = result.expect_err("超长路径必须报错");
assert_eq!(err.message, "path too long");
}
#[test]
fn test_normalize_request_overlong_query_rejected() {
let long_query = "a".repeat(crate::MAX_QUERY_LEN + 1);
let result = normalize_request(
Method::Get,
"http",
"example.com",
"/",
&long_query,
&[],
Protocol::Http1,
Transport::Plaintext,
);
let err = result.expect_err("超长 query 必须报错");
assert_eq!(err.message, "query too long");
}
#[test]
fn test_normalize_request_overlong_authority_rejected() {
let long_authority = "a".repeat(crate::MAX_AUTHORITY_LEN + 1);
let result = normalize_request(
Method::Get,
"http",
&long_authority,
"/",
"",
&[],
Protocol::Http1,
Transport::Plaintext,
);
let err = result.expect_err("超长 authority 必须报错");
assert_eq!(err.message, "authority too long");
}
#[test]
fn test_normalize_request_overlong_scheme_rejected() {
let result = normalize_request(
Method::Get,
"superlongscheme",
"example.com",
"/",
"",
&[],
Protocol::Http1,
Transport::Plaintext,
);
let err = result.expect_err("超长 scheme 必须报错");
assert_eq!(err.message, "scheme too long");
}
#[test]
fn test_authority_vs_host_key_semantics_lock() {
assert_eq!(
normalize_authority("Example.COM:443", "http"),
"example.com:443"
);
assert_eq!(normalize_host_key("Example.COM:443"), "example.com");
assert_eq!(
normalize_authority("Example.COM:443", "https"),
"example.com"
);
assert_eq!(normalize_authority("example.com.", "http"), "example.com.");
assert_eq!(normalize_host_key("example.com."), "example.com");
assert_eq!(normalize_authority("[::1]:443", "http"), "[::1]:443");
assert_eq!(normalize_host_key("[::1]:443"), "[::1]");
assert_eq!(normalize_authority("[::1]:443", "https"), "[::1]");
assert_eq!(normalize_host_key(" example.com "), "example.com");
assert_eq!(
normalize_authority(" example.com ", "http"),
" example.com "
);
}
}