Skip to main content

fast_down/utils/
content_pisposition.rs

1use std::{iter::Peekable, str::Chars};
2
3#[derive(Debug, PartialEq)]
4pub struct ContentDisposition {
5    pub filename: Option<String>,
6}
7impl ContentDisposition {
8    pub fn parse(header_value: &str) -> Self {
9        let mut filename = None;
10        let mut filename_star = None;
11        // 1. 跳过 disposition-type (如 "attachment")
12        // 如果没有分号,说明没有参数,直接返回
13        let rest = match header_value.find(';') {
14            Some(idx) => &header_value[idx + 1..],
15            None => return Self { filename: None },
16        };
17        let mut chars = rest.chars().peekable();
18        while chars.peek().is_some() {
19            Self::consume_whitespace(&mut chars);
20            // 读取 Key
21            let key = Self::read_key(&mut chars);
22            if key.is_empty() {
23                // 处理连续分号情况 (e.g. ";;")
24                if let Some(';') = chars.peek() {
25                    chars.next();
26                    continue;
27                } else {
28                    break;
29                }
30            }
31            // 检查 Key 后的分隔符
32            match chars.peek() {
33                Some('=') => {
34                    chars.next(); // 消费 '='
35                }
36                Some(';') => {
37                    // Key 后面直接跟分号,说明是 Flag 参数 (如 "hidden;")
38                    // 忽略此参数,直接消费分号并进入下一次循环
39                    chars.next();
40                    continue;
41                }
42                None => break, // 字符串结束
43                _ => {
44                    // 遇到非法字符,尝试跳到下一个分号恢复
45                    Self::skip_until(&mut chars, ';');
46                    continue;
47                }
48            }
49            Self::consume_whitespace(&mut chars);
50            // 读取 Value
51            let value = if let Some('"') = chars.peek() {
52                chars.next(); // 消费起始引号
53                Self::read_quoted_string(&mut chars)
54            } else {
55                Self::read_token(&mut chars)
56            };
57            // 如果 Value 后面紧跟分号,消费它
58            // 注意:如果 read_token 因为遇到空格停止,这里需要跳过可能的空格找到分号
59            Self::consume_whitespace(&mut chars);
60            if let Some(';') = chars.peek() {
61                chars.next();
62            }
63            // 匹配 Key
64            if key.eq_ignore_ascii_case("filename") {
65                filename = Some(value);
66            } else if key.eq_ignore_ascii_case("filename*") {
67                filename_star = Self::parse_filename_star(&value);
68            }
69        }
70        Self {
71            filename: filename_star.or(filename),
72        }
73    }
74
75    fn consume_whitespace(chars: &mut Peekable<Chars>) {
76        while let Some(c) = chars.peek()
77            && c.is_whitespace()
78        {
79            chars.next();
80        }
81    }
82
83    /// 读取 Key,遇到 `=` 或 `;` 停止
84    fn read_key(chars: &mut Peekable<Chars>) -> String {
85        let mut s = String::new();
86        while let Some(&c) = chars.peek()
87            && c != '='
88            && c != ';'
89        {
90            s.push(c);
91            chars.next();
92        }
93        s.trim().to_string()
94    }
95
96    /// 读取未加引号的 Token (Value)
97    /// 停止条件:遇到 `;` 或者 **空白字符**
98    fn read_token(chars: &mut Peekable<Chars>) -> String {
99        let mut s = String::new();
100        while let Some(&c) = chars.peek()
101            && c != ';'
102            && !c.is_whitespace()
103        {
104            s.push(c);
105            chars.next();
106        }
107        s
108    }
109
110    fn read_quoted_string(chars: &mut Peekable<Chars>) -> String {
111        let mut s = String::new();
112        while let Some(c) = chars.next() {
113            match c {
114                '"' => break,
115                '\\' => {
116                    if let Some(escaped) = chars.next() {
117                        s.push(escaped);
118                    }
119                }
120                _ => s.push(c),
121            }
122        }
123        s
124    }
125
126    fn skip_until(chars: &mut Peekable<Chars>, target: char) {
127        for c in chars.by_ref() {
128            if c == target {
129                break;
130            }
131        }
132    }
133
134    fn parse_filename_star(val: &str) -> Option<String> {
135        let mut parts = val.splitn(3, '\'');
136        let charset = parts.next()?;
137        parts.next()?;
138        let encoded_text = parts.next()?;
139        if charset.eq_ignore_ascii_case("UTF-8") {
140            Self::percent_decode(encoded_text)
141        } else {
142            None
143        }
144    }
145
146    fn percent_decode(s: &str) -> Option<String> {
147        let mut bytes = Vec::with_capacity(s.len());
148        let mut chars = s.chars();
149        while let Some(c) = chars.next() {
150            if c == '%' {
151                let h = chars.next()?.to_digit(16)?;
152                let l = chars.next()?.to_digit(16)?;
153                bytes.push(((h as u8) << 4) | (l as u8));
154            } else {
155                bytes.push(c as u8);
156            }
157        }
158        String::from_utf8(bytes).ok()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_multiple_params_no_semicolon() {
168        let s = r#"attachment; filename=foo.txt size=10"#;
169        let cd = ContentDisposition::parse(s);
170        assert_eq!(cd.filename.unwrap(), "foo.txt");
171    }
172
173    #[test]
174    fn test_quoted_with_spaces() {
175        let s = r#"attachment; filename="foo\" bar.txt"; size=10"#;
176        let cd = ContentDisposition::parse(s);
177        assert_eq!(cd.filename.unwrap(), "foo\" bar.txt");
178    }
179
180    #[test]
181    fn test_flag_parameter() {
182        let s = r#"attachment; hidden; filename="test.txt""#;
183        let cd = ContentDisposition::parse(s);
184        assert_eq!(cd.filename.unwrap(), "test.txt");
185    }
186
187    #[test]
188    fn test_complex_filename_star() {
189        let s = r#"attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt"#;
190        let cd = ContentDisposition::parse(s);
191        assert_eq!(cd.filename.unwrap(), "测试.txt");
192        let s = r#"attachment; filename=";;;"; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt"#;
193        let cd = ContentDisposition::parse(s);
194        assert_eq!(cd.filename.unwrap(), "测试.txt");
195    }
196
197    #[test]
198    fn test_empty_values() {
199        let s = r#"attachment; filename=";\";;"; filename*=""#; // filename* 格式不对会被忽略
200        let cd = ContentDisposition::parse(s);
201        assert_eq!(cd.filename.unwrap(), ";\";;");
202    }
203}