1use std::borrow::Cow;
2
3pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX;
4pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: u64 = std::u64::MAX;
5
6pub(crate) const MAX_HEADERS: usize = 32;
7pub(crate) const BOUNDARY_EXT: &str = "--";
8pub(crate) const CR: &str = "\r";
9#[allow(dead_code)]
10pub(crate) const LF: &str = "\n";
11pub(crate) const CRLF: &str = "\r\n";
12pub(crate) const CRLF_CRLF: &str = "\r\n\r\n";
13
14#[derive(PartialEq)]
15pub(crate) enum ContentDispositionAttr {
16 Name,
17 FileName,
18}
19
20fn trim_ascii_ws_start(bytes: &[u8]) -> &[u8] {
21 bytes
22 .iter()
23 .position(|b| !b.is_ascii_whitespace())
24 .map_or_else(|| &bytes[bytes.len()..], |i| &bytes[i..])
25}
26
27fn trim_ascii_ws_then(bytes: &[u8], char: u8) -> Option<&[u8]> {
28 match trim_ascii_ws_start(bytes) {
29 [first, rest @ ..] if *first == char => Some(rest),
30 _ => None,
31 }
32}
33
34impl ContentDispositionAttr {
35 pub fn extract_from<'h>(&self, mut header: &'h [u8]) -> Option<Cow<'h, str>> {
41 let prefix = match self {
43 ContentDispositionAttr::Name => &b"name"[..],
44 ContentDispositionAttr::FileName => &b"filename"[..],
45 };
46
47 while let Some(i) = memchr::memmem::find(header, prefix) {
48 let suffix = &header[(i + prefix.len())..];
50 if i > 0 && !(header[i - 1].is_ascii_whitespace() || header[i - 1] == b';') {
51 header = suffix;
52 continue;
53 }
54
55 let rest = trim_ascii_ws_then(suffix, b'=')?;
57 let (bytes, is_escaped) = if let Some(rest) = trim_ascii_ws_then(rest, b'"') {
58 let (mut k, mut escaped) = (memchr::memchr(b'"', rest)?, false);
59 while k > 0 && rest[k - 1] == b'\\' {
60 escaped = true;
61 k = k + 1 + memchr::memchr(b'"', &rest[(k + 1)..])?;
62 }
63
64 (&rest[..k], escaped)
65 } else {
66 let rest = trim_ascii_ws_start(rest);
67 let j = memchr::memchr2(b';', b' ', rest).unwrap_or(rest.len());
68 (&rest[..j], false)
69 };
70
71 return match std::str::from_utf8(bytes).ok()? {
72 name if is_escaped => Some(name.replace(r#"\""#, "\"").into()),
73 name => Some(name.into()),
74 };
75 }
76
77 None
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_content_disposition_name_only() {
87 let val = br#"form-data; name="my_field""#;
88 let name = ContentDispositionAttr::Name.extract_from(val);
89 let filename = ContentDispositionAttr::FileName.extract_from(val);
90 assert_eq!(name.unwrap(), "my_field");
91 assert!(filename.is_none());
92
93 let val = br#"form-data; name=my_field "#;
94 let name = ContentDispositionAttr::Name.extract_from(val);
95 let filename = ContentDispositionAttr::FileName.extract_from(val);
96 assert_eq!(name.unwrap(), "my_field");
97 assert!(filename.is_none());
98
99 let val = br#"form-data; name = my_field "#;
100 let name = ContentDispositionAttr::Name.extract_from(val);
101 let filename = ContentDispositionAttr::FileName.extract_from(val);
102 assert_eq!(name.unwrap(), "my_field");
103 assert!(filename.is_none());
104
105 let val = br#"form-data; name = "#;
106 let name = ContentDispositionAttr::Name.extract_from(val);
107 let filename = ContentDispositionAttr::FileName.extract_from(val);
108 assert_eq!(name.unwrap(), "");
109 assert!(filename.is_none());
110 }
111
112 #[test]
113 fn test_content_disposition_extraction() {
114 let val = br#"form-data; name="my_field"; filename="file abc.txt""#;
115 let name = ContentDispositionAttr::Name.extract_from(val);
116 let filename = ContentDispositionAttr::FileName.extract_from(val);
117 assert_eq!(name.unwrap(), "my_field");
118 assert_eq!(filename.unwrap(), "file abc.txt");
119
120 let val = "form-data; name=\"你好\"; filename=\"file abc.txt\"".as_bytes();
121 let name = ContentDispositionAttr::Name.extract_from(val);
122 let filename = ContentDispositionAttr::FileName.extract_from(val);
123 assert_eq!(name.unwrap(), "你好");
124 assert_eq!(filename.unwrap(), "file abc.txt");
125
126 let val = "form-data; name=\"কখগ\"; filename=\"你好.txt\"".as_bytes();
127 let name = ContentDispositionAttr::Name.extract_from(val);
128 let filename = ContentDispositionAttr::FileName.extract_from(val);
129 assert_eq!(name.unwrap(), "কখগ");
130 assert_eq!(filename.unwrap(), "你好.txt");
131 }
132
133 #[test]
134 fn test_content_disposition_file_name_only() {
135 let val = br#"form-data; filename="file-name.txt""#;
138 let name = ContentDispositionAttr::Name.extract_from(val);
139 let filename = ContentDispositionAttr::FileName.extract_from(val);
140 assert_eq!(filename.unwrap(), "file-name.txt");
141 assert!(name.is_none());
142
143 let val = "form-data; filename=\"কখগ-你好.txt\"".as_bytes();
144 let name = ContentDispositionAttr::Name.extract_from(val);
145 let filename = ContentDispositionAttr::FileName.extract_from(val);
146 assert_eq!(filename.unwrap(), "কখগ-你好.txt");
147 assert!(name.is_none());
148 }
149
150 #[test]
151 fn test_content_distribution_misordered_fields() {
152 let val = br#"form-data; filename=file-name.txt; name=file"#;
153 let name = ContentDispositionAttr::Name.extract_from(val);
154 let filename = ContentDispositionAttr::FileName.extract_from(val);
155 assert_eq!(filename.unwrap(), "file-name.txt");
156 assert_eq!(name.unwrap(), "file");
157
158 let val = br#"form-data; filename="file-name.txt"; name="file""#;
159 let name = ContentDispositionAttr::Name.extract_from(val);
160 let filename = ContentDispositionAttr::FileName.extract_from(val);
161 assert_eq!(filename.unwrap(), "file-name.txt");
162 assert_eq!(name.unwrap(), "file");
163
164 let val = "form-data; filename=\"你好.txt\"; name=\"কখগ\"".as_bytes();
165 let name = ContentDispositionAttr::Name.extract_from(val);
166 let filename = ContentDispositionAttr::FileName.extract_from(val);
167 assert_eq!(name.unwrap(), "কখগ");
168 assert_eq!(filename.unwrap(), "你好.txt");
169 }
170
171 #[test]
172 fn test_content_disposition_name_unquoted() {
173 let val = br#"form-data; name=my_field"#;
174 let name = ContentDispositionAttr::Name.extract_from(val);
175 let filename = ContentDispositionAttr::FileName.extract_from(val);
176 assert_eq!(name.unwrap(), "my_field");
177 assert!(filename.is_none());
178
179 let val = br#"form-data; name=my_field; filename=file-name.txt"#;
180 let name = ContentDispositionAttr::Name.extract_from(val);
181 let filename = ContentDispositionAttr::FileName.extract_from(val);
182 assert_eq!(name.unwrap(), "my_field");
183 assert_eq!(filename.unwrap(), "file-name.txt");
184 }
185
186 #[test]
187 fn test_content_disposition_name_quoted() {
188 let val = br#"form-data; name="my;f;ield""#;
189 let name = ContentDispositionAttr::Name.extract_from(val);
190 let filename = ContentDispositionAttr::FileName.extract_from(val);
191 assert_eq!(name.unwrap(), "my;f;ield");
192 assert!(filename.is_none());
193
194 let val = br#"form-data; name=my_field; filename = "file;name.txt""#;
195 let name = ContentDispositionAttr::Name.extract_from(val);
196 assert_eq!(name.unwrap(), "my_field");
197 let filename = ContentDispositionAttr::FileName.extract_from(val);
198 assert_eq!(filename.unwrap(), "file;name.txt");
199
200 let val = br#"form-data; name=; filename=filename.txt"#;
201 let name = ContentDispositionAttr::Name.extract_from(val);
202 let filename = ContentDispositionAttr::FileName.extract_from(val);
203 assert_eq!(name.unwrap(), "");
204 assert_eq!(filename.unwrap(), "filename.txt");
205
206 let val = br#"form-data; name=";"; filename=";""#;
207 let name = ContentDispositionAttr::Name.extract_from(val);
208 let filename = ContentDispositionAttr::FileName.extract_from(val);
209 assert_eq!(name.unwrap(), ";");
210 assert_eq!(filename.unwrap(), ";");
211 }
212
213 #[test]
214 fn test_content_disposition_name_escaped_quote() {
215 let val = br#"form-data; name="my\"field\"name""#;
216 let name = ContentDispositionAttr::Name.extract_from(val);
217 assert_eq!(name.unwrap(), r#"my"field"name"#);
218
219 let val = br#"form-data; name="myfield\"name""#;
220 let name = ContentDispositionAttr::Name.extract_from(val);
221 assert_eq!(name.unwrap(), r#"myfield"name"#);
222 }
223}