Skip to main content

zenith_api/
normalize.rs

1//! 规范化引擎
2//!
3//! 本模块提供 HTTP 请求/响应的单次规范化处理,
4//! 确保所有协议(HTTP/1.1、HTTP/2、HTTP/3)生成一致的 CanonicalRequest。
5//!
6//! # 库内定位(全链路声明)
7//!
8//! 本模块是**公共规范化参考实现**:
9//! - `normalize_path` / `percent_decode*`(含 `_into` 零拷贝变体)/ `is_valid_header_name`
10//!   是全 workspace 唯一实现,被 zenith-web(三协议 normalize)、zenith-cache(键构造)、
11//!   zenith-waf(WAF 解码)、zenith-ebpf 等生产路径真实复用;
12//! - `normalize_request` / `normalize_request_with_config` / `normalize_headers` /
13//!   `normalize_authority` / `normalize_query` 是面向外部集成方的一级规范化 API,
14//!   语义基线由 `tests/cross_protocol_consistency.rs`(跨协议一致性全套集成测试)
15//!   锁定;zenith-web 生产路径使用性能特化的 `normalize_http1/2/3_request`
16//!   (同一规范化语义,零堆分配热路径),两条路径的语义一致性由该测试套件保证。
17//!
18//! # 规范化规则
19//!
20//! ## 路径规范化
21//! - 点段折叠(. / .. 处理)
22//! - 多余斜杠处理
23//! - 大小写标准化(保留原始大小写)
24//! - URL 解码(安全字符)
25//!
26//! ## Header 规范化
27//! - 字段名小写
28//! - 多值合并(同名 Header 合并为逗号分隔)
29//! - 空白字符标准化
30//! - 非法字符过滤
31//!
32//! ## Authority 规范化
33//! - Host 与端口标准化
34//! - 默认端口省略
35//! - 大小写统一
36
37use crate::{CanonicalRequest, Method, Protocol, Transport};
38
39/// 规范化配置
40#[derive(Debug, Clone, Copy)]
41pub struct NormalizeConfig {
42    /// 是否进行路径解码
43    pub decode_path: bool,
44    /// 是否合并同名 Header
45    pub merge_headers: bool,
46    /// 是否标准化大小写
47    pub normalize_case: bool,
48}
49
50impl Default for NormalizeConfig {
51    fn default() -> Self {
52        Self {
53            decode_path: true,
54            merge_headers: true,
55            normalize_case: true,
56        }
57    }
58}
59
60/// 规范化错误
61#[derive(Debug, Clone)]
62pub struct NormalizeError {
63    /// 错误描述
64    pub message: &'static str,
65}
66
67impl core::fmt::Display for NormalizeError {
68    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
69        write!(f, "NormalizeError: {}", self.message)
70    }
71}
72
73/// 规范化 HTTP 请求
74///
75/// 将原始 HTTP 请求元素规范化为统一的 CanonicalRequest。
76/// 此函数执行单次遍历,消除协议差分攻击风险。
77///
78/// # Arguments
79/// * `method` - HTTP 方法
80/// * `scheme` - URL 方案(http/https)
81/// * `authority` - 授权主机(host:port)
82/// * `path` - 请求路径
83/// * `query` - 查询字符串
84/// * `headers` - 请求头列表
85/// * `protocol` - 协议类型
86/// * `transport` - 传输层类型
87///
88/// # Returns
89/// * `Ok(CanonicalRequest)` - 规范化后的请求
90/// * `Err(NormalizeError)` - 规范化失败
91#[allow(clippy::too_many_arguments)]
92pub fn normalize_request(
93    method: Method,
94    scheme: &str,
95    authority: &str,
96    path: &str,
97    query: &str,
98    headers: &[(&str, &str)],
99    protocol: Protocol,
100    transport: Transport,
101) -> Result<CanonicalRequest, NormalizeError> {
102    let config = NormalizeConfig::default();
103    normalize_request_with_config(
104        method, scheme, authority, path, query, headers, protocol, transport, config,
105    )
106}
107
108/// 使用自定义配置规范化 HTTP 请求
109#[allow(clippy::too_many_arguments)]
110pub fn normalize_request_with_config(
111    method: Method,
112    scheme: &str,
113    authority: &str,
114    path: &str,
115    query: &str,
116    headers: &[(&str, &str)],
117    protocol: Protocol,
118    transport: Transport,
119    config: NormalizeConfig,
120) -> Result<CanonicalRequest, NormalizeError> {
121    let mut request = CanonicalRequest::empty();
122
123    // 1. 设置方法
124    request.method = method;
125    request.protocol = protocol;
126    request.transport = transport;
127
128    // 2. 规范化方案
129    let normalized_scheme = if config.normalize_case {
130        scheme.to_lowercase()
131    } else {
132        scheme.to_string()
133    };
134    // fail-closed:超长拒绝整体报错,禁止静默截断后放行
135    if !request.set_scheme(&normalized_scheme) {
136        return Err(NormalizeError {
137            message: "scheme too long",
138        });
139    }
140
141    // 3. 规范化 Authority
142    let normalized_authority = normalize_authority(authority, &normalized_scheme);
143    if !request.set_authority(&normalized_authority) {
144        return Err(NormalizeError {
145            message: "authority too long",
146        });
147    }
148
149    // 4. 规范化路径
150    let normalized_path = normalize_path(path, config.decode_path)?;
151    if !request.set_path(&normalized_path) {
152        return Err(NormalizeError {
153            message: "path too long",
154        });
155    }
156
157    // 5. 规范化查询字符串
158    let normalized_query = normalize_query(query, config.decode_path);
159    if !request.set_query(&normalized_query) {
160        return Err(NormalizeError {
161            message: "query too long",
162        });
163    }
164
165    // 6. 规范化 Headers
166    normalize_headers(&mut request, headers, config)?;
167
168    Ok(request)
169}
170
171/// 规范化路径
172///
173/// # 规则
174/// - 折叠 "." 和 ".." 段
175/// - 合并多余斜杠
176/// - 保留前导斜杠
177pub fn normalize_path(path: &str, decode: bool) -> Result<String, NormalizeError> {
178    if path.is_empty() {
179        return Ok("/".to_string());
180    }
181
182    let bytes = path.as_bytes();
183    let mut result = Vec::with_capacity(path.len());
184
185    // 记录原始输入是否以 '/' 结尾(RFC 3986 §5.2.4:尾斜杠是路径语义的一部分,
186    // 区分目录与资源,丢弃会改变路由语义)
187    let has_trailing_slash = bytes.ends_with(b"/");
188
189    // 确定是否以斜杠开头
190    let starts_with_slash = bytes[0] == b'/';
191    if starts_with_slash {
192        result.push(b'/');
193    }
194
195    let segments: Vec<&[u8]> = bytes
196        .split(|&b| b == b'/')
197        .filter(|s| !s.is_empty())
198        .collect();
199
200    // 先解码后折叠:点段判定必须作用于解码后的字节。
201    // 若先按未解码字节折叠再解码,`%2e%2e` 会以编码形态绕过折叠,
202    // 解码后残留的 `..` 直接形成路径穿越语义漏洞。
203    // decode=false 时保持字面:不解码,编码形态的点段也不参与折叠。
204    let mut stack: Vec<Vec<u8>> = Vec::with_capacity(segments.len());
205
206    for segment in &segments {
207        let decoded: Vec<u8> = if decode {
208            // 安全解码:只解码 unreserved 字符(%2f 等 reserved 保持编码态,
209            // %252e 等双重编码只解一层,解出的 %2e 与 . / .. 不相等,按字面入栈)
210            safe_percent_decode(segment)
211        } else {
212            segment.to_vec()
213        };
214        match decoded.as_slice() {
215            b"." => {
216                // 当前目录,跳过
217            }
218            b".." => {
219                // 上一级目录(栈空时 pop 为 no-op,停在根)
220                stack.pop();
221            }
222            _ => {
223                stack.push(decoded);
224            }
225        }
226    }
227
228    for (i, segment) in stack.iter().enumerate() {
229        // 第一个元素不需要额外添加斜杠(starts_with_slash 已预先添加)
230        if i > 0 {
231            result.push(b'/');
232        }
233        result.extend_from_slice(segment);
234    }
235
236    // 如果没有任何内容,返回 "/"
237    if result.is_empty() {
238        result.push(b'/');
239    }
240
241    // RFC 3986 §5.2.4:保留尾斜杠语义。尾斜杠区分目录与资源,
242    // 丢弃会改变路由语义(如 /a/b/ 与 /a/b 可能映射不同处理器)。
243    // 仅在结果不以 '/' 结尾时追加(避免对 "/" 等已有尾斜杠的结果重复添加)。
244    if has_trailing_slash && !result.ends_with(b"/") {
245        result.push(b'/');
246    }
247
248    // 转换为字符串
249    String::from_utf8(result).map_err(|_| NormalizeError {
250        message: "invalid UTF-8 in path",
251    })
252}
253
254/// 规范化查询字符串
255///
256/// 主要处理:
257/// - 移除所有前导 `?`
258/// - 保留完整 query(`?` 是 query 中的合法数据字符,
259///   按 `?` 切分丢尾会静默丢失信息,禁止)
260/// - Unicode 空白字符统一映射为 ASCII 空格
261/// - `decode=true`:与路径相同的 unreserved-only 安全解码
262///   (`%41` → `A`,`%2f` 等 reserved 保持编码态);
263///   `decode=false`:保留原始编码
264pub fn normalize_query(query: &str, decode: bool) -> String {
265    if query.is_empty() {
266        return String::new();
267    }
268
269    // 移除所有前导 '?',保留剩余完整内容(包括中间的 '?')
270    let trimmed = query.trim_start_matches('?');
271
272    // 规范化空白字符
273    let normalized: String = trimmed
274        .chars()
275        .map(|c| if c.is_whitespace() { ' ' } else { c })
276        .collect();
277
278    if !decode {
279        return normalized;
280    }
281
282    // 与 normalize_path 相同的 unreserved-only 解码语义。
283    // safe_percent_decode 仅把合法 %XX 序列替换为 unreserved 集合内的
284    // ASCII 字节,其余字节原样透传,故合法 UTF-8 输入解码后必然仍是
285    // 合法 UTF-8;from_utf8 失败分支理论不可达,保底返回未解码串,
286    // 绝不 panic、绝不产出非法字符串。
287    match String::from_utf8(safe_percent_decode(normalized.as_bytes())) {
288        Ok(decoded) => decoded,
289        Err(_) => normalized,
290    }
291}
292
293/// 规范化 Authority(host:port)
294///
295/// # 规则
296/// - 默认端口省略(http:80, https:443)
297/// - Host 小写化
298/// - 处理 IPv6 地址
299///
300/// # 与 [`normalize_host_key`] 的分工差异(设计使然,禁止"统一"两者行为)
301/// 本函数是**请求 authority 的 RFC 语义**规范化,面向路由/转发;
302/// [`normalize_host_key`] 是**缓存键等值折叠**规范化。具体差异:
303/// - 默认端口判定:本函数 **scheme 感知**,仅 http 省略 `:80`、https 省略
304///   `:443`(http 下 `:443` 原样保留);`normalize_host_key` 不感知 scheme,
305///   `:80`/`:443` 一律剥离。
306/// - FQDN 尾点:本函数**保留**尾点(`example.com.` → `example.com.`);
307///   `normalize_host_key` 剥离尾点(→ `example.com`)。
308/// - 空白:本函数不 trim;`normalize_host_key` 会 trim 两端空白。
309pub fn normalize_authority(authority: &str, scheme: &str) -> String {
310    if authority.is_empty() {
311        return String::new();
312    }
313
314    let (host, port) = if let Some(bracket_start) = authority.find('[') {
315        // IPv6 地址(fail-closed:`]` 必须位于 `[` 之后,
316        // 括号不成对/反向时视为非 IPv6 形式,禁止切片反转 panic;
317        // 对端可控字段不得触发字节范围 panic(§4.4))
318        match authority.find(']') {
319            Some(bracket_end) if bracket_end > bracket_start => {
320                let host = &authority[bracket_start..=bracket_end];
321                let port_part = authority.get(bracket_end + 1..).unwrap_or("");
322                let port = port_part.trim_start_matches(':');
323                // 与非 IPv6 分支一致:端口必须为合法 u16 数字,
324                // 否则视为无端口(`[::1]:abc` → 仅保留 `[::1]`,避免保留非法端口)
325                if port.is_empty() || port.parse::<u16>().is_ok() {
326                    (host.to_string(), port.to_string())
327                } else {
328                    (host.to_string(), String::new())
329                }
330            }
331            _ => (authority.to_string(), String::new()),
332        }
333    } else if let Some(colon_pos) = authority.rfind(':') {
334        // 可能是 host:port
335        let host = &authority[..colon_pos];
336        let port = &authority[colon_pos + 1..];
337        // 验证端口是否为数字
338        if port.parse::<u16>().is_ok() {
339            (host.to_string(), port.to_string())
340        } else {
341            (authority.to_string(), String::new())
342        }
343    } else {
344        (authority.to_string(), String::new())
345    };
346
347    let normalized_host = host.to_lowercase();
348
349    // 检查默认端口
350    let default_port = match scheme {
351        "http" => Some("80"),
352        "https" => Some("443"),
353        _ => None,
354    };
355
356    if !port.is_empty() {
357        if let Some(default) = default_port
358            && port == default
359        {
360            // 默认端口,省略
361            return normalized_host;
362        }
363        format!("{}:{}", normalized_host, port)
364    } else {
365        normalized_host
366    }
367}
368
369/// 规范化 Host 用于缓存键等值折叠(scheme 无关策略)。
370///
371/// # 规则(缓存键专用,区别于 [`normalize_authority`] 的 RFC authority 语义)
372/// - `trim` + 小写化
373/// - 去 FQDN 尾点(`example.com.` → `example.com`)
374/// - **无条件**去除默认端口(`:80` / `:443`,不感知 scheme)——缓存键
375///   要求"同一监听服务"折叠;严格 URL authority 语义请用
376///   [`normalize_authority`](scheme 感知)。
377/// - IPv6 方括号保留;`]:` 之侧仅数字视作端口
378///
379/// # 统一实现约定
380/// 本函数是全 workspace 唯一的缓存键 host 规范化实现
381/// (zenith-cache 经此接口规范化,禁止第二份副本)。
382///
383/// # 与 [`normalize_authority`] 的分工差异(设计使然,禁止"统一"两者行为)
384/// - 默认端口:本函数**不感知 scheme**,`:80`/`:443` 一律剥离(缓存键要求
385///   "同一监听服务"折叠);`normalize_authority` 仅按 scheme 省略
386///   (http→80、https→443)。
387/// - FQDN 尾点:本函数剥离(`example.com.` → `example.com`);
388///   `normalize_authority` 保留尾点。
389/// - 空白:本函数 trim 两端空白;`normalize_authority` 不 trim。
390pub fn normalize_host_key(host: &str) -> String {
391    let h = host.trim();
392    if h.is_empty() {
393        return String::new();
394    }
395    let (name, port) = split_host_port(h);
396    let mut name = name.to_lowercase();
397    // 去尾部点(FQDN 尾点,如 example.com.)
398    while name.ends_with('.') {
399        name.pop();
400    }
401    // 去默认端口;非默认端口保留
402    match port {
403        Some("80") | Some("443") => name,
404        Some(p) => format!("{name}:{p}"),
405        None => name,
406    }
407}
408
409/// 拆分 `host[:port]`,处理 IPv6 方括号字面量(`[addr]` / `[addr]:port`)。
410///
411/// 返回 (host部分, 可选端口)。IPv6 的 host 部分含方括号;端口仅当
412/// 段内全为 ASCII 数字时成立。
413///
414/// # 公开为单一实现源
415///
416/// `normalize_host_key`、zenith-web `IdentityMiddleware::extract_host`、
417/// zenith-web SNI/Host 一致性检查共享本实现(禁止第二份副本):统一
418/// IPv6 方括号/裸主机/带端口的切分语义,杜绝 `split(':')` 一类对
419/// IPv6 字面量的截断失误。
420pub fn split_host_port(h: &str) -> (&str, Option<&str>) {
421    if let Some(end) = h.strip_prefix('[').and_then(|s| s.find(']')) {
422        // IPv6:']' 在 h 中的索引为 end+1(s 比 h 少前导 '[')
423        let addr = &h[..=end + 1]; // 含方括号
424        let rest = &h[end + 2..];
425        match rest.strip_prefix(':') {
426            Some(p) if !p.is_empty() => (addr, Some(p)),
427            _ => (addr, None),
428        }
429    } else if let Some(idx) = h.rfind(':') {
430        // 普通 host:port:仅当 ':' 后全为数字才视为端口
431        let (name, p) = h.split_at(idx);
432        let p = &p[1..];
433        if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) {
434            (name, Some(p))
435        } else {
436            (h, None)
437        }
438    } else {
439        (h, None)
440    }
441}
442
443/// 剥离 IPv6 字面量外层的方括号(authority → SNI 形对齐)
444///
445/// `split_host_port` 对 IPv6 authority 返回含括号 host(`"[::1]"`),
446/// 而 RFC 6066 SNI host_name 不允许括号(`"::1"`)——SNI/Host 一致性、
447/// 白名单比对等场景需要在比较前统一剥括号使两侧同形。
448///
449/// # 语义
450/// - 合法可配对方括号 `[x]` → 返回 `x`
451/// - 任意其他输入(空、单边括号、无括号)→ 原样返回(fail-closed,
452///   不产生收缩,由下游比对决定匹配性)
453#[inline]
454pub fn unbracket_ipv6(h: &str) -> &str {
455    h.strip_prefix('[')
456        .and_then(|s| s.strip_suffix(']'))
457        .unwrap_or(h)
458}
459
460/// 规范化 Headers
461///
462/// # 规则
463/// - 字段名小写
464/// - 合并同名 Header(用逗号分隔值)
465/// - 拒绝非法 header 名(fail-closed,不静默丢弃)
466pub fn normalize_headers(
467    request: &mut CanonicalRequest,
468    headers: &[(&str, &str)],
469    config: NormalizeConfig,
470) -> Result<(), NormalizeError> {
471    if headers.is_empty() {
472        return Ok(());
473    }
474
475    // 如果启用合并,使用临时存储
476    if config.merge_headers {
477        let mut merged: Vec<(String, Vec<String>)> = Vec::new();
478
479        for (name, value) in headers {
480            // 过滤 HTTP/2/3 伪头部(以 ':' 开头)
481            if name.starts_with(':') {
482                continue;
483            }
484
485            let normalized_name = if config.normalize_case {
486                name.to_lowercase()
487            } else {
488                name.to_string()
489            };
490
491            // 拒绝非法 header 名(fail-closed):不静默丢弃,整体返回错误
492            if !is_valid_header_name(&normalized_name) {
493                return Err(NormalizeError {
494                    message: "invalid header name",
495                });
496            }
497
498            // 规范化值(去除前后空白)
499            let normalized_value = value.trim().to_string();
500
501            // 查找是否已存在
502            if let Some(entry) = merged.iter_mut().find(|(n, _)| n == &normalized_name) {
503                entry.1.push(normalized_value);
504            } else {
505                merged.push((normalized_name, vec![normalized_value]));
506            }
507        }
508
509        // 添加合并后的 Headers
510        for (name, values) in &merged {
511            let combined_value = values.join(", ");
512            // SYS-026:合并前预检合并后总长,超限返回**精确错误**("header value too long"),
513            // 而非原实现把所有 add_header 失败一律映射为"header count exceeded"——
514            // 后者掩盖了真实原因,且大量合法 header 聚合超限会被误报为数量超限。
515            if combined_value.len() > crate::MAX_HEADER_VALUE_LEN {
516                return Err(NormalizeError {
517                    message: "header value too long",
518                });
519            }
520            request
521                .add_header(name.as_bytes(), combined_value.as_bytes())
522                .map_err(|msg| NormalizeError { message: msg })?;
523        }
524    } else {
525        // 不合并,直接添加
526        for (name, value) in headers {
527            // 过滤 HTTP/2/3 伪头部(以 ':' 开头)
528            if name.starts_with(':') {
529                continue;
530            }
531
532            let normalized_name = if config.normalize_case {
533                name.to_lowercase()
534            } else {
535                name.to_string()
536            };
537
538            if !is_valid_header_name(&normalized_name) {
539                return Err(NormalizeError {
540                    message: "invalid header name",
541                });
542            }
543
544            let normalized_value = value.trim();
545            request
546                .add_header(normalized_name.as_bytes(), normalized_value.as_bytes())
547                .map_err(|msg| NormalizeError { message: msg })?;
548        }
549    }
550
551    Ok(())
552}
553
554/// 检查 Header 名是否合法(RFC 7230 token 定义)
555///
556/// token = 1*tchar
557/// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
558///         "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
559pub fn is_valid_header_name(name: &str) -> bool {
560    if name.is_empty() {
561        return false;
562    }
563    name.chars().all(|c| {
564        c.is_ascii_alphanumeric()
565            || matches!(
566                c,
567                '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'
568            )
569    })
570}
571
572/// 非法百分号序列处理策略
573///
574/// 统一 WAF 与应用层(extractor / cache)的百分号解码语义,
575/// 消除"同一输入各层解码结果不同"的协议差分攻击面。
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577pub enum InvalidSequencePolicy {
578    /// 遇非法 `%` 序列整体拒绝(fail-closed,返回 `None`),
579    /// 禁止部分解码后放行
580    Reject,
581    /// 非法 `%` 序列按原字节保留(`%ZZ` → `%ZZ`),
582    /// 保留的字节仍参与后续模式匹配,不会丢失攻击特征
583    Preserve,
584}
585
586/// 百分号解码(fail-closed)
587///
588/// 将 `%XX` 序列解码为对应字节;`plus_as_space` 为 `true` 时把 `+`
589/// 解码为空格(`application/x-www-form-urlencoded` 语义)。
590///
591/// 语义等价于 `percent_decode_with_policy(input, plus_as_space, Reject)`。
592///
593/// # Returns
594/// * `Some(String)` - 解码成功
595/// * `None` - 非法输入:`%` 后不足两位十六进制字符,或解码结果不是合法 UTF-8
596///   (fail-closed:非法序列整体拒绝,禁止部分解码后放行)
597pub fn percent_decode(input: &str, plus_as_space: bool) -> Option<String> {
598    percent_decode_with_policy(input, plus_as_space, InvalidSequencePolicy::Reject)
599}
600
601/// 带非法序列策略的百分号解码(字符串级统一入口)
602///
603/// 将 `%XX` 序列解码为对应字节;`plus_as_space` 为 `true` 时把 `+`
604/// 解码为空格(`application/x-www-form-urlencoded` 语义)。
605///
606/// # 策略
607/// - [`InvalidSequencePolicy::Reject`]:遇非法 `%` 序列整体返回 `None`
608/// - [`InvalidSequencePolicy::Preserve`]:非法 `%` 序列按原字节保留
609///
610/// 无论哪种策略,解码结果不是合法 UTF-8 时均返回 `None`
611/// (字符串级 API 必须保证输出是合法 `String`)。
612///
613/// # Returns
614/// * `Some(String)` - 解码成功
615/// * `None` - 策略为 Reject 且遇非法序列,或解码结果不是合法 UTF-8
616pub fn percent_decode_with_policy(
617    input: &str,
618    plus_as_space: bool,
619    policy: InvalidSequencePolicy,
620) -> Option<String> {
621    let mut out = Vec::with_capacity(input.len());
622    decode_percent_vec(input.as_bytes(), plus_as_space, policy, &mut out)?;
623    // 解码结果必须是合法 UTF-8,否则整体拒绝
624    String::from_utf8(out).ok()
625}
626
627/// 字节级百分号解码(Preserve 策略,分配 `Vec`)
628///
629/// 非法 `%` 序列按原字节保留;`plus_as_space` 为 `true` 时 `+` 解码为空格。
630/// 不做 UTF-8 校验,输入/输出均为字节序列,
631/// 用于 body 等不保证 UTF-8 的场景。
632///
633/// Preserve 策略下解码永不失败,故直接返回 `Vec<u8>`。
634pub fn percent_decode_bytes(input: &[u8], plus_as_space: bool) -> Vec<u8> {
635    let mut out = Vec::with_capacity(input.len());
636    // Preserve 策略不会返回 None(非法序列按原字节保留)
637    let _ = decode_percent_vec(input, plus_as_space, InvalidSequencePolicy::Preserve, &mut out);
638    out
639}
640
641/// 字节级百分号解码,结果写入调用方缓冲区(Preserve 策略,零堆分配)
642///
643/// 非法 `%` 序列按原字节保留;`plus_as_space` 为 `true` 时 `+` 解码为空格。
644/// 不做 UTF-8 校验(调用方按需自行校验)。
645/// 供 WAF 等零堆分配热路径使用。
646///
647/// # Returns
648/// * `Some(&buf[..n])` - 解码结果(借用调用方缓冲区)
649/// * `None` - 缓冲区不足(fail-closed,禁止截断后放行)
650pub fn percent_decode_bytes_into<'a>(
651    input: &[u8],
652    buf: &'a mut [u8],
653    plus_as_space: bool,
654) -> Option<&'a [u8]> {
655    let mut i = 0usize;
656    let mut j = 0usize;
657
658    while i < input.len() {
659        // 边界检查:输出超出缓冲区容量则 fail-closed
660        if j >= buf.len() {
661            return None;
662        }
663        match input[i] {
664            b'%' => {
665                // 用 get 而非算术索引,杜绝越界
666                let hi = input.get(i + 1).copied().and_then(hex_value);
667                let lo = input.get(i + 2).copied().and_then(hex_value);
668                match (hi, lo) {
669                    (Some(h), Some(l)) => {
670                        // h/l <= 15,(h << 4) | l <= 255,不会溢出
671                        buf[j] = (h << 4) | l;
672                        j += 1;
673                        i += 3;
674                    }
675                    // Preserve:非法转义按原字节保留(仍参与模式匹配)
676                    _ => {
677                        buf[j] = b'%';
678                        j += 1;
679                        i += 1;
680                    }
681                }
682            }
683            b'+' if plus_as_space => {
684                buf[j] = b' ';
685                j += 1;
686                i += 1;
687            }
688            b => {
689                buf[j] = b;
690                j += 1;
691                i += 1;
692            }
693        }
694    }
695
696    Some(&buf[..j])
697}
698
699/// 核心解码循环:结果追加到 `out`,按策略处理非法 `%` 序列
700///
701/// - Reject:遇非法序列返回 `None`
702/// - Preserve:非法序列按原字节保留,永不失败
703fn decode_percent_vec(
704    input: &[u8],
705    plus_as_space: bool,
706    policy: InvalidSequencePolicy,
707    out: &mut Vec<u8>,
708) -> Option<()> {
709    let mut i = 0usize;
710
711    while i < input.len() {
712        match input[i] {
713            b'%' => {
714                // 用 get 而非算术索引,杜绝越界
715                let hi = input.get(i + 1).copied().and_then(hex_value);
716                let lo = input.get(i + 2).copied().and_then(hex_value);
717                match (hi, lo) {
718                    (Some(h), Some(l)) => {
719                        // h/l <= 15,(h << 4) | l <= 255,不会溢出
720                        out.push((h << 4) | l);
721                        i += 3;
722                    }
723                    _ => match policy {
724                        // fail-closed:非法序列整体拒绝,禁止部分解码后放行
725                        InvalidSequencePolicy::Reject => return None,
726                        // 非法序列按原字节保留
727                        InvalidSequencePolicy::Preserve => {
728                            out.push(b'%');
729                            i += 1;
730                        }
731                    },
732                }
733            }
734            b'+' if plus_as_space => {
735                out.push(b' ');
736                i += 1;
737            }
738            b => {
739                out.push(b);
740                i += 1;
741            }
742        }
743    }
744
745    Some(())
746}
747
748/// 十六进制字符 → 数值(0-15),非十六进制返回 None
749const fn hex_value(b: u8) -> Option<u8> {
750    match b {
751        b'0'..=b'9' => Some(b - b'0'),
752        b'a'..=b'f' => Some(b - b'a' + 10),
753        b'A'..=b'F' => Some(b - b'A' + 10),
754        _ => None,
755    }
756}
757
758/// 安全百分号编码解码
759///
760/// 只解码 unreserved 字符(RFC 3986):
761/// A-Z a-z 0-9 - _ . ~
762fn safe_percent_decode(input: &[u8]) -> Vec<u8> {
763    let mut result = Vec::with_capacity(input.len());
764    let mut i = 0;
765
766    while i < input.len() {
767        if input[i] == b'%' && i + 2 < input.len() {
768            // 检查是否为合法百分号编码
769            let hex = &input[i + 1..i + 3];
770            if let Ok(byte) = u8::from_str_radix(
771                core::str::from_utf8(hex).unwrap_or(""),
772                16,
773            ) {
774                // 只解码 unreserved 字符
775                if is_unreserved(byte) {
776                    result.push(byte);
777                    i += 3;
778                    continue;
779                }
780            }
781        }
782        result.push(input[i]);
783        i += 1;
784    }
785
786    result
787}
788
789/// 检查是否为可安全解码的字符
790///
791/// 包括 unreserved 字符(RFC 3986)和其他在 URL 路径中安全的字符:
792/// A-Z a-z 0-9 - _ . ~ SP %。
793/// '%' 可解码是有意设计:使双重编码 `%252e` 单趟解一层为字面 `%2e`
794/// (单趟解码不回扫输出,故解出的 `%2e` 不会再被解码折叠成 `.`),
795/// 与 normalize_path 的"先解码后折叠"语义配套——保证 `%2e%2e` 能被
796/// 折叠为 `..` 消除,而 `%252e%252e` 保持单层编码字面残留。
797fn is_unreserved(b: u8) -> bool {
798    b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~' | b'%')
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804
805    /// 单一实现源锁定:`split_host_port` + `unbracket_ipv6` 的
806    /// IPv6/端口语义矩阵(zenith-web IdentityMiddleware 与 SNI/Host
807    /// 一致性检查共同消费)。
808    #[test]
809    fn split_host_port_and_unbracket_ipv6_matrix() {
810        // 普通 host:port / 无端口
811        assert_eq!(split_host_port("example.com:8443"), ("example.com", Some("8443")));
812        assert_eq!(split_host_port("example.com"), ("example.com", None));
813        // 非数字尾段不是端口
814        assert_eq!(split_host_port("example.com:abc"), ("example.com:abc", None));
815        // IPv6 方括号(含/不含端口)
816        assert_eq!(split_host_port("[::1]"), ("[::1]", None));
817        assert_eq!(split_host_port("[::1]:8443"), ("[::1]", Some("8443")));
818        // IPv6 zone id 与含点混合地址
819        assert_eq!(split_host_port("[fe80::ff]:443"), ("[fe80::ff]", Some("443")));
820        assert_eq!(split_host_port("[::ffff:1.2.3.4]"), ("[::ffff:1.2.3.4]", None));
821
822        // unbracket_ipv6:成对方括号剥掉;单边/无括号输入原样返回
823        assert_eq!(unbracket_ipv6("[::1]"), "::1");
824        assert_eq!(unbracket_ipv6("[a]"), "a");
825        assert_eq!(unbracket_ipv6("::1"), "::1");
826        assert_eq!(unbracket_ipv6("example.com"), "example.com");
827        assert_eq!(unbracket_ipv6("[::"), "[::");
828        assert_eq!(unbracket_ipv6("a]"), "a]");
829        assert_eq!(unbracket_ipv6(""), "");
830    }
831
832    #[test]
833    fn test_normalize_path_simple() {
834        let result = normalize_path("/simple/path", true).unwrap();
835        assert_eq!(result, "/simple/path");
836    }
837
838    #[test]
839    fn test_normalize_path_dot_segments() {
840        let result = normalize_path("/a/./b/../c", true).unwrap();
841        assert_eq!(result, "/a/c");
842    }
843
844    #[test]
845    fn test_normalize_path_double_dot() {
846        let result = normalize_path("/a/b/c/../../d", true).unwrap();
847        assert_eq!(result, "/a/d");
848    }
849
850    #[test]
851    fn test_normalize_path_root() {
852        let result = normalize_path("/", true).unwrap();
853        assert_eq!(result, "/");
854    }
855
856    #[test]
857    fn test_normalize_path_empty() {
858        let result = normalize_path("", true).unwrap();
859        assert_eq!(result, "/");
860    }
861
862    #[test]
863    fn test_normalize_path_multiple_slashes() {
864        let result = normalize_path("//a///b", true).unwrap();
865        assert_eq!(result, "/a/b");
866    }
867
868    #[test]
869    fn test_normalize_path_percent_decode() {
870        // %7E (~) 是 unreserved,decode=true 时解码为字面字符
871        let result = normalize_path("/hello%7Eworld", true).unwrap();
872        assert_eq!(result, "/hello~world");
873    }
874
875    #[test]
876    fn test_normalize_path_no_decode() {
877        let result = normalize_path("/hello%20world", false).unwrap();
878        assert_eq!(result, "/hello%20world");
879    }
880
881    // ─────────────────────────────────────────────
882    // 编码点段折叠回归(先解码后折叠,路径穿越漏洞修复)
883    // ─────────────────────────────────────────────
884
885    #[test]
886    fn test_normalize_path_encoded_dot_segments_folded() {
887        // 回归:%2e%2e 解码为 .. 后必须参与折叠,禁止残留 .. 输出
888        assert_eq!(normalize_path("/a/%2e%2e/b", true).unwrap(), "/b");
889        assert_eq!(normalize_path("/a/%2e/b", true).unwrap(), "/a/b");
890        // 越过根目录的 .. 弹出空栈为 no-op,停在根
891        assert_eq!(
892            normalize_path("/%2e%2e/etc/passwd", true).unwrap(),
893            "/etc/passwd"
894        );
895        // 双重编码只解一层:%252e → %2e(字面入栈),不递归解码折叠
896        assert_eq!(
897            normalize_path("/a/%252e%252e/b", true).unwrap(),
898            "/a/%2e%2e/b"
899        );
900    }
901
902    #[test]
903    fn test_normalize_path_encoded_dot_segments_mixed_case() {
904        // 大小写混合十六进制同样折叠
905        assert_eq!(normalize_path("/a/%2E%2e/b", true).unwrap(), "/b");
906        assert_eq!(normalize_path("/a/%2e%2E/b", true).unwrap(), "/b");
907        assert_eq!(normalize_path("/a/%2E/b", true).unwrap(), "/a/b");
908    }
909
910    #[test]
911    fn test_normalize_path_encoded_dot_segments_reserved_kept() {
912        // %2f 为 reserved,不解码:编码斜杠不引入新分段
913        assert_eq!(
914            normalize_path("/a%2f..%2fb", true).unwrap(),
915            "/a%2f..%2fb"
916        );
917        // 段内非精确点段(解出 ... )按字面保留
918        assert_eq!(normalize_path("/a/..%2e/b", true).unwrap(), "/a/.../b");
919    }
920
921    #[test]
922    fn test_normalize_path_encoded_dot_segments_no_decode() {
923        // decode=false:编码点段保持字面,不参与折叠(新逻辑仅作用于 decode=true)
924        assert_eq!(
925            normalize_path("/a/%2e%2e/b", false).unwrap(),
926            "/a/%2e%2e/b"
927        );
928        assert_eq!(
929            normalize_path("/%2e%2e/etc/passwd", false).unwrap(),
930            "/%2e%2e/etc/passwd"
931        );
932    }
933
934    #[test]
935    fn test_normalize_query() {
936        let result = normalize_query("key=value&foo=bar", true);
937        assert_eq!(result, "key=value&foo=bar");
938    }
939
940    #[test]
941    fn test_normalize_query_with_leading_question() {
942        let result = normalize_query("?key=value", true);
943        assert_eq!(result, "key=value");
944    }
945
946    #[test]
947    fn test_normalize_query_empty() {
948        let result = normalize_query("", true);
949        assert!(result.is_empty());
950    }
951
952    #[test]
953    fn test_normalize_host_key_cache_semantics() {
954        // 小写 + trim
955        assert_eq!(normalize_host_key("  ExAmPLE.com  "), "example.com");
956        // FQDN 尾点折叠
957        assert_eq!(normalize_host_key("example.com."), "example.com");
958        assert_eq!(normalize_host_key("example.com.."), "example.com");
959        // 无条件去默认端口(scheme 无关,缓存键折叠语义)
960        assert_eq!(normalize_host_key("example.com:80"), "example.com");
961        assert_eq!(normalize_host_key("example.com:443"), "example.com");
962        // 非默认端口保留
963        assert_eq!(normalize_host_key("example.com:8080"), "example.com:8080");
964        // IPv6 字面量:方括号保留,外置端口按规则处理
965        assert_eq!(normalize_host_key("[::1]:8080"), "[::1]:8080");
966        assert_eq!(normalize_host_key("[::1]:443"), "[::1]");
967        assert_eq!(normalize_host_key("[2001:db8::a]"), "[2001:db8::a]");
968        // 非数字端口段不视为端口(保留整体原样,fail-closed 语义)
969        assert_eq!(normalize_host_key("example.com:abc"), "example.com:abc");
970        // 空输入
971        assert_eq!(normalize_host_key(""), "");
972    }
973
974    #[test]
975    fn test_normalize_authority_reverse_brackets_no_panic() {
976        // 回归(fuzz 实证缺陷):`]` 先于 `[` 时禁止切片反转 panic
977        // (对端可控 Host 字段不得触发 DoS)
978        let _ = normalize_authority("]x[", "https");
979        let _ = normalize_authority("]:80[abc", "http");
980        let _ = normalize_authority("[]", "https");
981        // IPv6 合法形式仍正确
982        assert_eq!(normalize_authority("[::1]:4444", "https"), "[::1]:4444");
983        assert_eq!(normalize_authority("[::1]:443", "https"), "[::1]");
984    }
985
986    #[test]
987    fn test_normalize_authority() {
988        let result = normalize_authority("Example.COM:443", "https");
989        assert_eq!(result, "example.com");
990    }
991
992    #[test]
993    fn test_normalize_authority_non_default_port() {
994        let result = normalize_authority("example.com:8080", "http");
995        assert_eq!(result, "example.com:8080");
996    }
997
998    #[test]
999    fn test_normalize_authority_http_default() {
1000        let result = normalize_authority("example.com:80", "http");
1001        assert_eq!(result, "example.com");
1002    }
1003
1004    #[test]
1005    fn test_normalize_authority_ipv6() {
1006        let result = normalize_authority("[::1]:8080", "http");
1007        assert_eq!(result, "[::1]:8080");
1008    }
1009
1010    #[test]
1011    fn test_normalize_headers_basic() {
1012        let headers = [("Content-Type", "application/json"), ("Accept", "text/html")];
1013        let result = normalize_request(
1014            Method::Get,
1015            "https",
1016            "example.com",
1017            "/test",
1018            "",
1019            &headers,
1020            Protocol::Http1,
1021            Transport::Tls13,
1022        )
1023        .unwrap();
1024
1025        assert_eq!(result.find_header("content-type").unwrap().value_str(), "application/json");
1026        assert_eq!(result.find_header("accept").unwrap().value_str(), "text/html");
1027    }
1028
1029    #[test]
1030    fn test_normalize_headers_merge() {
1031        let headers = [
1032            ("X-Custom", "value1"),
1033            ("x-custom", "value2"),
1034            ("Accept", "text/html"),
1035        ];
1036        let result = normalize_request(
1037            Method::Get,
1038            "https",
1039            "example.com",
1040            "/test",
1041            "",
1042            &headers,
1043            Protocol::Http1,
1044            Transport::Tls13,
1045        )
1046        .unwrap();
1047
1048        // 合并后应该只有 2 个 header
1049        assert_eq!(result.header_count(), 2);
1050        // X-Custom 应该被合并
1051        let custom = result.find_header("x-custom").unwrap();
1052        assert!(custom.value_str().contains("value1"));
1053        assert!(custom.value_str().contains("value2"));
1054    }
1055
1056    #[test]
1057    fn test_normalize_headers_merged_value_overlong_precise_error() {
1058        // SYS-026 回归:多个同名 header 聚合后总长超 MAX_HEADER_VALUE_LEN 时,
1059        // 必须返回精确错误 "header value too long"(原实现统一映射为
1060        // "header count exceeded",掩盖超长合并这一 DoS 面)。
1061        let v100 = "a".repeat(100);
1062        let headers = [
1063            ("X-Big", v100.as_str()),
1064            ("x-big", v100.as_str()),
1065            ("X-BIG", v100.as_str()),
1066        ];
1067        let result = normalize_request(
1068            Method::Get,
1069            "http",
1070            "example.com",
1071            "/",
1072            "",
1073            &headers,
1074            Protocol::Http1,
1075            Transport::Plaintext,
1076        );
1077        let err = result.expect_err("合并总长超限必须报错");
1078        assert_eq!(
1079            err.message, "header value too long",
1080            "合并超长必须返回精确错误而非 header count exceeded"
1081        );
1082    }
1083
1084    #[test]
1085    fn test_normalize_headers_invalid_name() {
1086        let headers = [("Invalid Header!", "value"), ("Valid", "ok")];
1087        let result = normalize_request(
1088            Method::Get,
1089            "https",
1090            "example.com",
1091            "/test",
1092            "",
1093            &headers,
1094            Protocol::Http1,
1095            Transport::Tls13,
1096        );
1097
1098        // 无效 header 名应导致整体规范化失败(fail-closed,不静默丢弃)
1099        let err = result.expect_err("invalid header name should be rejected");
1100        assert_eq!(err.message, "invalid header name");
1101    }
1102
1103    #[test]
1104    fn test_is_valid_header_name() {
1105        assert!(is_valid_header_name("content-type"));
1106        assert!(is_valid_header_name("x-custom-header"));
1107        assert!(is_valid_header_name("Authorization"));
1108        assert!(!is_valid_header_name("invalid header"));
1109        assert!(!is_valid_header_name("header\r\n"));
1110    }
1111
1112    #[test]
1113    fn test_full_normalize() {
1114        let headers = [
1115            ("Content-Type", "application/json"),
1116            ("Host", "Example.COM"),
1117        ];
1118        let result = normalize_request(
1119            Method::Post,
1120            "HTTPS",
1121            "Example.COM:443",
1122            "/Api/Test/./..",
1123            "?key=value",
1124            &headers,
1125            Protocol::Http2,
1126            Transport::Tls13,
1127        )
1128        .unwrap();
1129
1130        assert_eq!(result.method, Method::Post);
1131        assert_eq!(result.scheme_str(), "https");
1132        assert_eq!(result.authority_str(), "example.com");
1133        // /Api/Test/./.. -> . 跳过, .. 弹出 Test, 结果为 /Api
1134        assert_eq!(result.path_str(), "/Api");
1135        assert_eq!(result.query_str(), "key=value");
1136        assert_eq!(result.find_header("content-type").unwrap().value_str(), "application/json");
1137        assert_eq!(result.find_header("host").unwrap().value_str(), "Example.COM");
1138    }
1139
1140    #[test]
1141    fn test_safe_percent_decode() {
1142        let result = safe_percent_decode(b"hello%7Eworld");
1143        assert_eq!(result, b"hello~world");
1144
1145        // 非 unreserved 字符不解码
1146        let result = safe_percent_decode(b"test%2Fpath");
1147        assert_eq!(result, b"test%2Fpath"); // 斜杠不解码
1148    }
1149
1150    // ─────────────────────────────────────────────
1151    // 路径规范化 - 更多边界情况
1152    // ─────────────────────────────────────────────
1153
1154    #[test]
1155    fn test_normalize_path_trailing_slash() {
1156        // RFC 3986 §5.2.4:尾斜杠必须保留,区分目录与资源
1157        let result = normalize_path("/a/b/", true).unwrap();
1158        assert_eq!(result, "/a/b/");
1159    }
1160
1161    #[test]
1162    fn test_normalize_path_only_dots() {
1163        // 全部是 .. 的情况,应该停在根
1164        let result = normalize_path("/../../../..", true).unwrap();
1165        assert_eq!(result, "/");
1166    }
1167
1168    #[test]
1169    fn test_normalize_path_single_dot_root() {
1170        let result = normalize_path("/.", true).unwrap();
1171        assert_eq!(result, "/");
1172    }
1173
1174    #[test]
1175    fn test_normalize_path_double_dot_root() {
1176        let result = normalize_path("/..", true).unwrap();
1177        assert_eq!(result, "/");
1178    }
1179
1180    #[test]
1181    fn test_normalize_path_complex_dots() {
1182        let result = normalize_path("/a/b/c/./d/../e/../../f", true).unwrap();
1183        assert_eq!(result, "/a/b/f");
1184    }
1185
1186    #[test]
1187    fn test_normalize_path_multiple_dots_in_segment() {
1188        // 段内的点不应该被当作 . 或 ..
1189        let result = normalize_path("/..hidden/.bashrc/test..", true).unwrap();
1190        assert_eq!(result, "/..hidden/.bashrc/test..");
1191    }
1192
1193    #[test]
1194    fn test_normalize_path_no_leading_slash() {
1195        let result = normalize_path("a/b/c", true).unwrap();
1196        assert_eq!(result, "a/b/c");
1197    }
1198
1199    #[test]
1200    fn test_normalize_path_single_segment() {
1201        let result = normalize_path("/test", true).unwrap();
1202        assert_eq!(result, "/test");
1203    }
1204
1205    #[test]
1206    fn test_normalize_path_only_slashes() {
1207        let result = normalize_path("///", true).unwrap();
1208        assert_eq!(result, "/");
1209    }
1210
1211    #[test]
1212    fn test_normalize_path_many_slashes() {
1213        let result = normalize_path("/a///b//c////d", true).unwrap();
1214        assert_eq!(result, "/a/b/c/d");
1215    }
1216
1217    // ─────────────────────────────────────────────
1218    // 百分号解码 - 边界情况
1219    // ─────────────────────────────────────────────
1220
1221    #[test]
1222    fn test_percent_decode_mixed_case_hex() {
1223        // 大小写混合的十六进制都应该能解码
1224        let result = safe_percent_decode(b"test%2fdata");
1225        assert_eq!(result, b"test%2fdata"); // %2f 是斜杠,属于保留字符,不解码
1226
1227        let result = safe_percent_decode(b"hello%7eworld");
1228        assert_eq!(result, b"hello~world"); // %7e 是 ~,属于 unreserved
1229    }
1230
1231    #[test]
1232    fn test_percent_decode_uppercase_hex() {
1233        let result = safe_percent_decode(b"hello%7Eworld");
1234        assert_eq!(result, b"hello~world");
1235    }
1236
1237    #[test]
1238    fn test_percent_decode_incomplete_sequence() {
1239        // % 后面不足两位
1240        let result = safe_percent_decode(b"test%2");
1241        assert_eq!(result, b"test%2");
1242
1243        let result = safe_percent_decode(b"test%");
1244        assert_eq!(result, b"test%");
1245    }
1246
1247    #[test]
1248    fn test_percent_decode_invalid_hex() {
1249        // 非十六进制字符
1250        let result = safe_percent_decode(b"test%ZZdata");
1251        assert_eq!(result, b"test%ZZdata");
1252    }
1253
1254    #[test]
1255    fn test_percent_decode_unreserved_chars() {
1256        // 测试所有 unreserved 字符的解码
1257        // A-Z, a-z, 0-9, -, _, ., ~
1258        let result = safe_percent_decode(b"%41%5A%61%7A%30%39%2D%5F%2E%7E");
1259        assert_eq!(result, b"AZaz09-_.~");
1260    }
1261
1262    #[test]
1263    fn test_percent_decode_reserved_chars_kept() {
1264        // 保留字符应该保持编码状态
1265        let result = safe_percent_decode(b"%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D");
1266        assert_eq!(result, b"%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D");
1267    }
1268
1269    #[test]
1270    fn test_percent_decode_multiple_sequences() {
1271        let result = safe_percent_decode(b"%7E%7Ehello%7Eworld%7E%7E");
1272        assert_eq!(result, b"~~hello~world~~");
1273    }
1274
1275    #[test]
1276    fn test_percent_decode_empty_input() {
1277        let result = safe_percent_decode(b"");
1278        assert_eq!(result, b"");
1279    }
1280
1281    // ─────────────────────────────────────────────
1282    // percent_decode(fail-closed 公开 API)
1283    // ─────────────────────────────────────────────
1284
1285    #[test]
1286    fn test_percent_decode_pub_basic() {
1287        assert_eq!(percent_decode("hello%20world", false).unwrap(), "hello world");
1288        assert_eq!(percent_decode("%41%42%43", false).unwrap(), "ABC");
1289        assert_eq!(percent_decode("plain", false).unwrap(), "plain");
1290        assert_eq!(percent_decode("", false).unwrap(), "");
1291    }
1292
1293    #[test]
1294    fn test_percent_decode_pub_plus_as_space() {
1295        // plus_as_space=true:'+' 解码为空格(表单语义)
1296        assert_eq!(percent_decode("a+b+c", true).unwrap(), "a b c");
1297        // plus_as_space=false:'+' 原样保留
1298        assert_eq!(percent_decode("a+b", false).unwrap(), "a+b");
1299        // %2B 始终解码为 '+',与 plus_as_space 无关
1300        assert_eq!(percent_decode("a%2Bb", true).unwrap(), "a+b");
1301    }
1302
1303    #[test]
1304    fn test_percent_decode_pub_invalid_sequences() {
1305        // '%' 在末尾(不足两位)→ None
1306        assert!(percent_decode("test%", false).is_none());
1307        assert!(percent_decode("test%2", false).is_none());
1308        // 非十六进制字符 → None
1309        assert!(percent_decode("test%ZZdata", false).is_none());
1310        assert!(percent_decode("%2g", false).is_none());
1311        assert!(percent_decode("%g2", false).is_none());
1312        // 解码结果非合法 UTF-8 → None
1313        assert!(percent_decode("%FF%FE", false).is_none());
1314    }
1315
1316    #[test]
1317    fn test_percent_decode_pub_mixed_case_hex() {
1318        assert_eq!(percent_decode("%7e", false).unwrap(), "~");
1319        assert_eq!(percent_decode("%7E", false).unwrap(), "~");
1320        assert_eq!(percent_decode("%2f", false).unwrap(), "/");
1321        assert_eq!(percent_decode("%2F", false).unwrap(), "/");
1322    }
1323
1324    // ─────────────────────────────────────────────
1325    // percent_decode_with_policy(统一解码 API)
1326    // ─────────────────────────────────────────────
1327
1328    #[test]
1329    fn test_policy_reject_delegates_percent_decode() {
1330        // Reject 策略必须与 percent_decode 语义完全一致
1331        let cases = [
1332            "hello%20world",
1333            "a+b",
1334            "test%",
1335            "test%2",
1336            "test%ZZdata",
1337            "%FF%FE",
1338            "%41%42%43",
1339            "",
1340        ];
1341        for case in cases {
1342            for plus in [false, true] {
1343                assert_eq!(
1344                    percent_decode_with_policy(case, plus, InvalidSequencePolicy::Reject),
1345                    percent_decode(case, plus),
1346                    "Reject 策略与 percent_decode 不一致: {:?} (plus={})",
1347                    case,
1348                    plus
1349                );
1350            }
1351        }
1352    }
1353
1354    #[test]
1355    fn test_policy_preserve_invalid_sequences() {
1356        // 非法 % 序列按原字节保留
1357        assert_eq!(
1358            percent_decode_with_policy("test%ZZdata", false, InvalidSequencePolicy::Preserve).unwrap(),
1359            "test%ZZdata"
1360        );
1361        assert_eq!(
1362            percent_decode_with_policy("%", false, InvalidSequencePolicy::Preserve).unwrap(),
1363            "%"
1364        );
1365        assert_eq!(
1366            percent_decode_with_policy("%2", false, InvalidSequencePolicy::Preserve).unwrap(),
1367            "%2"
1368        );
1369        // 混合:合法序列正常解码,非法序列保留
1370        assert_eq!(
1371            percent_decode_with_policy("100%+pure", true, InvalidSequencePolicy::Preserve).unwrap(),
1372            "100% pure"
1373        );
1374        // 保留后仍可能形成新的合法序列边界:%2 后跟 Z 不消费 Z
1375        assert_eq!(
1376            percent_decode_with_policy("%2Z%41", false, InvalidSequencePolicy::Preserve).unwrap(),
1377            "%2ZA"
1378        );
1379    }
1380
1381    #[test]
1382    fn test_policy_preserve_invalid_utf8_rejected() {
1383        // Preserve 策略下非法 UTF-8 仍整体拒绝(字符串级 API 约束)
1384        assert!(percent_decode_with_policy("%FF", false, InvalidSequencePolicy::Preserve).is_none());
1385        assert!(percent_decode_with_policy("%FF%FE", true, InvalidSequencePolicy::Preserve).is_none());
1386    }
1387
1388    #[test]
1389    fn test_policy_preserve_plus_as_space() {
1390        assert_eq!(
1391            percent_decode_with_policy("a+b+c", true, InvalidSequencePolicy::Preserve).unwrap(),
1392            "a b c"
1393        );
1394        assert_eq!(
1395            percent_decode_with_policy("a+b", false, InvalidSequencePolicy::Preserve).unwrap(),
1396            "a+b"
1397        );
1398    }
1399
1400    // ─────────────────────────────────────────────
1401    // 字节级解码(percent_decode_bytes / percent_decode_bytes_into)
1402    // ─────────────────────────────────────────────
1403
1404    #[test]
1405    fn test_percent_decode_bytes_basic() {
1406        assert_eq!(percent_decode_bytes(b"hello%20world", false), b"hello world");
1407        assert_eq!(percent_decode_bytes(b"a+b", true), b"a b");
1408        assert_eq!(percent_decode_bytes(b"a+b", false), b"a+b");
1409        // 非法序列保留
1410        assert_eq!(percent_decode_bytes(b"%ZZ", false), b"%ZZ");
1411        assert_eq!(percent_decode_bytes(b"%", false), b"%");
1412        assert_eq!(percent_decode_bytes(b"%2", false), b"%2");
1413        // 字节级不做 UTF-8 校验
1414        assert_eq!(percent_decode_bytes(b"%FF", false), b"\xFF");
1415        assert_eq!(percent_decode_bytes(b"", false), b"");
1416    }
1417
1418    #[test]
1419    fn test_percent_decode_bytes_into_basic() {
1420        let mut buf = [0u8; 64];
1421        assert_eq!(
1422            percent_decode_bytes_into(b"id=%27+OR+%271%27%3D%271", &mut buf, true),
1423            Some(&b"id=' OR '1'='1"[..])
1424        );
1425        // 非 UTF-8 字节允许存在
1426        assert_eq!(
1427            percent_decode_bytes_into(b"%FF", &mut buf, true),
1428            Some(&b"\xFF"[..])
1429        );
1430        // 非法序列保留
1431        assert_eq!(
1432            percent_decode_bytes_into(b"%ZZ", &mut buf, true),
1433            Some(&b"%ZZ"[..])
1434        );
1435    }
1436
1437    #[test]
1438    fn test_percent_decode_bytes_into_buffer_overflow_fail_closed() {
1439        // 输出超过缓冲区容量 → fail-closed 返回 None(禁止截断放行)
1440        let mut tiny = [0u8; 2];
1441        assert_eq!(percent_decode_bytes_into(b"abcdef", &mut tiny, true), None);
1442        // 恰好填满缓冲区 → 成功
1443        let mut exact = [0u8; 6];
1444        assert_eq!(
1445            percent_decode_bytes_into(b"abcdef", &mut exact, true),
1446            Some(&b"abcdef"[..])
1447        );
1448        // 空输入 + 空缓冲区 → 成功(无输出需求)
1449        let mut empty: [u8; 0] = [];
1450        assert_eq!(
1451            percent_decode_bytes_into(b"", &mut empty, true),
1452            Some(&b""[..])
1453        );
1454    }
1455
1456    #[test]
1457    fn test_hex_value_all() {
1458        assert_eq!(hex_value(b'0'), Some(0));
1459        assert_eq!(hex_value(b'9'), Some(9));
1460        assert_eq!(hex_value(b'a'), Some(10));
1461        assert_eq!(hex_value(b'f'), Some(15));
1462        assert_eq!(hex_value(b'A'), Some(10));
1463        assert_eq!(hex_value(b'F'), Some(15));
1464        assert_eq!(hex_value(b'g'), None);
1465        assert_eq!(hex_value(b'G'), None);
1466        assert_eq!(hex_value(b' '), None);
1467        assert_eq!(hex_value(b'%'), None);
1468    }
1469
1470    // ─────────────────────────────────────────────
1471    // 查询字符串规范化 - 更多边界
1472    // ─────────────────────────────────────────────
1473
1474    #[test]
1475    fn test_normalize_query_multiple_question_marks() {
1476        // 前导 '?' 全部移除;query 中间的 '?' 是合法数据字符,必须保留完整
1477        let result = normalize_query("??key=value??foo=bar", true);
1478        assert_eq!(result, "key=value??foo=bar");
1479    }
1480
1481    #[test]
1482    fn test_normalize_query_whitespace_normalization() {
1483        // Unicode 空白 → ASCII 空格保留;%20 非 unreserved(RFC 3986),decode=true 也不解码
1484        let result = normalize_query("key=hello%20world\t\n", true);
1485        assert_eq!(result, "key=hello%20world  ");
1486    }
1487
1488    #[test]
1489    fn test_normalize_query_decode_true_unreserved_only() {
1490        // decode=true:与路径相同的 unreserved-only 解码
1491        assert_eq!(normalize_query("a=%41%2e%2e&b=%7E", true), "a=A..&b=~");
1492        // reserved 字符保持编码态
1493        assert_eq!(normalize_query("p=%2f%3F%23", true), "p=%2f%3F%23");
1494        // 非法 % 序列按字节透传(safe_percent_decode 语义)
1495        assert_eq!(normalize_query("q=100%+pure", true), "q=100%+pure");
1496    }
1497
1498    #[test]
1499    fn test_normalize_query_decode_false_passthrough() {
1500        // decode=false:保留原始编码(现状行为不变)
1501        assert_eq!(normalize_query("a=%41%2e&b=%2f", false), "a=%41%2e&b=%2f");
1502        // 空白映射不受 decode 开关影响
1503        assert_eq!(normalize_query("a=b\tc", false), "a=b c");
1504    }
1505
1506    #[test]
1507    fn test_normalize_query_only_question_marks() {
1508        let result = normalize_query("???", true);
1509        assert_eq!(result, "");
1510    }
1511
1512    #[test]
1513    fn test_normalize_query_complex() {
1514        let result = normalize_query("?a=1&b=2&c=3", true);
1515        assert_eq!(result, "a=1&b=2&c=3");
1516    }
1517
1518    // ─────────────────────────────────────────────
1519    // Authority 规范化 - 更多边界
1520    // ─────────────────────────────────────────────
1521
1522    #[test]
1523    fn test_normalize_authority_empty() {
1524        let result = normalize_authority("", "http");
1525        assert_eq!(result, "");
1526    }
1527
1528    #[test]
1529    fn test_normalize_authority_no_port_http() {
1530        let result = normalize_authority("example.com", "http");
1531        assert_eq!(result, "example.com");
1532    }
1533
1534    #[test]
1535    fn test_normalize_authority_no_port_https() {
1536        let result = normalize_authority("example.com", "https");
1537        assert_eq!(result, "example.com");
1538    }
1539
1540    #[test]
1541    fn test_normalize_authority_host_lowercase() {
1542        let result = normalize_authority("EXAMPLE.COM", "http");
1543        assert_eq!(result, "example.com");
1544    }
1545
1546    #[test]
1547    fn test_normalize_authority_mixed_case() {
1548        let result = normalize_authority("My-Host.Example.COM:8080", "http");
1549        assert_eq!(result, "my-host.example.com:8080");
1550    }
1551
1552    #[test]
1553    fn test_normalize_authority_non_numeric_port() {
1554        // 非数字端口应该被当作主机名的一部分
1555        let result = normalize_authority("example.com:http", "http");
1556        assert_eq!(result, "example.com:http");
1557    }
1558
1559    #[test]
1560    fn test_normalize_authority_ipv6_no_port() {
1561        let result = normalize_authority("[::1]", "http");
1562        assert_eq!(result, "[::1]");
1563    }
1564
1565    #[test]
1566    fn test_normalize_authority_ipv6_default_port() {
1567        let result = normalize_authority("[::1]:443", "https");
1568        assert_eq!(result, "[::1]");
1569    }
1570
1571    #[test]
1572    fn test_normalize_authority_ipv6_unclosed_bracket() {
1573        let result = normalize_authority("[::1:8080", "http");
1574        assert_eq!(result, "[::1:8080");
1575    }
1576
1577    #[test]
1578    fn test_normalize_authority_unknown_scheme() {
1579        // 未知协议不省略任何端口
1580        let result = normalize_authority("example.com:1234", "ftp");
1581        assert_eq!(result, "example.com:1234");
1582    }
1583
1584    // ─────────────────────────────────────────────
1585    // Header 规范化 - 更多场景
1586    // ─────────────────────────────────────────────
1587
1588    #[test]
1589    fn test_normalize_headers_pseudo_headers_filtered() {
1590        let headers = [
1591            (":method", "GET"),
1592            (":path", "/test"),
1593            (":scheme", "https"),
1594            ("content-type", "application/json"),
1595        ];
1596        let result = normalize_request(
1597            Method::Get,
1598            "https",
1599            "example.com",
1600            "/test",
1601            "",
1602            &headers,
1603            Protocol::Http2,
1604            Transport::Tls13,
1605        )
1606        .unwrap();
1607
1608        // 伪头部应该被过滤掉
1609        assert_eq!(result.header_count(), 1);
1610        assert!(result.find_header("content-type").is_some());
1611    }
1612
1613    #[test]
1614    fn test_normalize_headers_value_trim() {
1615        let headers = [("X-Test", "  hello world  ")];
1616        let result = normalize_request(
1617            Method::Get,
1618            "http",
1619            "example.com",
1620            "/",
1621            "",
1622            &headers,
1623            Protocol::Http1,
1624            Transport::Plaintext,
1625        )
1626        .unwrap();
1627
1628        let hdr = result.find_header("x-test").unwrap();
1629        assert_eq!(hdr.value_str(), "hello world");
1630    }
1631
1632    #[test]
1633    fn test_normalize_headers_empty_value() {
1634        let headers = [("X-Empty", ""), ("X-Normal", "value")];
1635        let result = normalize_request(
1636            Method::Get,
1637            "http",
1638            "example.com",
1639            "/",
1640            "",
1641            &headers,
1642            Protocol::Http1,
1643            Transport::Plaintext,
1644        )
1645        .unwrap();
1646
1647        assert_eq!(result.header_count(), 2);
1648        assert_eq!(result.find_header("x-empty").unwrap().value_str(), "");
1649    }
1650
1651    #[test]
1652    fn test_normalize_headers_no_merge_mode() {
1653        let config = NormalizeConfig {
1654            merge_headers: false,
1655            ..NormalizeConfig::default()
1656        };
1657        let headers = [("X-Custom", "v1"), ("x-custom", "v2")];
1658        let result = normalize_request_with_config(
1659            Method::Get,
1660            "http",
1661            "example.com",
1662            "/",
1663            "",
1664            &headers,
1665            Protocol::Http1,
1666            Transport::Plaintext,
1667            config,
1668        )
1669        .unwrap();
1670
1671        // 不合并模式下,同名 header 保留两个
1672        assert_eq!(result.header_count(), 2);
1673    }
1674
1675    #[test]
1676    fn test_normalize_headers_no_normalize_case() {
1677        let config = NormalizeConfig {
1678            normalize_case: false,
1679            ..NormalizeConfig::default()
1680        };
1681        let headers = [("Content-Type", "application/json")];
1682        let result = normalize_request_with_config(
1683            Method::Get,
1684            "http",
1685            "example.com",
1686            "/",
1687            "",
1688            &headers,
1689            Protocol::Http1,
1690            Transport::Plaintext,
1691            config,
1692        )
1693        .unwrap();
1694
1695        // 不规范化大小写:存储保留原始大小写;
1696        // find_header 按 RFC 7230 §3.2 大小写不敏感,两种写法都能命中
1697        assert!(result.find_header("Content-Type").is_some());
1698        assert!(result.find_header("content-type").is_some());
1699        let hdr = result.find_header("content-type").unwrap();
1700        assert_eq!(hdr.name_str(), "Content-Type");
1701    }
1702
1703    #[test]
1704    fn test_normalize_headers_all_config_off() {
1705        let config = NormalizeConfig {
1706            decode_path: false,
1707            merge_headers: false,
1708            normalize_case: false,
1709        };
1710        let headers = [("X-Test", "A"), ("x-test", "B")];
1711        let result = normalize_request_with_config(
1712            Method::Get,
1713            "HTTP",
1714            "Example.COM",
1715            "/hello%20world",
1716            "",
1717            &headers,
1718            Protocol::Http1,
1719            Transport::Plaintext,
1720            config,
1721        )
1722        .unwrap();
1723
1724        // scheme 不规范化
1725        assert_eq!(result.scheme_str(), "HTTP");
1726        // 路径不解码
1727        assert_eq!(result.path_str(), "/hello%20world");
1728        // header 不合并,大小不变
1729        assert_eq!(result.header_count(), 2);
1730    }
1731
1732    #[test]
1733    fn test_normalize_headers_header_count_exceeded() {
1734        let mut header_names: Vec<String> = Vec::new();
1735        for i in 0..100 {
1736            header_names.push(format!("X-Header-{}", i));
1737        }
1738        let header_refs: Vec<(&str, &str)> = header_names.iter().map(|k| (k.as_str(), "value")).collect();
1739
1740        let result = normalize_request(
1741            Method::Get,
1742            "http",
1743            "example.com",
1744            "/",
1745            "",
1746            &header_refs,
1747            Protocol::Http1,
1748            Transport::Plaintext,
1749        );
1750
1751        // 超过最大 header 数量应该失败
1752        assert!(result.is_err());
1753    }
1754
1755    // ─────────────────────────────────────────────
1756    // is_valid_header_name 更多边界
1757    // ─────────────────────────────────────────────
1758
1759    #[test]
1760    fn test_is_valid_header_name_all_special_chars() {
1761        // 所有允许的特殊字符
1762        assert!(is_valid_header_name("!#$%&'*+-.^_`|~"));
1763    }
1764
1765    #[test]
1766    fn test_is_valid_header_name_empty() {
1767        assert!(!is_valid_header_name(""));
1768    }
1769
1770    #[test]
1771    fn test_is_valid_header_name_with_spaces() {
1772        assert!(!is_valid_header_name("content type"));
1773    }
1774
1775    #[test]
1776    fn test_is_valid_header_name_with_colon() {
1777        assert!(!is_valid_header_name("content-type:"));
1778    }
1779
1780    #[test]
1781    fn test_is_valid_header_name_with_newline() {
1782        assert!(!is_valid_header_name("content\r\ntype"));
1783    }
1784
1785    #[test]
1786    fn test_is_valid_header_name_with_null() {
1787        assert!(!is_valid_header_name("content\0type"));
1788    }
1789
1790    // ─────────────────────────────────────────────
1791    // NormalizeError Display 测试
1792    // ─────────────────────────────────────────────
1793
1794    #[test]
1795    fn test_normalize_error_display() {
1796        let err = NormalizeError {
1797            message: "test error message",
1798        };
1799        assert_eq!(
1800            format!("{}", err),
1801            "NormalizeError: test error message"
1802        );
1803    }
1804
1805    // ─────────────────────────────────────────────
1806    // NormalizeConfig 默认值测试
1807    // ─────────────────────────────────────────────
1808
1809    #[test]
1810    fn test_normalize_config_default() {
1811        let config = NormalizeConfig::default();
1812        assert!(config.decode_path);
1813        assert!(config.merge_headers);
1814        assert!(config.normalize_case);
1815    }
1816
1817    // ─────────────────────────────────────────────
1818    // 完整规范化 - 各种协议组合
1819    // ─────────────────────────────────────────────
1820
1821    #[test]
1822    fn test_full_normalize_http1_plaintext() {
1823        let result = normalize_request(
1824            Method::Get,
1825            "http",
1826            "example.com:80",
1827            "/path/../to/./resource",
1828            "?q=test",
1829            &[("Host", "example.com"), ("Accept", "text/html")],
1830            Protocol::Http1,
1831            Transport::Plaintext,
1832        )
1833        .unwrap();
1834
1835        assert_eq!(result.method, Method::Get);
1836        assert_eq!(result.protocol, Protocol::Http1);
1837        assert_eq!(result.transport, Transport::Plaintext);
1838        assert_eq!(result.scheme_str(), "http");
1839        assert_eq!(result.authority_str(), "example.com");
1840        assert_eq!(result.path_str(), "/to/resource");
1841        assert_eq!(result.query_str(), "q=test");
1842    }
1843
1844    #[test]
1845    fn test_full_normalize_http2_tls() {
1846        let result = normalize_request(
1847            Method::Post,
1848            "https",
1849            "api.example.com:443",
1850            "/api/v1/data",
1851            "verbose=true",
1852            &[
1853                (":method", "POST"),
1854                (":scheme", "https"),
1855                (":path", "/api/v1/data"),
1856                ("content-type", "application/json"),
1857                ("content-type", "text/plain"),
1858            ],
1859            Protocol::Http2,
1860            Transport::Tls13,
1861        )
1862        .unwrap();
1863
1864        assert_eq!(result.protocol, Protocol::Http2);
1865        assert_eq!(result.transport, Transport::Tls13);
1866        // 伪头部被过滤
1867        assert_eq!(result.header_count(), 1);
1868        // 同名 header 被合并
1869        let ct = result.find_header("content-type").unwrap();
1870        assert!(ct.value_str().contains("application/json"));
1871        assert!(ct.value_str().contains("text/plain"));
1872    }
1873
1874    #[test]
1875    fn test_full_normalize_http3() {
1876        let result = normalize_request(
1877            Method::Get,
1878            "https",
1879            "quic.example.com",
1880            "/",
1881            "",
1882            &[("user-agent", "test-agent")],
1883            Protocol::Http3,
1884            Transport::Tls13,
1885        )
1886        .unwrap();
1887
1888        assert_eq!(result.protocol, Protocol::Http3);
1889        assert_eq!(result.transport, Transport::Tls13);
1890        assert_eq!(result.path_str(), "/");
1891    }
1892
1893    // ─────────────────────────────────────────────
1894    // 方法规范化(通过 normalize_request)
1895    // ─────────────────────────────────────────────
1896
1897    #[test]
1898    fn test_normalize_all_methods() {
1899        let methods = [
1900            Method::Get,
1901            Method::Post,
1902            Method::Put,
1903            Method::Delete,
1904            Method::Patch,
1905            Method::Head,
1906            Method::Options,
1907            Method::Connect,
1908            Method::Trace,
1909        ];
1910
1911        for method in methods.iter() {
1912            let result = normalize_request(
1913                *method,
1914                "http",
1915                "example.com",
1916                "/",
1917                "",
1918                &[],
1919                Protocol::Http1,
1920                Transport::Plaintext,
1921            )
1922            .unwrap();
1923            assert_eq!(result.method, *method);
1924        }
1925    }
1926
1927    // ─────────────────────────────────────────────
1928    // normalize_request 链路 fail-closed(超长拒绝,禁止静默截断)
1929    // ─────────────────────────────────────────────
1930
1931    #[test]
1932    fn test_normalize_request_overlong_path_rejected() {
1933        let long_path = format!("/{}", "a".repeat(crate::MAX_PATH_LEN + 1));
1934        let result = normalize_request(
1935            Method::Get,
1936            "http",
1937            "example.com",
1938            &long_path,
1939            "",
1940            &[],
1941            Protocol::Http1,
1942            Transport::Plaintext,
1943        );
1944        let err = result.expect_err("超长路径必须报错");
1945        assert_eq!(err.message, "path too long");
1946    }
1947
1948    #[test]
1949    fn test_normalize_request_overlong_query_rejected() {
1950        let long_query = "a".repeat(crate::MAX_QUERY_LEN + 1);
1951        let result = normalize_request(
1952            Method::Get,
1953            "http",
1954            "example.com",
1955            "/",
1956            &long_query,
1957            &[],
1958            Protocol::Http1,
1959            Transport::Plaintext,
1960        );
1961        let err = result.expect_err("超长 query 必须报错");
1962        assert_eq!(err.message, "query too long");
1963    }
1964
1965    #[test]
1966    fn test_normalize_request_overlong_authority_rejected() {
1967        let long_authority = "a".repeat(crate::MAX_AUTHORITY_LEN + 1);
1968        let result = normalize_request(
1969            Method::Get,
1970            "http",
1971            &long_authority,
1972            "/",
1973            "",
1974            &[],
1975            Protocol::Http1,
1976            Transport::Plaintext,
1977        );
1978        let err = result.expect_err("超长 authority 必须报错");
1979        assert_eq!(err.message, "authority too long");
1980    }
1981
1982    #[test]
1983    fn test_normalize_request_overlong_scheme_rejected() {
1984        // scheme 固定容量 8 字节,超长整体拒绝
1985        let result = normalize_request(
1986            Method::Get,
1987            "superlongscheme",
1988            "example.com",
1989            "/",
1990            "",
1991            &[],
1992            Protocol::Http1,
1993            Transport::Plaintext,
1994        );
1995        let err = result.expect_err("超长 scheme 必须报错");
1996        assert_eq!(err.message, "scheme too long");
1997    }
1998
1999    // ─────────────────────────────────────────────
2000    // normalize_authority 与 normalize_host_key 语义对照(锁定既定分工)
2001    // ─────────────────────────────────────────────
2002
2003    #[test]
2004    fn test_authority_vs_host_key_semantics_lock() {
2005        // 默认端口策略:authority scheme 感知;host_key 无条件剥 80/443
2006        // http 下 443 非默认端口
2007        assert_eq!(
2008            normalize_authority("Example.COM:443", "http"),
2009            "example.com:443"
2010        );
2011        assert_eq!(normalize_host_key("Example.COM:443"), "example.com");
2012        // https 下两者都省略 443
2013        assert_eq!(
2014            normalize_authority("Example.COM:443", "https"),
2015            "example.com"
2016        );
2017
2018        // FQDN 尾点:authority 保留,host_key 剥离
2019        assert_eq!(normalize_authority("example.com.", "http"), "example.com.");
2020        assert_eq!(normalize_host_key("example.com."), "example.com");
2021
2022        // IPv6:http 下 443 非默认 → authority 保留;host_key 无条件剥离
2023        assert_eq!(normalize_authority("[::1]:443", "http"), "[::1]:443");
2024        assert_eq!(normalize_host_key("[::1]:443"), "[::1]");
2025        // https 下 authority 同样省略
2026        assert_eq!(normalize_authority("[::1]:443", "https"), "[::1]");
2027
2028        // 空白:host_key trim,authority 不 trim
2029        assert_eq!(normalize_host_key("  example.com  "), "example.com");
2030        assert_eq!(
2031            normalize_authority("  example.com  ", "http"),
2032            "  example.com  "
2033        );
2034    }
2035}