fast_down/utils/
content_pisposition.rs1use 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 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 let key = Self::read_key(&mut chars);
22 if key.is_empty() {
23 if let Some(';') = chars.peek() {
25 chars.next();
26 continue;
27 } else {
28 break;
29 }
30 }
31 match chars.peek() {
33 Some('=') => {
34 chars.next(); }
36 Some(';') => {
37 chars.next();
40 continue;
41 }
42 None => break, _ => {
44 Self::skip_until(&mut chars, ';');
46 continue;
47 }
48 }
49 Self::consume_whitespace(&mut chars);
50 let value = if let Some('"') = chars.peek() {
52 chars.next(); Self::read_quoted_string(&mut chars)
54 } else {
55 Self::read_token(&mut chars)
56 };
57 Self::consume_whitespace(&mut chars);
60 if let Some(';') = chars.peek() {
61 chars.next();
62 }
63 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 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 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*=""#; let cd = ContentDisposition::parse(s);
201 assert_eq!(cd.filename.unwrap(), ";\";;");
202 }
203}