Skip to main content

base_web_multipart/
multipart_parser.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::syntax::{is_field_name_byte, is_field_value_byte};
5use crate::{
6    Boundary, MultipartError, MultipartErrorKind, MultipartEvent, MultipartLimits, Parsed, PartHead,
7};
8use memchr::memmem::Finder;
9
10/// A sans-I/O parser for a multipart body, as RFC 2046 section 5.1 defines it, fed bytes as they arrive.
11///
12/// The caller owns the buffer. Each call to [`Self::parse`] reports how many bytes it consumed and what they held; the caller drops the consumed bytes, appends whatever arrives next, and calls again with what remains. Events borrow from the input, so content and header fields are never copied. A part's header section is returned whole, so the caller must be able to buffer [`Self::minimum_buffer_size`] bytes; the parser reports an error rather than needing more.
13///
14/// ```
15/// use base_web_multipart::{Boundary, MultipartEvent, MultipartParser};
16///
17/// let body = b"--b\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nvalue\r\n--b--\r\n";
18/// let mut parser = MultipartParser::new(&Boundary::new("b")?);
19/// let mut input = &body[..];
20/// let mut content = Vec::new();
21/// loop {
22///     let parsed = parser.parse(input)?;
23///     match parsed.event {
24///         MultipartEvent::Part(head) => assert_eq!(head.len(), 1),
25///         MultipartEvent::Content(bytes) => content.extend_from_slice(bytes),
26///         MultipartEvent::PartEnd => {}
27///         MultipartEvent::End => break,
28///         MultipartEvent::Incomplete => unreachable!("the whole body is available"),
29///     }
30///     input = &input[parsed.consumed..];
31/// }
32/// assert_eq!(content, b"value");
33/// # Ok::<(), Box<dyn std::error::Error>>(())
34/// ```
35#[derive(Clone, Debug)]
36pub struct MultipartParser {
37    /// `CRLF "--" boundary`, which ends a part's content.
38    delimiter: Vec<u8>,
39    /// Finds `"--" boundary`, wherever it appears, so that what precedes it can be checked.
40    finder: Finder<'static>,
41    limits: MultipartLimits,
42    state: State,
43    /// The bytes consumed before the current call, for the offsets of errors.
44    offset: u64,
45    preamble_size: usize,
46    part_count: usize,
47    /// The error the parser reported, which it reports again for every later call.
48    failed: Option<MultipartError>,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52enum State {
53    /// Nothing consumed: the body may begin with the boundary itself, without a preceding line break.
54    Start,
55    /// Before the first boundary.
56    Preamble,
57    /// After a boundary, before `--` or the line break that begins a part, with the spaces and tabs consumed so far.
58    Delimiter {
59        padding: usize,
60    },
61    /// After `--` that closes the body, with the spaces and tabs consumed so far, before the line break that ends the closing boundary.
62    Closing {
63        padding: usize,
64    },
65    /// In a part's header section, with the padding after its boundary, which counts toward its size, and the bytes of the input already searched for its end.
66    Headers {
67        padding: usize,
68        searched: usize,
69    },
70    /// At the empty line that ends a part's header section, whose line break may also begin the delimiter of a part with no content.
71    BodyStart,
72    Content,
73    End,
74}
75
76/// A `"--" boundary` in the input, classified by what precedes it.
77enum Found {
78    /// Preceded by CRLF: a delimiter, starting at the CR.
79    Delimiter(usize),
80    /// Preceded by a bare CR or LF, at this position, which some readers take for a delimiter.
81    Bare(usize),
82    /// Not in the input.
83    Absent,
84}
85
86impl MultipartParser {
87    /// A parser for a body with this boundary, with the default limits.
88    pub fn new(boundary: &Boundary) -> Self {
89        Self::with_limits(boundary, MultipartLimits::default())
90    }
91
92    /// A parser for a body with this boundary and these limits.
93    pub fn with_limits(boundary: &Boundary, limits: MultipartLimits) -> Self {
94        let delimiter = [b"\r\n--", boundary.as_str().as_bytes()].concat();
95        let finder = Finder::new(&delimiter[2..]).into_owned();
96        Self {
97            delimiter,
98            finder,
99            limits,
100            state: State::Start,
101            offset: 0,
102            preamble_size: 0,
103            part_count: 0,
104            failed: None,
105        }
106    }
107
108    /// The limits this parser applies.
109    pub fn limits(&self) -> MultipartLimits {
110        self.limits
111    }
112
113    /// The input a caller must be able to buffer: given at least this many bytes, [`Self::parse`] always consumes some or reports an event other than [`MultipartEvent::Incomplete`].
114    pub fn minimum_buffer_size(&self) -> usize {
115        self.limits
116            .header_size_limit
117            .saturating_add(2)
118            .max(self.delimiter.len())
119    }
120
121    /// Whether the closing boundary line has been read, so that [`MultipartEvent::End`] was reported.
122    pub fn is_finished(&self) -> bool {
123        self.state == State::End
124    }
125
126    /// Parse the next event from `input`, which must begin with the bytes the previous call did not consume.
127    ///
128    /// # Errors
129    ///
130    /// A [`MultipartError`] when the body is malformed or exceeds a limit. Once the parser reports an error, it reports the same error for every later call.
131    pub fn parse<'a>(&mut self, input: &'a [u8]) -> Result<Parsed<'a>, MultipartError> {
132        if let Some(error) = self.failed {
133            return Err(error);
134        }
135        let mut consumed = 0;
136        match self.step(input, &mut consumed) {
137            Ok(event) => {
138                self.offset += consumed as u64;
139                Ok(Parsed { consumed, event })
140            }
141            Err(error) => {
142                self.failed = Some(error);
143                Err(error)
144            }
145        }
146    }
147
148    /// Check that the body was complete, once its input has ended. A body may end immediately after its closing `--`, without a line break.
149    ///
150    /// # Errors
151    ///
152    /// The parser's error, if it reported one, or [`MultipartErrorKind::UnexpectedEnd`] when the closing boundary was not reached.
153    pub fn finish(&self) -> Result<(), MultipartError> {
154        if let Some(error) = self.failed {
155            return Err(error);
156        }
157        match self.state {
158            State::End | State::Closing { .. } => Ok(()),
159            _ => Err(MultipartError::new(
160                MultipartErrorKind::UnexpectedEnd,
161                self.offset,
162            )),
163        }
164    }
165
166    fn step<'a>(
167        &mut self,
168        input: &'a [u8],
169        consumed: &mut usize,
170    ) -> Result<MultipartEvent<'a>, MultipartError> {
171        // An error at this position in the input; the preamble and padding limits give their own offsets.
172        let error =
173            |kind, consumed: usize| MultipartError::new(kind, self.offset + consumed as u64);
174        loop {
175            let rest = &input[*consumed..];
176            match self.state {
177                State::Start => {
178                    let dash_boundary = &self.delimiter[2..];
179                    if rest.starts_with(dash_boundary) {
180                        *consumed += dash_boundary.len();
181                        self.state = State::Delimiter { padding: 0 };
182                    } else if rest.len() < dash_boundary.len() && dash_boundary.starts_with(rest) {
183                        return Ok(MultipartEvent::Incomplete);
184                    } else {
185                        self.state = State::Preamble;
186                    }
187                }
188                State::Preamble => {
189                    let found = self.find(rest);
190                    // The preamble before a bare line break is counted before the break is refused, as it is when the body is split before the break.
191                    let skipped = match found {
192                        Found::Delimiter(index) | Found::Bare(index) => index,
193                        Found::Absent => self.safe_length(rest),
194                    };
195                    // The preamble size never exceeds its limit, so this does not overflow.
196                    if skipped > self.limits.preamble_size_limit - self.preamble_size {
197                        // The preamble begins the body, so the error is at its start however the body is split.
198                        return Err(MultipartError::new(
199                            MultipartErrorKind::PreambleSizeLimit,
200                            0,
201                        ));
202                    }
203                    self.preamble_size += skipped;
204                    *consumed += skipped;
205                    match found {
206                        Found::Delimiter(_) => {
207                            *consumed += self.delimiter.len();
208                            self.state = State::Delimiter { padding: 0 };
209                        }
210                        Found::Bare(_) => {
211                            return Err(error(MultipartErrorKind::InvalidDelimiter, *consumed));
212                        }
213                        Found::Absent => return Ok(MultipartEvent::Incomplete),
214                    }
215                }
216                State::Delimiter { padding } => {
217                    if padding == 0 && rest.starts_with(b"--") {
218                        *consumed += 2;
219                        self.state = State::Closing { padding: 0 };
220                        continue;
221                    }
222                    let padding = self.pad(rest, padding, consumed)?;
223                    self.state = State::Delimiter { padding };
224                    let rest = &input[*consumed..];
225                    if rest.starts_with(b"\r\n") {
226                        self.part_count += 1;
227                        if self.part_count > self.limits.part_count_limit {
228                            return Err(error(MultipartErrorKind::PartCountLimit, *consumed));
229                        }
230                        *consumed += 2;
231                        self.state = State::Headers {
232                            padding,
233                            searched: 0,
234                        };
235                    } else if rest.is_empty() || rest == b"\r" || (padding == 0 && rest == b"-") {
236                        return Ok(MultipartEvent::Incomplete);
237                    } else {
238                        return Err(error(MultipartErrorKind::InvalidDelimiter, *consumed));
239                    }
240                }
241                State::Closing { padding } => {
242                    let padding = self.pad(rest, padding, consumed)?;
243                    self.state = State::Closing { padding };
244                    let rest = &input[*consumed..];
245                    if rest.starts_with(b"\r\n") {
246                        *consumed += 2;
247                        self.state = State::End;
248                        return Ok(MultipartEvent::End);
249                    } else if rest.is_empty() || rest == b"\r" {
250                        // The body may also end here, which `finish` accepts.
251                        return Ok(MultipartEvent::Incomplete);
252                    } else {
253                        return Err(error(MultipartErrorKind::InvalidDelimiter, *consumed));
254                    }
255                }
256                State::Headers { padding, searched } => {
257                    // The padding after the boundary counts toward the header section's size.
258                    let limit = self.limits.header_size_limit - padding;
259                    let length = if rest.starts_with(b"\r\n") {
260                        Some(0)
261                    } else {
262                        let start = searched.saturating_sub(3).min(rest.len());
263                        memchr::memmem::find(&rest[start..], b"\r\n\r\n")
264                            .map(|index| start + index + 2)
265                    };
266                    let Some(length) = length else {
267                        // A section within the limit ends within its length and the empty line's CRLF.
268                        if rest.len() >= limit.saturating_add(2) {
269                            return Err(error(MultipartErrorKind::HeaderSizeLimit, *consumed));
270                        }
271                        self.state = State::Headers {
272                            padding,
273                            searched: rest.len(),
274                        };
275                        return Ok(MultipartEvent::Incomplete);
276                    };
277                    if length > limit {
278                        return Err(error(MultipartErrorKind::HeaderSizeLimit, *consumed));
279                    }
280                    let section = &rest[..length];
281                    self.check_section(section)
282                        .map_err(|kind| error(kind, *consumed))?;
283                    *consumed += length;
284                    self.state = State::BodyStart;
285                    return Ok(MultipartEvent::Part(PartHead::new(section)));
286                }
287                State::BodyStart => {
288                    // RFC 2046 makes the content `[CRLF *OCTET]`, so the empty line's CRLF is also the delimiter's when the part has no content.
289                    if rest.starts_with(&self.delimiter) {
290                        *consumed += self.delimiter.len();
291                        self.state = State::Delimiter { padding: 0 };
292                        return Ok(MultipartEvent::PartEnd);
293                    }
294                    if rest.len() < self.delimiter.len() && self.delimiter.starts_with(rest) {
295                        return Ok(MultipartEvent::Incomplete);
296                    }
297                    *consumed += 2;
298                    self.state = State::Content;
299                }
300                State::Content => {
301                    // SECURITY: Content is not limited, so a part can be arbitrarily large. This is not a denial-of-service weakness: content is reported as it arrives, borrowed from the caller's input, so the parser holds none of it, and design/scope.md puts content limits with the consumer, such as `base-web-content`'s field and upload limits, which know what each part is for.
302                    return match self.find(rest) {
303                        Found::Delimiter(0) => {
304                            *consumed += self.delimiter.len();
305                            self.state = State::Delimiter { padding: 0 };
306                            Ok(MultipartEvent::PartEnd)
307                        }
308                        Found::Delimiter(index) => {
309                            *consumed += index;
310                            Ok(MultipartEvent::Content(&rest[..index]))
311                        }
312                        // The content before a bare line break is reported before the break is refused, as it is when the body is split before the break.
313                        Found::Bare(0) => {
314                            Err(error(MultipartErrorKind::InvalidDelimiter, *consumed))
315                        }
316                        Found::Bare(index) => {
317                            *consumed += index;
318                            Ok(MultipartEvent::Content(&rest[..index]))
319                        }
320                        Found::Absent => match self.safe_length(rest) {
321                            0 => Ok(MultipartEvent::Incomplete),
322                            length => {
323                                *consumed += length;
324                                Ok(MultipartEvent::Content(&rest[..length]))
325                            }
326                        },
327                    };
328                }
329                State::End => {
330                    // SECURITY: The epilogue is ignored and not limited. This is not a denial-of-service weakness: it is consumed as it arrives without being kept or reported, so it costs no memory, and a limit on the body's total size belongs to the transport or the consumer.
331                    *consumed += rest.len();
332                    return Ok(MultipartEvent::End);
333                }
334            }
335        }
336    }
337
338    /// Consume the spaces and tabs of transport padding after a boundary, which count toward the header size limit.
339    fn pad(
340        &self,
341        rest: &[u8],
342        padding: usize,
343        consumed: &mut usize,
344    ) -> Result<usize, MultipartError> {
345        let spaces = rest
346            .iter()
347            .take_while(|&&byte| byte == b' ' || byte == b'\t')
348            .count();
349        // The padding never exceeds the limit, so this does not overflow.
350        if spaces > self.limits.header_size_limit - padding {
351            // The padding consumed by earlier calls precedes this input, so the error is at the padding's start however the body is split.
352            let start = self.offset + *consumed as u64 - padding as u64;
353            return Err(MultipartError::new(
354                MultipartErrorKind::HeaderSizeLimit,
355                start,
356            ));
357        }
358        let padding = padding + spaces;
359        *consumed += spaces;
360        Ok(padding)
361    }
362
363    /// The first `"--" boundary` in `input` that follows a line break. One after CRLF is a delimiter; one after a bare CR or LF is an error, since some readers take it for a delimiter; others are content.
364    fn find(&self, input: &[u8]) -> Found {
365        let mut from = 0;
366        while let Some(index) = self.finder.find(&input[from..]).map(|index| from + index) {
367            if index >= 2 && &input[index - 2..index] == b"\r\n" {
368                return Found::Delimiter(index - 2);
369            }
370            if index >= 1 && matches!(input[index - 1], b'\r' | b'\n') {
371                return Found::Bare(index - 1);
372            }
373            from = index + 1;
374        }
375        Found::Absent
376    }
377
378    /// The bytes at the start of `input` that cannot be part of a boundary still arriving: everything before the first line break from which the rest could become `CRLF "--" boundary`, or a boundary after a bare line break.
379    fn safe_length(&self, input: &[u8]) -> usize {
380        let dash_boundary = &self.delimiter[2..];
381        let start = input.len().saturating_sub(self.delimiter.len() - 1);
382        memchr::memchr2_iter(b'\r', b'\n', &input[start..])
383            .map(|index| start + index)
384            .find(|&index| {
385                self.delimiter.starts_with(&input[index..])
386                    || dash_boundary.starts_with(&input[index + 1..])
387            })
388            .unwrap_or(input.len())
389    }
390
391    /// Check each line of a header section, which ends with CRLF and has no empty line.
392    fn check_section(&self, section: &[u8]) -> Result<(), MultipartErrorKind> {
393        let (mut count, mut rest) = (0, section);
394        // Lines are split at CRLF only, so a bare CR or LF stays in its line, where neither a name nor a value allows it.
395        while let Some(end) = memchr::memmem::find(rest, b"\r\n") {
396            let line = &rest[..end];
397            rest = &rest[end + 2..];
398            count += 1;
399            if count > self.limits.header_count_limit {
400                return Err(MultipartErrorKind::HeaderCountLimit);
401            }
402            if line
403                .first()
404                .is_some_and(|&byte| byte == b' ' || byte == b'\t')
405            {
406                return Err(MultipartErrorKind::FoldedField);
407            }
408            let colon = memchr::memchr(b':', line).ok_or(MultipartErrorKind::InvalidField)?;
409            let (name, value) = (&line[..colon], &line[colon + 1..]);
410            if name.is_empty()
411                || !name.iter().all(|&byte| is_field_name_byte(byte))
412                || !value.iter().all(|&byte| is_field_value_byte(byte))
413            {
414                return Err(MultipartErrorKind::InvalidField);
415            }
416        }
417        Ok(())
418    }
419}