sa_token_adapter/
utils.rs

1// Author: 金书记
2//
3// 中文 | English
4// 通用工具函数模块 | Common utility functions module
5//
6// 代码流程逻辑 | Code Flow Logic
7// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8//
9// 本模块提供了框架适配器的通用工具函数,简化了新框架集成的复杂度。
10// This module provides common utility functions for framework adapters,
11// simplifying the complexity of integrating new frameworks.
12//
13// 核心功能 | Core Features:
14// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15//
16// 1. Cookie 解析
17//    - parse_cookies(): 解析 HTTP Cookie 头
18//    - 支持多个 cookie 对,自动处理空格
19//
20// 2. 查询字符串解析
21//    - parse_query_string(): 解析 URL 查询参数
22//    - 自动 URL 解码,支持特殊字符
23//
24// 3. Cookie 构建
25//    - build_cookie_string(): 从 CookieOptions 构建完整的 Set-Cookie 字符串
26//    - 支持所有标准 cookie 属性(Domain、Path、Max-Age、Secure、HttpOnly、SameSite)
27//
28// 4. Header 解析
29//    - extract_bearer_token(): 从 Authorization 头提取 Bearer token
30//
31// 使用场景 | Use Cases:
32// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33//
34// 新框架适配器实现时,只需调用这些工具函数,无需重复编写解析逻辑:
35// When implementing a new framework adapter, just call these utility functions
36// without rewriting parsing logic:
37//
38// ```rust
39// impl SaRequest for MyFrameworkRequest {
40//     fn get_cookie(&self, name: &str) -> Option<String> {
41//         self.get_header("cookie")
42//             .and_then(|cookies| parse_cookies(&cookies).get(name).cloned())
43//     }
44// }
45// ```
46//
47// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48
49use std::collections::HashMap;
50use crate::context::CookieOptions;
51
52/// 中文 | English
53/// 解析 Cookie 字符串为键值对映射 | Parse Cookie string into key-value map
54///
55/// # 参数 | Parameters
56/// - `cookie_header`: Cookie 头字符串,格式如 "key1=value1; key2=value2"
57///
58/// # 返回 | Returns
59/// - `HashMap<String, String>`: Cookie 名称到值的映射
60///
61/// # 示例 | Example
62/// ```
63/// use sa_token_adapter::utils::parse_cookies;
64///
65/// let cookies = parse_cookies("session=abc123; user=john");
66/// assert_eq!(cookies.get("session"), Some(&"abc123".to_string()));
67/// ```
68pub fn parse_cookies(cookie_header: &str) -> HashMap<String, String> {
69    let mut cookies = HashMap::new();
70    for pair in cookie_header.split(';') {
71        let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
72        if parts.len() == 2 {
73            cookies.insert(parts[0].to_string(), parts[1].to_string());
74        }
75    }
76    cookies
77}
78
79/// 中文 | English
80/// 解析 URL 查询字符串为键值对映射 | Parse URL query string into key-value map
81///
82/// # 参数 | Parameters
83/// - `query`: 查询字符串,格式如 "key1=value1&key2=value2"
84///
85/// # 返回 | Returns
86/// - `HashMap<String, String>`: 参数名称到值的映射(自动 URL 解码)
87///
88/// # 示例 | Example
89/// ```
90/// use sa_token_adapter::utils::parse_query_string;
91///
92/// let params = parse_query_string("name=John%20Doe&age=30");
93/// assert_eq!(params.get("name"), Some(&"John Doe".to_string()));
94/// ```
95pub fn parse_query_string(query: &str) -> HashMap<String, String> {
96    let mut params = HashMap::new();
97    for pair in query.split('&') {
98        let parts: Vec<&str> = pair.splitn(2, '=').collect();
99        if parts.len() == 2 {
100            params.insert(
101                urlencoding::decode(parts[0]).unwrap_or_default().to_string(),
102                urlencoding::decode(parts[1]).unwrap_or_default().to_string(),
103            );
104        }
105    }
106    params
107}
108
109/// 中文 | English
110/// 从 CookieOptions 构建完整的 Set-Cookie 字符串 | Build complete Set-Cookie string from CookieOptions
111///
112/// # 参数 | Parameters
113/// - `name`: Cookie 名称 | Cookie name
114/// - `value`: Cookie 值 | Cookie value
115/// - `options`: Cookie 选项(Domain、Path、Max-Age 等)| Cookie options
116///
117/// # 返回 | Returns
118/// - `String`: 完整的 Set-Cookie 头值
119///
120/// # 示例 | Example
121/// ```
122/// use sa_token_adapter::{utils::build_cookie_string, CookieOptions, SameSite};
123///
124/// let cookie = build_cookie_string("session", "abc123", CookieOptions {
125///     domain: Some("example.com".to_string()),
126///     path: Some("/".to_string()),
127///     max_age: Some(3600),
128///     http_only: true,
129///     secure: true,
130///     same_site: Some(SameSite::Strict),
131/// });
132/// // 结果: "session=abc123; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Strict"
133/// ```
134pub fn build_cookie_string(name: &str, value: &str, options: CookieOptions) -> String {
135    let mut cookie = format!("{}={}", name, value);
136    
137    if let Some(domain) = options.domain {
138        cookie.push_str(&format!("; Domain={}", domain));
139    }
140    if let Some(path) = options.path {
141        cookie.push_str(&format!("; Path={}", path));
142    }
143    if let Some(max_age) = options.max_age {
144        cookie.push_str(&format!("; Max-Age={}", max_age));
145    }
146    if options.http_only {
147        cookie.push_str("; HttpOnly");
148    }
149    if options.secure {
150        cookie.push_str("; Secure");
151    }
152    if let Some(same_site) = options.same_site {
153        cookie.push_str(&format!("; SameSite={}", same_site));
154    }
155    
156    cookie
157}
158
159/// 中文 | English
160/// 从 Authorization 头提取 Bearer token | Extract Bearer token from Authorization header
161///
162/// # 参数 | Parameters
163/// - `auth_header`: Authorization 头值,格式如 "Bearer <token>"
164///
165/// # 返回 | Returns
166/// - `Option<String>`: 提取的 token,如果格式不正确则返回 None
167///
168/// # 示例 | Example
169/// ```
170/// use sa_token_adapter::utils::extract_bearer_token;
171///
172/// let token = extract_bearer_token("Bearer abc123xyz");
173/// assert_eq!(token, Some("abc123xyz".to_string()));
174///
175/// let invalid = extract_bearer_token("Basic xyz");
176/// assert_eq!(invalid, None);
177/// ```
178pub fn extract_bearer_token(auth_header: &str) -> Option<String> {
179    auth_header
180        .strip_prefix("Bearer ")
181        .map(|token| token.trim().to_string())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_parse_cookies() {
190        let cookies = parse_cookies("session=abc123; user=john; theme=dark");
191        assert_eq!(cookies.get("session"), Some(&"abc123".to_string()));
192        assert_eq!(cookies.get("user"), Some(&"john".to_string()));
193        assert_eq!(cookies.get("theme"), Some(&"dark".to_string()));
194    }
195
196    #[test]
197    fn test_parse_query_string() {
198        let params = parse_query_string("name=John%20Doe&age=30&city=New%20York");
199        assert_eq!(params.get("name"), Some(&"John Doe".to_string()));
200        assert_eq!(params.get("age"), Some(&"30".to_string()));
201        assert_eq!(params.get("city"), Some(&"New York".to_string()));
202    }
203
204    #[test]
205    fn test_build_cookie_string() {
206        use crate::context::SameSite;
207        
208        let cookie = build_cookie_string("session", "abc123", CookieOptions {
209            domain: Some("example.com".to_string()),
210            path: Some("/".to_string()),
211            max_age: Some(3600),
212            http_only: true,
213            secure: true,
214            same_site: Some(SameSite::Strict),
215        });
216        
217        assert!(cookie.contains("session=abc123"));
218        assert!(cookie.contains("Domain=example.com"));
219        assert!(cookie.contains("Path=/"));
220        assert!(cookie.contains("Max-Age=3600"));
221        assert!(cookie.contains("HttpOnly"));
222        assert!(cookie.contains("Secure"));
223        assert!(cookie.contains("SameSite=Strict"));
224    }
225
226    #[test]
227    fn test_extract_bearer_token() {
228        assert_eq!(
229            extract_bearer_token("Bearer abc123xyz"),
230            Some("abc123xyz".to_string())
231        );
232        assert_eq!(extract_bearer_token("Bearer  token_with_spaces  "), Some("token_with_spaces".to_string()));
233        assert_eq!(extract_bearer_token("Basic xyz"), None);
234        assert_eq!(extract_bearer_token("Bearer"), None);
235    }
236}
237