Skip to main content

form_net/
kind.rs

1//! Form kind detection from the request's `Content-Type`, and the parameter
2//! rules shared with part headers.
3
4use parse_net::{DuplicatePolicy, Params, Word};
5
6use crate::error::{EncodingError, FormError, Location, SyntaxError};
7use crate::multipart::boundary::Boundary;
8
9/// parse-net limits for form headers: at most 16 parameters, and repeated
10/// names reported to form-net, which applies its own repeat rules (D-063).
11pub(crate) const HEADER_LIMITS: parse_net::Limits = parse_net::Limits::DEFAULT
12    .max_params(16)
13    .duplicate_params(DuplicatePolicy::First);
14
15/// The name of the first parameter whose name was already used, compared
16/// ASCII case-insensitively. Quadratic in the parameter count, which
17/// `HEADER_LIMITS` caps at 16.
18pub(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
31/// The bytes of a parameter value as sent: a token, or the content of a
32/// quoted string with backslashes kept literal.
33pub(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/// Which form encoding a request body uses.
41///
42/// # Examples
43///
44/// ```
45/// use form_net::{FormError, FormKind};
46///
47/// match FormKind::detect(b"multipart/form-data; boundary=----WebKitFormBoundaryx7Y")? {
48///     FormKind::Urlencoded => { /* parse_urlencoded */ }
49///     FormKind::Multipart(boundary) => assert_eq!(boundary.as_bytes(), b"----WebKitFormBoundaryx7Y"),
50///     _ => return Err(FormError::UnsupportedMediaType),
51/// }
52/// assert_eq!(FormKind::detect(b"Application/X-WWW-Form-Urlencoded; charset=UTF-8")?, FormKind::Urlencoded);
53/// assert_eq!(FormKind::detect(b"text/plain"), Err(FormError::UnsupportedMediaType));
54/// # Ok::<(), FormError>(())
55/// ```
56#[non_exhaustive]
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum FormKind {
59    /// `application/x-www-form-urlencoded`.
60    Urlencoded,
61    /// `multipart/form-data`, with its validated boundary.
62    Multipart(Boundary),
63}
64
65impl FormKind {
66    /// Reads a request's `Content-Type` value. A pure function of the header
67    /// bytes; it never looks at the body.
68    ///
69    /// `application/x-www-form-urlencoded` is accepted with any parameters
70    /// except a `charset` other than UTF-8 or US-ASCII. `multipart/form-data`
71    /// needs exactly one valid `boundary`. Everything else, including
72    /// `text/plain`, is `UnsupportedMediaType`. A repeated parameter is
73    /// refused.
74    ///
75    /// # Errors
76    ///
77    /// `UnsupportedMediaType`; `Syntax { InvalidContentType | MissingBoundary
78    /// | DuplicateBoundary | InvalidBoundary }`; `Encoding { UnsupportedCharset }`.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use form_net::{FormError, FormKind, SyntaxError};
84    ///
85    /// assert!(matches!(
86    ///     FormKind::detect(b"multipart/form-data; boundary=a; boundary=b"),
87    ///     Err(FormError::Syntax { reason: SyntaxError::DuplicateBoundary, .. })
88    /// ));
89    /// ```
90    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(&params) {
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}