multer/
constants.rs

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    /// Extract ContentDisposition Attribute from header.
36    ///
37    /// Some older clients may not quote the name or filename, so we allow them,
38    /// but require them to be percent encoded. Only allocates if percent
39    /// decoding, and there are characters that need to be decoded.
40    pub fn extract_from<'h>(&self, mut header: &'h [u8]) -> Option<Cow<'h, str>> {
41        // TODO: The prefix should be matched case-insensitively.
42        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            // Check if we found a superstring of `prefix`; continue if so.
49            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            // Now find and trim the `=`. Handle quoted strings first.
56            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        // These are technically malformed, as RFC 7578 says the `name`
136        // parameter _must_ be included. But okay.
137        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}