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) const fn word_raw<'a>(w: &Word<'a>) -> &'a [u8] {
34 match w {
35 Word::Token(t) => t,
36 Word::Quoted(q) => q.raw(),
37 }
38}
39
40#[non_exhaustive]
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum FormKind {
59 Urlencoded,
61 Multipart(Boundary),
63}
64
65impl FormKind {
66 pub fn detect(content_type: &[u8]) -> Result<FormKind, FormError> {
91 if parse_net::text::trim_ows(content_type).is_empty() {
92 return Err(FormError::UnsupportedMediaType);
93 }
94 let mt = parse_net::media::parse(content_type, &HEADER_LIMITS).map_err(|e| {
95 FormError::syntax(
96 SyntaxError::InvalidContentType,
97 Location::at(e.offset() as u64),
98 )
99 })?;
100 let urlencoded = mt.is_form_urlencoded();
101 if !urlencoded && !mt.is_multipart_form_data() {
102 return Err(FormError::UnsupportedMediaType);
103 }
104 let params = mt.params();
105 if let Some(name) = repeated_name(¶ms) {
106 let reason = if name.eq_ignore_ascii_case(b"boundary") {
107 SyntaxError::DuplicateBoundary
108 } else {
109 SyntaxError::InvalidContentType
110 };
111 return Err(FormError::syntax(reason, Location::at(0)));
112 }
113 if urlencoded {
114 if let Some(cs) = params.get("charset") {
115 if !(cs.eq_ignore_ascii_case(b"utf-8") || cs.eq_ignore_ascii_case(b"us-ascii")) {
116 return Err(FormError::encoding(
117 EncodingError::UnsupportedCharset,
118 Location::at(0),
119 ));
120 }
121 }
122 return Ok(FormKind::Urlencoded);
123 }
124 let Some(word) = params.get("boundary") else {
125 return Err(FormError::syntax(
126 SyntaxError::MissingBoundary,
127 Location::at(0),
128 ));
129 };
130 if let Word::Quoted(q) = word {
131 if q.has_escapes() {
132 return Err(FormError::syntax(
133 SyntaxError::InvalidBoundary,
134 Location::at(0),
135 ));
136 }
137 }
138 Boundary::new(word_raw(&word)).map(FormKind::Multipart)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::error::Limit;
146
147 fn reason(ct: &[u8]) -> Option<SyntaxError> {
148 match FormKind::detect(ct) {
149 Err(FormError::Syntax { reason, .. }) => Some(reason),
150 _ => None,
151 }
152 }
153
154 #[test]
155 fn test_detect_urlencoded_cases() {
156 for ct in [
157 &b"application/x-www-form-urlencoded"[..],
158 b"APPLICATION/X-WWW-FORM-URLENCODED",
159 b"application/x-www-form-urlencoded; charset=UTF-8",
160 b"application/x-www-form-urlencoded; charset=\"utf-8\"",
161 b"application/x-www-form-urlencoded; charset=us-ascii; other=1",
162 b" application/x-www-form-urlencoded ",
163 ] {
164 assert_eq!(FormKind::detect(ct), Ok(FormKind::Urlencoded), "{ct:?}");
165 }
166 assert!(matches!(
167 FormKind::detect(b"application/x-www-form-urlencoded; charset=iso-8859-1"),
168 Err(FormError::Encoding {
169 reason: EncodingError::UnsupportedCharset,
170 ..
171 })
172 ));
173 assert_eq!(
174 reason(b"application/x-www-form-urlencoded; charset=utf-8; Charset=utf-8"),
175 Some(SyntaxError::InvalidContentType)
176 );
177 }
178
179 #[test]
180 fn test_detect_multipart_boundary_rules() {
181 assert!(matches!(
182 FormKind::detect(b"multipart/form-data; boundary=x"),
183 Ok(FormKind::Multipart(_))
184 ));
185 assert!(matches!(
186 FormKind::detect(b"Multipart/Form-Data; Boundary=\"a b\""),
187 Ok(FormKind::Multipart(b)) if b.as_bytes() == b"a b"
188 ));
189 assert_eq!(
190 reason(b"multipart/form-data"),
191 Some(SyntaxError::MissingBoundary)
192 );
193 assert_eq!(
194 reason(b"multipart/form-data; boundary=a; boundary=a"),
195 Some(SyntaxError::DuplicateBoundary)
196 );
197 assert_eq!(
198 reason(b"multipart/form-data; boundary=\"\""),
199 Some(SyntaxError::InvalidBoundary)
200 );
201 assert_eq!(
202 reason(b"multipart/form-data; boundary=\"ab \""),
203 Some(SyntaxError::InvalidBoundary)
204 );
205 assert_eq!(
206 reason(b"multipart/form-data; boundary=\"a\\b\""),
207 Some(SyntaxError::InvalidBoundary)
208 );
209 assert_eq!(
210 reason(b"multipart/form-data; boundary="),
211 Some(SyntaxError::InvalidContentType)
212 );
213 assert_eq!(
214 reason(b"multipart/form-data; boundary=a; x=1; X=2"),
215 Some(SyntaxError::InvalidContentType)
216 );
217 assert_eq!(
218 reason(b"multipart/form-data; boundary=x junk"),
219 Some(SyntaxError::InvalidContentType)
220 );
221 }
222
223 #[test]
224 fn test_detect_unsupported_types() {
225 for ct in [
226 &b""[..],
227 b" ",
228 b"text/plain",
229 b"multipart/mixed; boundary=x",
230 b"application/json",
231 ] {
232 assert_eq!(
233 FormKind::detect(ct),
234 Err(FormError::UnsupportedMediaType),
235 "{ct:?}"
236 );
237 }
238 }
239
240 #[test]
241 fn test_detect_too_many_parameters_refused() {
242 let mut ct = alloc::vec::Vec::from(&b"multipart/form-data; boundary=x"[..]);
243 for i in 0..16 {
244 ct.extend_from_slice(alloc::format!("; p{i}=1").as_bytes());
245 }
246 assert_eq!(reason(&ct), Some(SyntaxError::InvalidContentType));
247 let _ = Limit::Body;
248 }
249}