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}