Skip to main content

form_net/multipart/
boundary.rs

1//! The multipart boundary: validation, the delimiter bytes, and the failure
2//! table of the streaming matcher.
3
4use core::fmt;
5
6use crate::error::{FormError, Location, SyntaxError};
7
8/// The longest boundary RFC 2046 allows.
9pub const MAX_BOUNDARY_LEN: usize = 70;
10
11/// The longest delimiter: CRLF, `--`, and a 70-byte boundary.
12pub const MAX_DELIMITER_LEN: usize = 74;
13
14/// RFC 2046 `bchars`: digits, letters, `'()+_,-./:=?`, and space.
15const fn is_bchar(b: u8) -> bool {
16    b.is_ascii_alphanumeric()
17        || matches!(
18            b,
19            b'\''
20                | b'('
21                | b')'
22                | b'+'
23                | b'_'
24                | b','
25                | b'-'
26                | b'.'
27                | b'/'
28                | b':'
29                | b'='
30                | b'?'
31                | b' '
32        )
33}
34
35/// A validated boundary with its delimiter (`CRLF "--" boundary`, 5 to 74
36/// bytes) and the failure table of the streaming matcher, built once.
37///
38/// `Debug` prints the boundary: it is not secret.
39///
40/// # Examples
41///
42/// ```
43/// use form_net::Boundary;
44///
45/// let b = Boundary::new(b"----WebKitFormBoundaryx7Y")?;
46/// assert_eq!(b.delimiter(), b"\r\n------WebKitFormBoundaryx7Y");
47/// assert!(Boundary::new(b"").is_err());
48/// assert!(Boundary::new(&[b'a'; 71]).is_err());
49/// assert!(Boundary::new(b"ends with space ").is_err());
50/// assert!(Boundary::new(b"has\"quote").is_err());
51/// assert_eq!(Boundary::parse(b"multipart/form-data; boundary=\"a b\"")?.as_bytes(), b"a b");
52/// # Ok::<(), form_net::FormError>(())
53/// ```
54#[must_use]
55#[derive(Clone, Copy, PartialEq, Eq)]
56pub struct Boundary {
57    delimiter: [u8; MAX_DELIMITER_LEN],
58    fail: [u8; MAX_DELIMITER_LEN],
59    len: u8,
60}
61
62impl Boundary {
63    /// Validates a bare boundary (without `--`): 1 to 70 bytes of RFC 2046
64    /// `bchars`, not ending in a space.
65    ///
66    /// # Errors
67    ///
68    /// `Syntax { InvalidBoundary }`, at the offending byte.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// assert_eq!(form_net::Boundary::new(b"X")?.delimiter(), b"\r\n--X");
74    /// # Ok::<(), form_net::FormError>(())
75    /// ```
76    pub fn new(boundary: &[u8]) -> Result<Boundary, FormError> {
77        let invalid =
78            |at: usize| FormError::syntax(SyntaxError::InvalidBoundary, Location::at(at as u64));
79        if boundary.is_empty() {
80            return Err(invalid(0));
81        }
82        if boundary.len() > MAX_BOUNDARY_LEN {
83            return Err(invalid(MAX_BOUNDARY_LEN));
84        }
85        if let Some(at) = boundary.iter().position(|&b| !is_bchar(b)) {
86            return Err(invalid(at));
87        }
88        if boundary.last() == Some(&b' ') {
89            return Err(invalid(boundary.len().wrapping_sub(1)));
90        }
91        let mut delimiter = [0u8; MAX_DELIMITER_LEN];
92        let len = boundary.len().wrapping_add(4);
93        if let Some(head) = delimiter.get_mut(..4) {
94            head.copy_from_slice(b"\r\n--");
95        }
96        if let Some(rest) = delimiter.get_mut(4..len) {
97            rest.copy_from_slice(boundary);
98        }
99        let fail = failure_table(&delimiter, len);
100        Ok(Boundary {
101            delimiter,
102            fail,
103            len: len as u8,
104        })
105    }
106
107    /// A fresh boundary: `form-net-` and 32 lowercase hex characters from 16
108    /// bytes of the OS random source (41 bytes, all `bchars`).
109    ///
110    /// # Errors
111    ///
112    /// `Random` when the OS random source fails (fail closed).
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// let b = form_net::Boundary::generate()?;
118    /// assert_eq!(b.as_bytes().len(), 41);
119    /// assert!(b.as_bytes().starts_with(b"form-net-"));
120    /// assert_ne!(b, form_net::Boundary::generate()?);
121    /// # Ok::<(), form_net::FormError>(())
122    /// ```
123    #[cfg(feature = "random")]
124    #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
125    pub fn generate() -> Result<Boundary, FormError> {
126        let hex = crate::random::random_hex()?;
127        let mut name = [0u8; 41];
128        if let Some(head) = name.get_mut(..9) {
129            head.copy_from_slice(b"form-net-");
130        }
131        if let Some(tail) = name.get_mut(9..) {
132            tail.copy_from_slice(&hex);
133        }
134        Boundary::new(&name)
135    }
136
137    /// Reads the boundary from a whole `multipart/form-data` `Content-Type`
138    /// value.
139    ///
140    /// # Errors
141    ///
142    /// `UnsupportedMediaType` for any other media type;
143    /// `Syntax { InvalidContentType | MissingBoundary | DuplicateBoundary | InvalidBoundary }`.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// let b = form_net::Boundary::parse(b"multipart/form-data; boundary=----x")?;
149    /// assert_eq!(b.as_bytes(), b"----x");
150    /// assert!(form_net::Boundary::parse(b"text/plain").is_err());
151    /// # Ok::<(), form_net::FormError>(())
152    /// ```
153    pub fn parse(content_type: &[u8]) -> Result<Boundary, FormError> {
154        match crate::kind::FormKind::detect(content_type)? {
155            crate::kind::FormKind::Multipart(b) => Ok(b),
156            crate::kind::FormKind::Urlencoded => Err(FormError::UnsupportedMediaType),
157        }
158    }
159
160    /// The boundary, without `--`.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// assert_eq!(form_net::Boundary::new(b"abc")?.as_bytes(), b"abc");
166    /// # Ok::<(), form_net::FormError>(())
167    /// ```
168    #[must_use]
169    pub fn as_bytes(&self) -> &[u8] {
170        self.delimiter.get(4..usize::from(self.len)).unwrap_or(&[])
171    }
172
173    /// The full delimiter: CRLF, `--`, and the boundary.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// assert_eq!(form_net::Boundary::new(b"abc")?.delimiter(), b"\r\n--abc");
179    /// # Ok::<(), form_net::FormError>(())
180    /// ```
181    #[must_use]
182    pub fn delimiter(&self) -> &[u8] {
183        self.delimiter.get(..usize::from(self.len)).unwrap_or(&[])
184    }
185
186    /// One step of the matcher: the new state after byte `c` from state `k`
187    /// (the number of delimiter bytes matched so far, below the length).
188    #[inline]
189    pub(crate) fn step(&self, mut k: usize, c: u8) -> usize {
190        #[cfg(test)]
191        tests::count_visits(1);
192        loop {
193            if self.delimiter.get(k) == Some(&c) {
194                return k.wrapping_add(1);
195            }
196            if k == 0 {
197                return 0;
198            }
199            k = usize::from(*self.fail.get(k.wrapping_sub(1)).unwrap_or(&0));
200        }
201    }
202
203    pub(crate) fn len(&self) -> usize {
204        usize::from(self.len)
205    }
206
207    /// One step of the bare-break matcher: the new state after byte `c`
208    /// from state `m`. It matches a lone CR or LF followed by `--boundary`,
209    /// the delimiter with its first byte made a choice of CR or LF and its
210    /// LF dropped. The boundary holds neither, so the only restart point is
211    /// the line break itself and no failure table is needed (D-113).
212    #[inline]
213    pub(crate) fn bare_step(&self, m: usize, c: u8) -> usize {
214        if m > 0 && self.delimiter.get(m.wrapping_add(1)) == Some(&c) {
215            m.wrapping_add(1)
216        } else {
217            usize::from(crate::scan::is_break(c))
218        }
219    }
220
221    /// Runs both matchers over `hay` from states `k` (the delimiter) and
222    /// `m` (a line break followed by `--boundary`). Returns the final
223    /// states and what completed first; a completed delimiter wins over
224    /// the bare-break match that ends at the same byte inside it.
225    ///
226    /// From states 0 a match can only start at a CR or LF followed by `-`:
227    /// the delimiter's LF is followed by `--`, and the boundary holds no
228    /// line break. So the search skips to the next such pair 32 positions
229    /// at a time and compares `--boundary` after it whole; the byte before
230    /// tells a delimiter (CR LF) from a bare break. A miss resumes at the
231    /// byte that differed: the bytes before it are boundary, which holds no
232    /// line break. A match that spans chunks is compared too, against the bytes it
233    /// still needs at the start of `hay` and the bytes `hay` still has at
234    /// its end: with no line break in the boundary, at most one match is
235    /// live past a lone CR, and the moment it fails nothing else is. So the
236    /// states returned are exactly the automata's, at a compare per chunk
237    /// (D-102, D-113, D-117). The same fact keeps the compares linear: a
238    /// candidate can never start inside the bytes the one before it matched.
239    pub(crate) fn run(&self, mut k: usize, mut m: usize, hay: &[u8]) -> (usize, usize, Hit) {
240        let len = self.len();
241        let bare = len.wrapping_sub(1);
242        let delimiter = self.delimiter();
243        let dashes = delimiter.get(2..).unwrap_or(&[]);
244        let mut i = 0usize;
245        if k == 1 {
246            // A CR carried in is the one state with two futures: the next
247            // byte makes it a delimiter, a bare break, or nothing.
248            let Some(&c) = hay.first() else {
249                return (k, m, Hit::None);
250            };
251            k = self.step(k, c);
252            m = self.bare_step(m, c);
253            i = 1;
254            if k == 1 {
255                // Another CR: the search below starts at it.
256                (i, k, m) = (0, 0, 0);
257            }
258        }
259        if m > 0 {
260            // One match carried in, delimiter or bare: compare what it
261            // still needs.
262            let need = delimiter.get(m.wrapping_add(1)..).unwrap_or(&[]);
263            let rest = hay.get(i..).unwrap_or(&[]);
264            let same = shared(need, rest);
265            if same == need.len() {
266                let end = i.wrapping_add(same);
267                return if k > 0 {
268                    (len, 0, Hit::Delimiter(end))
269                } else {
270                    (0, bare, Hit::BareBreak(end))
271                };
272            }
273            if same == rest.len() {
274                // All of `hay` extended the match.
275                let k = if k > 0 { k.wrapping_add(same) } else { 0 };
276                return (k, m.wrapping_add(same), Hit::None);
277            }
278            i = i.wrapping_add(same);
279        }
280        while i < hay.len() {
281            let at = crate::scan::find_break_dash(hay, i);
282            if at >= hay.len() {
283                break;
284            }
285            let start = at.wrapping_add(1);
286            let after_cr = hay.get(at) == Some(&b'\n')
287                && at > 0
288                && hay.get(at.wrapping_sub(1)) == Some(&b'\r');
289            let rest = hay.get(start..).unwrap_or(&[]);
290            let same = shared(dashes, rest);
291            if same == dashes.len() {
292                let end = start.wrapping_add(same);
293                return if after_cr {
294                    (len, 0, Hit::Delimiter(end))
295                } else {
296                    (0, bare, Hit::BareBreak(end))
297                };
298            }
299            if same == rest.len() {
300                // All of `hay` is the start of a match, which goes on in
301                // the next chunk.
302                let k = if after_cr {
303                    same.wrapping_add(2)
304                } else {
305                    usize::from(same == 0 && hay.get(at) == Some(&b'\r'))
306                };
307                return (k, same.wrapping_add(1), Hit::None);
308            }
309            i = start.wrapping_add(same);
310        }
311        (0, 0, Hit::None)
312    }
313}
314
315/// How many leading bytes `a` and `b` share.
316#[inline]
317pub(crate) fn shared(a: &[u8], b: &[u8]) -> usize {
318    let n = crate::scan::common_prefix(a, b);
319    #[cfg(test)]
320    tests::count_visits(n.wrapping_add(1));
321    n
322}
323
324/// What a run of the matchers found.
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub(crate) enum Hit {
327    /// Nothing: all of `hay` was consumed.
328    None,
329    /// The delimiter, ending just before this index.
330    Delimiter(usize),
331    /// `--boundary` after a lone CR or LF, ending just before this index
332    /// (D-113).
333    BareBreak(usize),
334}
335
336/// The Knuth-Morris-Pratt failure table: `fail[i]` is the length of the
337/// longest proper prefix of `d[..=i]` that is also its suffix.
338fn failure_table(d: &[u8; MAX_DELIMITER_LEN], len: usize) -> [u8; MAX_DELIMITER_LEN] {
339    let mut fail = [0u8; MAX_DELIMITER_LEN];
340    let mut k = 0usize;
341    for i in 1..len {
342        let c = *d.get(i).unwrap_or(&0);
343        while k > 0 && d.get(k) != Some(&c) {
344            k = usize::from(*fail.get(k.wrapping_sub(1)).unwrap_or(&0));
345        }
346        if d.get(k) == Some(&c) {
347            k = k.wrapping_add(1);
348        }
349        if let Some(slot) = fail.get_mut(i) {
350            *slot = k as u8;
351        }
352    }
353    fail
354}
355
356impl fmt::Debug for Boundary {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        // bchars are printable ASCII, so this is always valid text.
359        let text = core::str::from_utf8(self.as_bytes()).unwrap_or("");
360        f.debug_tuple("Boundary").field(&text).finish()
361    }
362}
363
364#[cfg(test)]
365pub(crate) mod tests {
366    use super::*;
367    use alloc::vec::Vec;
368    use core::cell::Cell;
369
370    std::thread_local! {
371        static VISITS: Cell<u64> = const { Cell::new(0) };
372    }
373
374    /// Counts bytes the matcher compares: one call to `step`, which may
375    /// follow the failure table several times (KMP bounds those by the
376    /// bytes seen), or the bytes of one whole compare.
377    pub(crate) fn count_visits(n: usize) {
378        VISITS.with(|v| v.set(v.get() + n as u64));
379    }
380
381    #[cfg(feature = "alloc")]
382    pub(crate) fn visits() -> u64 {
383        VISITS.with(Cell::get)
384    }
385
386    /// Runs `body` through a parser in `chunk`-byte pieces and returns the
387    /// matcher's byte visits.
388    #[cfg(feature = "alloc")]
389    fn visits_for(body: &[u8], chunk: usize) -> u64 {
390        use crate::{Event, Limits, MultipartParser, Policy};
391        let limits = Limits::DEFAULT
392            .with_max_body_bytes(64 << 20)
393            .with_max_file_bytes(64 << 20);
394        let mut p = MultipartParser::new(
395            Boundary::new(b"boundary").unwrap(),
396            &limits,
397            &Policy::DEFAULT,
398        )
399        .unwrap();
400        let before = visits();
401        let chunks: Vec<&[u8]> = body.chunks(chunk).collect();
402        for (i, c) in chunks.iter().enumerate() {
403            let mut rest: &[u8] = c;
404            loop {
405                let step = p.parse(rest, i + 1 == chunks.len()).unwrap();
406                rest = &rest[step.consumed..];
407                if matches!(step.event, Event::NeedInput | Event::End) {
408                    break;
409                }
410            }
411        }
412        visits() - before
413    }
414
415    #[cfg(feature = "alloc")]
416    #[test]
417    fn test_matcher_visits_are_linear_for_adversarial_bodies() {
418        let head = b"--boundary\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n";
419        let shapes: [&[u8]; 4] = [b"\r", b"\r\n--boundar", b"\r\n-", b"-"];
420        for shape in shapes {
421            for chunk in [1usize, 7, 4096] {
422                let mut body = head.to_vec();
423                while body.len() < head.len() + 64 * 1024 {
424                    body.extend_from_slice(shape);
425                }
426                let data = (body.len() - head.len()) as u64;
427                body.extend_from_slice(b"\r\n--boundary--");
428                let v = visits_for(&body, chunk);
429                // Each data byte is examined a bounded number of times
430                // (KMP), plus at most a delimiter's length rescanned per
431                // chunk boundary.
432                let bound = 2 * data + (body.len() / chunk + 1) as u64 * 16 + 64;
433                assert!(
434                    v <= bound,
435                    "{shape:?} chunk {chunk}: {v} visits for {data} bytes"
436                );
437            }
438        }
439    }
440
441    #[test]
442    fn test_boundary_every_byte_value() {
443        for b in 0u8..=255 {
444            let r = Boundary::new(&[b'a', b, b'z']);
445            assert_eq!(r.is_ok(), is_bchar(b), "byte {b:#x}");
446        }
447    }
448
449    #[test]
450    fn test_boundary_lengths_and_trailing_space() {
451        assert!(Boundary::new(b"").is_err());
452        assert!(Boundary::new(b"a").is_ok());
453        assert!(Boundary::new(&[b'a'; 69]).is_ok());
454        assert!(Boundary::new(&[b'a'; 70]).is_ok());
455        assert!(Boundary::new(&[b'a'; 71]).is_err());
456        assert!(Boundary::new(b"a ").is_err());
457        assert!(Boundary::new(b"a b").is_ok());
458        assert!(Boundary::new(b" ").is_err());
459    }
460
461    #[test]
462    fn test_failure_table_matches_definition() {
463        for raw in [&b"a"[..], b"aaaa", b"-", b"--", b"-a-", b"---", b"a-b--a"] {
464            let b = Boundary::new(raw).unwrap();
465            let d = b.delimiter();
466            for i in 0..d.len() {
467                let best = (0..=i)
468                    .rev()
469                    .find(|&l| l <= i && d[..l] == d[i + 1 - l..=i])
470                    .unwrap();
471                assert_eq!(usize::from(b.fail[i]), best, "{raw:?} at {i}");
472            }
473        }
474    }
475
476    /// The first match in `hay` by a plain search: the delimiter, or a
477    /// lone CR or LF followed by `--boundary`, by where it ends.
478    fn naive_hit(b: &Boundary, hay: &[u8]) -> Hit {
479        let d = b.delimiter();
480        let dashes = &d[2..];
481        for end in 0..=hay.len() {
482            if end >= d.len() && &hay[end - d.len()..end] == d {
483                return Hit::Delimiter(end);
484            }
485            if end > dashes.len() && &hay[end - dashes.len()..end] == dashes {
486                let p = end - dashes.len() - 1;
487                if hay[p] == b'\r' || hay[p] == b'\n' {
488                    return Hit::BareBreak(end);
489                }
490            }
491        }
492        Hit::None
493    }
494
495    /// Runs the matchers over `chunks` in turn, carrying the states, and
496    /// returns the first hit with its index in the whole.
497    fn chunked_hit(b: &Boundary, chunks: &[&[u8]]) -> Hit {
498        let (mut k, mut m, mut base) = (0, 0, 0);
499        for chunk in chunks {
500            let (k2, m2, hit) = b.run(k, m, chunk);
501            match hit {
502                Hit::Delimiter(e) => return Hit::Delimiter(base + e),
503                Hit::BareBreak(e) => return Hit::BareBreak(base + e),
504                Hit::None => (k, m, base) = (k2, m2, base + chunk.len()),
505            }
506        }
507        Hit::None
508    }
509
510    #[test]
511    fn test_run_finds_like_naive_and_carries_state() {
512        let b = Boundary::new(b"-ab").unwrap();
513        let d = b.delimiter().to_vec();
514        let mut hay = Vec::new();
515        hay.extend_from_slice(b"x\r\n--a\r\n\r\n--\r\n--");
516        hay.extend_from_slice(&d);
517        hay.extend_from_slice(b"tail");
518        let expected = naive_hit(&b, &hay);
519        assert!(matches!(expected, Hit::Delimiter(_)));
520        assert_eq!(chunked_hit(&b, &[&hay]), expected);
521        for split in 0..hay.len() {
522            let (x, y) = hay.split_at(split);
523            assert_eq!(chunked_hit(&b, &[x, y]), expected, "split {split}");
524        }
525    }
526
527    /// D-113: the delimiter and bare breaks against the plain search, on
528    /// hays dense in CR, LF, and dashes, whole, at every split, and byte
529    /// by byte, for boundaries with and without dashes of their own.
530    #[test]
531    fn test_bare_breaks_and_delimiters_match_naive_at_every_split() {
532        let mut seed = 11u64;
533        for boundary in [&b"-ab"[..], b"--", b"x", b"ab-"] {
534            let b = Boundary::new(boundary).unwrap();
535            for len in 0..40 {
536                for _ in 0..60 {
537                    let hay: Vec<u8> = (0..len)
538                        .map(|_| {
539                            seed ^= seed << 13;
540                            seed ^= seed >> 7;
541                            seed ^= seed << 17;
542                            b"\r\n--abx"[(seed % 7) as usize]
543                        })
544                        .collect();
545                    let expected = naive_hit(&b, &hay);
546                    assert_eq!(chunked_hit(&b, &[&hay]), expected, "{hay:?}");
547                    for split in 0..=hay.len() {
548                        let (x, y) = hay.split_at(split);
549                        assert_eq!(chunked_hit(&b, &[x, y]), expected, "{hay:?} at {split}");
550                    }
551                    let bytes: Vec<&[u8]> = hay.chunks(1).collect();
552                    assert_eq!(chunked_hit(&b, &bytes), expected, "{hay:?} bytewise");
553                }
554            }
555        }
556    }
557
558    /// D-117: `run` compares where the automata step, and the states it
559    /// leaves are exactly theirs, so a match carries across any split.
560    #[test]
561    fn test_run_leaves_the_automata_states_at_every_split() {
562        let mut seed = 5u64;
563        for boundary in [&b"-ab"[..], b"--", b"x", b"ab-", b"abcdefghijklmnop"] {
564            let b = Boundary::new(boundary).unwrap();
565            let bare = b.len() - 1;
566            let hay_bytes: &[u8] = &[b'\r', b'\n', b'-', b'-', boundary[0], b'x'];
567            for len in 0..48 {
568                for _ in 0..40 {
569                    let mut hay: Vec<u8> = (0..len)
570                        .map(|_| {
571                            seed ^= seed << 13;
572                            seed ^= seed >> 7;
573                            seed ^= seed << 17;
574                            hay_bytes[(seed % 6) as usize]
575                        })
576                        .collect();
577                    // A partial match at the end, of every length.
578                    let d = b.delimiter();
579                    hay.extend_from_slice(&d[..len % d.len()]);
580                    if !matches!(naive_hit(&b, &hay), Hit::None) {
581                        continue;
582                    }
583                    let (mut k, mut m) = (0, 0);
584                    for &c in &hay {
585                        k = b.step(k, c);
586                        m = b.bare_step(m, c);
587                        assert!(k < b.len() && m < bare);
588                    }
589                    assert_eq!(&b.run(0, 0, &hay), &(k, m, Hit::None), "{hay:?}");
590                    for split in 0..=hay.len() {
591                        let (x, y) = hay.split_at(split);
592                        let (k1, m1, _) = b.run(0, 0, x);
593                        assert_eq!(&b.run(k1, m1, y), &(k, m, Hit::None), "{hay:?} at {split}");
594                    }
595                }
596            }
597        }
598    }
599
600    #[test]
601    fn test_parse_reports_unsupported_for_urlencoded() {
602        assert_eq!(
603            Boundary::parse(b"application/x-www-form-urlencoded"),
604            Err(FormError::UnsupportedMediaType)
605        );
606    }
607}