1use parse_net::{DuplicatePolicy, Params, Word};
5
6use crate::error::{EncodingError, FormError, Location, SyntaxError};
7use crate::multipart::boundary::Boundary;
8
9pub(crate) const HEADER_LIMITS: parse_net::Limits = parse_net::Limits::DEFAULT
12 .max_params(16)
13 .duplicate_params(DuplicatePolicy::First);
14
15pub(crate) fn repeated_name<'a>(params: &Params<'a>) -> Option<&'a [u8]> {
19 for (i, p) in params.iter().enumerate() {
20 if params
21 .iter()
22 .take(i)
23 .any(|q| q.name.eq_ignore_ascii_case(p.name))
24 {
25 return Some(p.name);
26 }
27 }
28 None
29}
30
31pub(crate) fn has_empty_parameter(s: &[u8]) -> bool {
37 if s.iter().filter(|&&b| b == b';').count() < 2 {
40 return false;
41 }
42 let mut quoted = false;
43 let mut escaped = false;
44 let mut after_semicolon = false;
45 for &b in s {
46 if quoted {
47 if escaped {
48 escaped = false;
49 } else if b == b'\\' {
50 escaped = true;
51 } else if b == b'"' {
52 quoted = false;
53 }
54 continue;
55 }
56 match b {
57 b';' if after_semicolon => return true,
58 b';' => after_semicolon = true,
59 b' ' | b'\t' => {}
60 b'"' => {
61 quoted = true;
62 after_semicolon = false;
63 }
64 _ => after_semicolon = false,
65 }
66 }
67 false
68}
69
70pub(crate) fn holds_name_assignment(value: &[u8]) -> bool {
78 if !value.contains(&b'=') {
79 return false;
80 }
81 let at = |i: usize| {
82 let rest = value.get(i..).unwrap_or(&[]);
83 let starts = |p: &[u8]| {
84 rest.get(..p.len())
85 .is_some_and(|r| r.eq_ignore_ascii_case(p))
86 };
87 starts(b"name=") || starts(b"filename=")
88 };
89 if at(0) {
90 return true;
91 }
92 value
93 .iter()
94 .enumerate()
95 .any(|(i, &b)| matches!(b, b';' | b',' | b' ' | b'\t') && at(i.wrapping_add(1)))
96}
97
98pub(crate) const fn word_raw<'a>(w: &Word<'a>) -> &'a [u8] {
101 match w {
102 Word::Token(t) => t,
103 Word::Quoted(q) => q.raw(),
104 }
105}
106
107#[non_exhaustive]
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum FormKind {
126 Urlencoded,
128 Multipart(Boundary),
130}
131
132impl FormKind {
133 pub fn detect(content_type: &[u8]) -> Result<FormKind, FormError> {
159 if parse_net::text::trim_ows(content_type).is_empty() {
160 return Err(FormError::UnsupportedMediaType);
161 }
162 let mt = parse_net::media::parse(content_type, &HEADER_LIMITS).map_err(|e| {
163 FormError::syntax(
164 SyntaxError::InvalidContentType,
165 Location::at(e.offset() as u64),
166 )
167 })?;
168 let urlencoded = mt.is_form_urlencoded();
169 if !urlencoded && !mt.is_multipart_form_data() {
170 return Err(FormError::UnsupportedMediaType);
171 }
172 let params = mt.params();
173 if has_empty_parameter(content_type) {
174 return Err(FormError::syntax(
175 SyntaxError::InvalidContentType,
176 Location::at(0),
177 ));
178 }
179 if let Some(p) = content_type
182 .contains(&b'*')
183 .then(|| params.iter().find(|p| p.name.contains(&b'*')))
184 .flatten()
185 {
186 let reason = if p.name.len() >= 8
190 && p.name
191 .get(..8)
192 .is_some_and(|n| n.eq_ignore_ascii_case(b"boundary"))
193 {
194 SyntaxError::InvalidBoundary
195 } else {
196 SyntaxError::InvalidContentType
197 };
198 return Err(FormError::syntax(reason, Location::at(0)));
199 }
200 if let Some(name) = repeated_name(¶ms) {
201 let reason = if name.eq_ignore_ascii_case(b"boundary") {
202 SyntaxError::DuplicateBoundary
203 } else {
204 SyntaxError::InvalidContentType
205 };
206 return Err(FormError::syntax(reason, Location::at(0)));
207 }
208 if urlencoded {
209 if let Some(cs) = params.get("charset") {
210 if !cs.eq_ignore_ascii_case(b"utf-8") {
214 return Err(FormError::encoding(
215 EncodingError::UnsupportedCharset,
216 Location::at(0),
217 ));
218 }
219 }
220 return Ok(FormKind::Urlencoded);
221 }
222 let Some(word) = params.get("boundary") else {
223 return Err(FormError::syntax(
224 SyntaxError::MissingBoundary,
225 Location::at(0),
226 ));
227 };
228 if let Word::Quoted(q) = word {
229 if q.has_escapes() {
230 return Err(FormError::syntax(
231 SyntaxError::InvalidBoundary,
232 Location::at(0),
233 ));
234 }
235 }
236 Boundary::new(word_raw(&word)).map(FormKind::Multipart)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::error::Limit;
244
245 fn reason(ct: &[u8]) -> Option<SyntaxError> {
246 match FormKind::detect(ct) {
247 Err(FormError::Syntax { reason, .. }) => Some(reason),
248 _ => None,
249 }
250 }
251
252 #[test]
253 fn test_detect_urlencoded_cases() {
254 for ct in [
255 &b"application/x-www-form-urlencoded"[..],
256 b"APPLICATION/X-WWW-FORM-URLENCODED",
257 b"application/x-www-form-urlencoded; charset=UTF-8",
258 b"application/x-www-form-urlencoded; charset=\"utf-8\"",
259 b"application/x-www-form-urlencoded; charset=utf-8; other=1",
260 b" application/x-www-form-urlencoded ",
261 ] {
262 assert_eq!(FormKind::detect(ct), Ok(FormKind::Urlencoded), "{ct:?}");
263 }
264 assert!(matches!(
265 FormKind::detect(b"application/x-www-form-urlencoded; charset=iso-8859-1"),
266 Err(FormError::Encoding {
267 reason: EncodingError::UnsupportedCharset,
268 ..
269 })
270 ));
271 assert_eq!(
272 reason(b"application/x-www-form-urlencoded; charset=utf-8; Charset=utf-8"),
273 Some(SyntaxError::InvalidContentType)
274 );
275 }
276
277 #[test]
278 fn test_detect_multipart_boundary_rules() {
279 assert!(matches!(
280 FormKind::detect(b"multipart/form-data; boundary=x"),
281 Ok(FormKind::Multipart(_))
282 ));
283 assert!(matches!(
284 FormKind::detect(b"Multipart/Form-Data; Boundary=\"a b\""),
285 Ok(FormKind::Multipart(b)) if b.as_bytes() == b"a b"
286 ));
287 assert_eq!(
288 reason(b"multipart/form-data"),
289 Some(SyntaxError::MissingBoundary)
290 );
291 assert_eq!(
292 reason(b"multipart/form-data; boundary=a; boundary=a"),
293 Some(SyntaxError::DuplicateBoundary)
294 );
295 assert_eq!(
296 reason(b"multipart/form-data; boundary=\"\""),
297 Some(SyntaxError::InvalidBoundary)
298 );
299 assert_eq!(
300 reason(b"multipart/form-data; boundary=\"ab \""),
301 Some(SyntaxError::InvalidBoundary)
302 );
303 assert_eq!(
304 reason(b"multipart/form-data; boundary=\"a\\b\""),
305 Some(SyntaxError::InvalidBoundary)
306 );
307 assert_eq!(
308 reason(b"multipart/form-data; boundary="),
309 Some(SyntaxError::InvalidContentType)
310 );
311 assert_eq!(
312 reason(b"multipart/form-data; boundary=a; x=1; X=2"),
313 Some(SyntaxError::InvalidContentType)
314 );
315 assert_eq!(
316 reason(b"multipart/form-data; boundary=x junk"),
317 Some(SyntaxError::InvalidContentType)
318 );
319 }
320
321 #[test]
322 fn test_detect_unsupported_types() {
323 for ct in [
324 &b""[..],
325 b" ",
326 b"text/plain",
327 b"multipart/mixed; boundary=x",
328 b"application/json",
329 ] {
330 assert_eq!(
331 FormKind::detect(ct),
332 Err(FormError::UnsupportedMediaType),
333 "{ct:?}"
334 );
335 }
336 }
337
338 #[test]
339 fn test_detect_too_many_parameters_refused() {
340 let mut ct = alloc::vec::Vec::from(&b"multipart/form-data; boundary=x"[..]);
341 for i in 0..16 {
342 ct.extend_from_slice(alloc::format!("; p{i}=1").as_bytes());
343 }
344 assert_eq!(reason(&ct), Some(SyntaxError::InvalidContentType));
345 let _ = Limit::Body;
346 }
347}