zenith-web 0.1.0

Zenith Web 应用框架:编译期 Trie 路由、类型化 Extractor、中间件 DAG、静态文件服务、统一错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
//! 类型化 Extractor / Responder 系统
//!
//! 提供从 CanonicalRequest 中提取类型化参数的能力,
//! 以及将处理结果转换为 CanonicalResponse 的能力。

use rustc_hash::FxHashMap;

use zenith_api::normalize::{percent_decode_with_policy, InvalidSequencePolicy};
use zenith_api::{CanonicalRequest, CanonicalResponse};

/// 提取错误
#[derive(Debug, Clone)]
pub enum ExtractError {
    /// 参数未找到
    NotFound(String),
    /// 参数类型转换失败
    ParseError {
        /// 参数名
        name: String,
        /// 期望的类型
        expected: &'static str,
    },
    /// 值超出范围
    OutOfRange {
        /// 参数名
        name: String,
        /// 实际值
        value: String,
    },
    /// 自定义错误
    Custom(String),
}

impl std::fmt::Display for ExtractError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ExtractError::NotFound(name) => write!(f, "Parameter not found: {}", name),
            ExtractError::ParseError { name, expected } => {
                write!(f, "Failed to parse '{}' as {}", name, expected)
            }
            ExtractError::OutOfRange { name, value } => {
                write!(f, "Parameter '{}' value '{}' out of range", name, value)
            }
            ExtractError::Custom(msg) => write!(f, "{}", msg),
        }
    }
}

impl std::error::Error for ExtractError {}

/// 提取器特征
pub trait FromRequest: Sized {
    /// 从请求中提取类型化参数
    fn from_request(request: &CanonicalRequest, params: &FxHashMap<String, String>) -> Result<Self, ExtractError>;
}

/// 响应器特征
pub trait IntoResponse {
    /// 转换为响应
    fn into_response(self) -> CanonicalResponse;
}

// ---------------------------------------------------------------------------
// 路径参数提取器
// ---------------------------------------------------------------------------

/// 路径参数(按名称提取)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathParam<'a> {
    /// 参数名
    pub name: &'a str,
    /// 参数值
    pub value: String,
}

impl<'a> PathParam<'a> {
    /// 返回参数值的字符串切片
    pub fn as_str(&self) -> &str {
        &self.value
    }

    /// 解析参数值为指定类型
    pub fn parse<T: std::str::FromStr>(&self) -> Result<T, ExtractError> {
        self.value.parse::<T>().map_err(|_| ExtractError::ParseError {
            name: self.name.to_string(),
            expected: std::any::type_name::<T>(),
        })
    }
}

/// 从路径参数按名称提取
pub fn path_param<'a>(
    params: &'a FxHashMap<String, String>,
    name: &'a str,
) -> Result<PathParam<'a>, ExtractError> {
    params
        .get(name)
        .map(|v| PathParam { name, value: v.clone() })
        .ok_or_else(|| ExtractError::NotFound(name.to_string()))
}

/// 路径参数提取为指定类型
pub fn path_param_parse<T>(
    params: &FxHashMap<String, String>,
    name: &str,
) -> Result<T, ExtractError>
where
    T: std::str::FromStr,
{
    let param = path_param(params, name)?;
    param.parse::<T>()
}

// ---------------------------------------------------------------------------
// 查询参数提取器
// ---------------------------------------------------------------------------

/// 解析查询字符串为 HashMap
pub fn parse_query(query: &str) -> FxHashMap<String, String> {
    let mut params = FxHashMap::default();
    if query.is_empty() {
        return params;
    }

    for pair in query.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            // 解码失败(非法 UTF-8)时跳过该参数:
            // 废除旧的"静默变空串"语义,避免与 WAF 解码结果产生差分
            let (Some(decoded_key), Some(decoded_value)) = (url_decode(key), url_decode(value))
            else {
                continue;
            };
            params.insert(decoded_key, decoded_value);
        } else if !pair.is_empty()
            && let Some(decoded) = url_decode(pair)
        {
            params.insert(decoded, String::new());
        }
    }

    params
}

/// URL 解码(百分号编码)
///
/// 统一委托 `zenith_api::normalize::percent_decode_with_policy`
/// (Preserve 策略,`+` 转空格),与 WAF 使用同一套解码语义,
/// 消除 WAF 与应用层的协议差分:
/// - 非法 `%` 序列按原样保留(`%ZZ` → `%ZZ`)
/// - `+` 解码为空格(`application/x-www-form-urlencoded` 语义)
/// - 解码结果非合法 UTF-8 时返回 `None`,由调用方跳过该参数
///   (废除旧的"非法 UTF-8 静默变空串"语义)
pub fn url_decode(s: &str) -> Option<String> {
    percent_decode_with_policy(s, true, InvalidSequencePolicy::Preserve)
}

/// 查询参数提取
///
/// 使用线性扫描直接定位目标 key,避免每次调用分配整个 `FxHashMap`。
/// URL 解码语义与 `parse_query` 完全一致(Preserve 策略、`+` 转空格)。
pub fn query_param(
    request: &CanonicalRequest,
    name: &str,
) -> Result<String, ExtractError> {
    let query = request.query_str();
    if query.is_empty() {
        return Err(ExtractError::NotFound(name.to_string()));
    }
    for pair in query.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            let Some(decoded_key) = url_decode(key) else { continue };
            let Some(decoded_value) = url_decode(value) else { continue };
            if decoded_key == name {
                return Ok(decoded_value);
            }
        } else if !pair.is_empty() {
            let Some(decoded) = url_decode(pair) else { continue };
            if decoded == name {
                return Ok(String::new());
            }
        }
    }
    Err(ExtractError::NotFound(name.to_string()))
}

/// 查询参数提取(带默认值)
pub fn query_param_or(request: &CanonicalRequest, name: &str, default: &str) -> String {
    query_param(request, name).unwrap_or_else(|_| default.to_string())
}

/// 查询参数解析为指定类型
pub fn query_param_parse<T>(request: &CanonicalRequest, name: &str) -> Result<T, ExtractError>
where
    T: std::str::FromStr,
{
    let value = query_param(request, name)?;
    value.parse::<T>().map_err(|_| ExtractError::ParseError {
        name: name.to_string(),
        expected: std::any::type_name::<T>(),
    })
}

// ---------------------------------------------------------------------------
// 请求头提取器
// ---------------------------------------------------------------------------

/// 从请求头按名称提取
pub fn header_value<'a>(request: &'a CanonicalRequest, name: &str) -> Option<&'a str> {
    request.find_header(name).map(|h| h.value_str())
}

/// 从请求头提取(必需)
pub fn header_required<'a>(request: &'a CanonicalRequest, name: &str) -> Result<&'a str, ExtractError> {
    header_value(request, name).ok_or_else(|| ExtractError::NotFound(name.to_string()))
}

// ---------------------------------------------------------------------------
// 常用提取器类型
// ---------------------------------------------------------------------------

/// 提取路径参数 ID(u64)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UserId(pub u64);

impl UserId {
    /// 从路径参数中提取 ID
    pub fn from_params(params: &FxHashMap<String, String>) -> Result<Self, ExtractError> {
        let id: u64 = path_param_parse(params, "id")?;
        Ok(Self(id))
    }
}

impl FromRequest for UserId {
    fn from_request(_request: &CanonicalRequest, params: &FxHashMap<String, String>) -> Result<Self, ExtractError> {
        Self::from_params(params)
    }
}

/// 提取分页参数
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pagination {
    /// 当前页码(1-based)
    pub page: u32,
    /// 每页条数
    pub per_page: u32,
}

impl Pagination {
    /// 从请求查询参数中提取分页参数
    pub fn from_request(request: &CanonicalRequest) -> Self {
        let page = query_param_parse::<u32>(request, "page").unwrap_or(1);
        let per_page = query_param_parse::<u32>(request, "per_page").unwrap_or(20);
        Self { page, per_page }
    }

    /// 计算偏移量(用于数据库查询)
    ///
    /// page 为 0 时饱和返回 0(禁止 (page-1) 下溢为 u32::MAX 导致巨大偏移)
    pub fn offset(&self) -> u32 {
        // page 为 1-based:page=1 → offset=0, page=2 → offset=per_page, ...
        // page=0 是非法值但 from_request 默认返回 1,此处防御性处理
        if self.page == 0 {
            return 0;
        }
        // page >= 1 时 (page-1) 不会下溢;使用 saturating_mul 防止大 page*per_page 溢出
        self.page.saturating_sub(1).saturating_mul(self.per_page)
    }
}

impl FromRequest for Pagination {
    fn from_request(request: &CanonicalRequest, _params: &FxHashMap<String, String>) -> Result<Self, ExtractError> {
        Ok(Self::from_request(request))
    }
}

// ---------------------------------------------------------------------------
// 响应器实现
// ---------------------------------------------------------------------------

impl IntoResponse for &str {
    fn into_response(self) -> CanonicalResponse {
        let mut response = CanonicalResponse::new(200);
        let _ = response
            .add_header(b"content-type", b"text/plain");
        response.set_body(self.as_bytes().to_vec());
        response
    }
}

impl IntoResponse for String {
    fn into_response(self) -> CanonicalResponse {
        let mut response = CanonicalResponse::new(200);
        let _ = response
            .add_header(b"content-type", b"text/plain");
        response.set_body(self.into_bytes());
        response
    }
}

impl IntoResponse for &String {
    fn into_response(self) -> CanonicalResponse {
        let mut response = CanonicalResponse::new(200);
        let _ = response
            .add_header(b"content-type", b"text/plain");
        response.set_body(self.as_bytes().to_vec());
        response
    }
}

impl IntoResponse for () {
    fn into_response(self) -> CanonicalResponse {
        CanonicalResponse::new(204)
    }
}

impl IntoResponse for u16 {
    fn into_response(self) -> CanonicalResponse {
        CanonicalResponse::new(self)
    }
}

impl<T: IntoResponse> IntoResponse for Result<T, ExtractError> {
    fn into_response(self) -> CanonicalResponse {
        match self {
            Ok(val) => val.into_response(),
            Err(e) => {
                let mut response = CanonicalResponse::new(400);
                response.set_body(e.to_string().into_bytes());
                response
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_path_param() {
        let mut params = FxHashMap::default();
        params.insert("id".to_string(), "42".to_string());

        let result = path_param(&params, "id").unwrap();
        assert_eq!(result.as_str(), "42");

        let id: u64 = path_param_parse(&params, "id").unwrap();
        assert_eq!(id, 42);
    }

    #[test]
    fn test_path_param_not_found() {
        let params = FxHashMap::default();
        let result = path_param(&params, "id");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_query() {
        let params = parse_query("name=hello&age=25&flag");
        assert_eq!(params.get("name").unwrap(), "hello");
        assert_eq!(params.get("age").unwrap(), "25");
        assert_eq!(params.get("flag").unwrap(), "");
    }

    #[test]
    fn test_url_decode() {
        assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
        assert_eq!(url_decode("%E4%BD%A0%E5%A5%BD").unwrap(), "你好");
        assert_eq!(url_decode("simple").unwrap(), "simple");
    }

    #[test]
    fn test_url_decode_unified_semantics() {
        // 统一语义(与 WAF 一致):+ 转空格
        assert_eq!(url_decode("a+b+c").unwrap(), "a b c");
        // 非法 % 序列按原样保留(%ZZ → %ZZ)
        assert_eq!(url_decode("test%ZZdata").unwrap(), "test%ZZdata");
        assert_eq!(url_decode("%").unwrap(), "%");
        assert_eq!(url_decode("%2").unwrap(), "%2");
        // 非法 UTF-8 返回 None(废除旧的"静默变空串"语义)
        assert!(url_decode("%FF").is_none());
        assert!(url_decode("%FF%FE").is_none());
    }

    #[test]
    fn test_parse_query_skips_invalid_utf8_param() {
        // 非法 UTF-8 的参数被跳过,不影响其他参数
        let params = parse_query("bad=%FF&good=ok");
        assert!(!params.contains_key("bad"));
        assert_eq!(params.get("good").unwrap(), "ok");
        // key 非法 UTF-8 同样跳过
        let params = parse_query("%FF=v&a=1");
        assert_eq!(params.len(), 1);
        assert_eq!(params.get("a").unwrap(), "1");
    }

    #[test]
    fn test_parse_query_plus_as_space() {
        // 统一语义:query 中 + 解码为空格(与 WAF 表单语义一致)
        let params = parse_query("name=John+Doe");
        assert_eq!(params.get("name").unwrap(), "John Doe");
    }

    #[test]
    fn test_query_param() {
        let mut request = CanonicalRequest::empty();
        let _ = request.set_query("key=value&num=42");

        let val = query_param(&request, "key").unwrap();
        assert_eq!(val, "value");

        let num: u32 = query_param_parse(&request, "num").unwrap();
        assert_eq!(num, 42);

        let default = query_param_or(&request, "missing", "default");
        assert_eq!(default, "default");
    }

    #[test]
    fn test_header_extract() {
        let mut request = CanonicalRequest::empty();
        request
            .add_header(b"x-custom", b"test-value")
            .unwrap();

        let val = header_value(&request, "x-custom").unwrap();
        assert_eq!(val, "test-value");

        let missing = header_value(&request, "x-missing");
        assert!(missing.is_none());
    }

    #[test]
    fn test_user_id() {
        let mut params = FxHashMap::default();
        params.insert("id".to_string(), "99".to_string());

        let user_id = UserId::from_params(&params).unwrap();
        assert_eq!(user_id.0, 99);
    }

    #[test]
    fn test_pagination() {
        let mut request = CanonicalRequest::empty();
        let _ = request.set_query("page=3&per_page=10");

        let pagination = Pagination::from_request(&request);
        assert_eq!(pagination.page, 3);
        assert_eq!(pagination.per_page, 10);
        assert_eq!(pagination.offset(), 20);
    }

    #[test]
    fn test_into_response_str() {
        let response = "Hello World".into_response();
        assert_eq!(response.status_code, 200);
        assert_eq!(response.body(), b"Hello World");
    }

    #[test]
    fn test_into_response_status() {
        let response: CanonicalResponse = 404u16.into_response();
        assert_eq!(response.status_code, 404);
    }

    #[test]
    fn test_into_response_result() {
        let ok_result: Result<&str, ExtractError> = Ok("success");
        let response = ok_result.into_response();
        assert_eq!(response.status_code, 200);

        let err_result: Result<&str, ExtractError> =
            Err(ExtractError::NotFound("test".to_string()));
        let response = err_result.into_response();
        assert_eq!(response.status_code, 400);
    }
}