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 {
75 value.contains(&b'=') && value.windows(5).any(|w| w.eq_ignore_ascii_case(b"name="))
76}
77
78pub(crate) const fn word_raw<'a>(w: &Word<'a>) -> &'a [u8] {
81 match w {
82 Word::Token(t) => t,
83 Word::Quoted(q) => q.raw(),
84 }
85}
86
87#[non_exhaustive]
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum FormKind {
106 Urlencoded,
108 Multipart(Boundary),
110}
111
112impl FormKind {
113 pub fn detect(content_type: &[u8]) -> Result<FormKind, FormError> {
139 if parse_net::text::trim_ows(content_type).is_empty() {
140 return Err(FormError::UnsupportedMediaType);
141 }
142 let mt = parse_net::media::parse(content_type, &HEADER_LIMITS).map_err(|e| {
143 FormError::syntax(
144 SyntaxError::InvalidContentType,
145 Location::at(e.offset() as u64),
146 )
147 })?;
148 let urlencoded = mt.is_form_urlencoded();
149 if !urlencoded && !mt.is_multipart_form_data() {
150 return Err(FormError::UnsupportedMediaType);
151 }
152 let params = mt.params();
153 if has_empty_parameter(content_type) {
154 return Err(FormError::syntax(
155 SyntaxError::InvalidContentType,
156 Location::at(0),
157 ));
158 }
159 if let Some(p) = content_type
162 .contains(&b'*')
163 .then(|| params.iter().find(|p| p.name.contains(&b'*')))
164 .flatten()
165 {
166 let reason = if p.name.len() >= 8
170 && p.name
171 .get(..8)
172 .is_some_and(|n| n.eq_ignore_ascii_case(b"boundary"))
173 {
174 SyntaxError::InvalidBoundary
175 } else {
176 SyntaxError::InvalidContentType
177 };
178 return Err(FormError::syntax(reason, Location::at(0)));
179 }
180 if let Some(name) = repeated_name(¶ms) {
181 let reason = if name.eq_ignore_ascii_case(b"boundary") {
182 SyntaxError::DuplicateBoundary
183 } else {
184 SyntaxError::InvalidContentType
185 };
186 return Err(FormError::syntax(reason, Location::at(0)));
187 }
188 if urlencoded {
189 if let Some(cs) = params.get("charset") {
190 if !(cs.eq_ignore_ascii_case(b"utf-8") || cs.eq_ignore_ascii_case(b"us-ascii")) {
191 return Err(FormError::encoding(
192 EncodingError::UnsupportedCharset,
193 Location::at(0),
194 ));
195 }
196 }
197 return Ok(FormKind::Urlencoded);
198 }
199 let Some(word) = params.get("boundary") else {
200 return Err(FormError::syntax(
201 SyntaxError::MissingBoundary,
202 Location::at(0),
203 ));
204 };
205 if let Word::Quoted(q) = word {
206 if q.has_escapes() {
207 return Err(FormError::syntax(
208 SyntaxError::InvalidBoundary,
209 Location::at(0),
210 ));
211 }
212 }
213 Boundary::new(word_raw(&word)).map(FormKind::Multipart)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::error::Limit;
221
222 fn reason(ct: &[u8]) -> Option<SyntaxError> {
223 match FormKind::detect(ct) {
224 Err(FormError::Syntax { reason, .. }) => Some(reason),
225 _ => None,
226 }
227 }
228
229 #[test]
230 fn test_detect_urlencoded_cases() {
231 for ct in [
232 &b"application/x-www-form-urlencoded"[..],
233 b"APPLICATION/X-WWW-FORM-URLENCODED",
234 b"application/x-www-form-urlencoded; charset=UTF-8",
235 b"application/x-www-form-urlencoded; charset=\"utf-8\"",
236 b"application/x-www-form-urlencoded; charset=us-ascii; other=1",
237 b" application/x-www-form-urlencoded ",
238 ] {
239 assert_eq!(FormKind::detect(ct), Ok(FormKind::Urlencoded), "{ct:?}");
240 }
241 assert!(matches!(
242 FormKind::detect(b"application/x-www-form-urlencoded; charset=iso-8859-1"),
243 Err(FormError::Encoding {
244 reason: EncodingError::UnsupportedCharset,
245 ..
246 })
247 ));
248 assert_eq!(
249 reason(b"application/x-www-form-urlencoded; charset=utf-8; Charset=utf-8"),
250 Some(SyntaxError::InvalidContentType)
251 );
252 }
253
254 #[test]
255 fn test_detect_multipart_boundary_rules() {
256 assert!(matches!(
257 FormKind::detect(b"multipart/form-data; boundary=x"),
258 Ok(FormKind::Multipart(_))
259 ));
260 assert!(matches!(
261 FormKind::detect(b"Multipart/Form-Data; Boundary=\"a b\""),
262 Ok(FormKind::Multipart(b)) if b.as_bytes() == b"a b"
263 ));
264 assert_eq!(
265 reason(b"multipart/form-data"),
266 Some(SyntaxError::MissingBoundary)
267 );
268 assert_eq!(
269 reason(b"multipart/form-data; boundary=a; boundary=a"),
270 Some(SyntaxError::DuplicateBoundary)
271 );
272 assert_eq!(
273 reason(b"multipart/form-data; boundary=\"\""),
274 Some(SyntaxError::InvalidBoundary)
275 );
276 assert_eq!(
277 reason(b"multipart/form-data; boundary=\"ab \""),
278 Some(SyntaxError::InvalidBoundary)
279 );
280 assert_eq!(
281 reason(b"multipart/form-data; boundary=\"a\\b\""),
282 Some(SyntaxError::InvalidBoundary)
283 );
284 assert_eq!(
285 reason(b"multipart/form-data; boundary="),
286 Some(SyntaxError::InvalidContentType)
287 );
288 assert_eq!(
289 reason(b"multipart/form-data; boundary=a; x=1; X=2"),
290 Some(SyntaxError::InvalidContentType)
291 );
292 assert_eq!(
293 reason(b"multipart/form-data; boundary=x junk"),
294 Some(SyntaxError::InvalidContentType)
295 );
296 }
297
298 #[test]
299 fn test_detect_unsupported_types() {
300 for ct in [
301 &b""[..],
302 b" ",
303 b"text/plain",
304 b"multipart/mixed; boundary=x",
305 b"application/json",
306 ] {
307 assert_eq!(
308 FormKind::detect(ct),
309 Err(FormError::UnsupportedMediaType),
310 "{ct:?}"
311 );
312 }
313 }
314
315 #[test]
316 fn test_detect_too_many_parameters_refused() {
317 let mut ct = alloc::vec::Vec::from(&b"multipart/form-data; boundary=x"[..]);
318 for i in 0..16 {
319 ct.extend_from_slice(alloc::format!("; p{i}=1").as_bytes());
320 }
321 assert_eq!(reason(&ct), Some(SyntaxError::InvalidContentType));
322 let _ = Limit::Body;
323 }
324}