Skip to main content

nostd_multipart/
parser.rs

1//! The streaming parser (DESIGN.md §2–7, §9–10).
2
3use crate::chars::{is_ows, is_token, is_valid_boundary, ows_trimmed_range, param, trim_ows};
4use crate::Error;
5
6/// Maximum number of SP/HTAB transport-padding bytes accepted between the boundary
7/// and the CRLF of a part delimiter (rule 3). More padding means "not a delimiter".
8pub const MAX_PADDING: usize = 8;
9
10/// `min_buffer()` minus the boundary length: `"\r\n--"` + padding + `"\r\n"`.
11const LOOKAHEAD_EXTRA: usize = 4 + MAX_PADDING + 2;
12
13/// One parse event. `'i` is the lifetime of the `input` slice passed to
14/// [`Parser::parse`], `'p` the borrow of the parser.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Event<'i, 'p> {
17    /// A part delimiter was consumed; headers follow.
18    PartStart,
19    /// One header line of the current part. Both slices borrow the parser's line
20    /// buffer and are valid until the next call.
21    Header {
22        /// The field name, an RFC 7230 token (case preserved).
23        name: &'p [u8],
24        /// The field value with surrounding SP/HTAB removed; may contain bytes
25        /// ≥ 0x80.
26        value: &'p [u8],
27    },
28    /// The empty line after the headers; body data follows.
29    HeadersEnd,
30    /// A non-empty sub-slice of `input` belonging to the current part body.
31    Data(&'i [u8]),
32    /// The body of the current part is complete.
33    PartEnd,
34    /// The close delimiter was consumed. Every later call returns this again.
35    Finished,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum State {
40    Preamble { at_start: bool },
41    Headers,
42    Body,
43    NewPart,
44    Finished,
45    Failed(Error),
46}
47
48/// Streaming, zero-copy `multipart/form-data` parser.
49///
50/// `LINE` is the size of the only buffer the parser owns: a part header line
51/// (including its CRLF) must fit into it. `MAX_HEADERS` bounds the number of
52/// header lines per part and costs no memory.
53///
54/// `size_of::<Parser<LINE, _>>() == LINE + 5 * size_of::<usize>()` (asserted by a
55/// test): the boundary slice, the line length, the header counter and the state.
56#[derive(Debug, Clone)]
57pub struct Parser<'b, const LINE: usize = 512, const MAX_HEADERS: usize = 16> {
58    boundary: &'b [u8],
59    state: State,
60    line: [u8; LINE],
61    line_len: usize,
62    header_count: usize,
63}
64
65impl<'b, const LINE: usize, const MAX_HEADERS: usize> Parser<'b, LINE, MAX_HEADERS> {
66    /// Creates a parser for `boundary` (rule 1), which is borrowed, not copied.
67    ///
68    /// # Errors
69    /// [`Error::Boundary`] if `boundary` is not 1–70 RFC 2046 `bchars` ending in a
70    /// non-space byte.
71    pub fn new(boundary: &'b [u8]) -> Result<Self, Error> {
72        if !is_valid_boundary(boundary) {
73            return Err(Error::Boundary);
74        }
75        Ok(Self {
76            boundary,
77            state: State::Preamble { at_start: true },
78            line: [0; LINE],
79            line_len: 0,
80            header_count: 0,
81        })
82    }
83
84    /// Creates a parser from a `Content-Type` header value such as
85    /// `multipart/form-data; boundary=----x` (rule 1). The boundary is borrowed
86    /// from `content_type`, quoted or unquoted.
87    ///
88    /// # Errors
89    /// [`Error::Boundary`] if the value is not a `multipart/*` media type with a
90    /// valid `boundary` parameter.
91    pub fn from_content_type(content_type: &'b [u8]) -> Result<Self, Error> {
92        match boundary_param(content_type) {
93            Some(b) => Self::new(b),
94            None => Err(Error::Boundary),
95        }
96    }
97
98    /// The boundary this parser was created with.
99    #[must_use]
100    pub const fn boundary(&self) -> &'b [u8] {
101        self.boundary
102    }
103
104    /// The smallest `input.len()` that guarantees progress on every call
105    /// (`boundary.len() + MAX_PADDING + 6`, see DESIGN.md §9).
106    #[must_use]
107    pub const fn min_buffer(&self) -> usize {
108        self.boundary.len().saturating_add(LOOKAHEAD_EXTRA)
109    }
110
111    /// `true` once `Finished` has been returned.
112    #[must_use]
113    pub const fn is_finished(&self) -> bool {
114        matches!(self.state, State::Finished)
115    }
116
117    /// Consumes a prefix of `input` and yields at most one event.
118    ///
119    /// The parser may consume fewer bytes than given (a possible delimiter prefix
120    /// at the end is held back): drop the first `consumed` bytes, append new ones
121    /// and call again. `eof = true` means no more input will follow.
122    ///
123    /// Progress guarantee: if `input.len() >= self.min_buffer()` or `eof`, every
124    /// call consumes ≥ 1 byte, returns an event, or returns an error. After an
125    /// error every call returns the same error; after `Finished` every call
126    /// returns `(input.len(), Some(Finished))`.
127    ///
128    /// # Errors
129    /// See [`Error`]; the parser is unusable afterwards.
130    pub fn parse<'i>(
131        &mut self,
132        input: &'i [u8],
133        eof: bool,
134    ) -> Result<(usize, Option<Event<'i, '_>>), Error> {
135        let (consumed, ev) = self.step(input, eof)?;
136        Ok((consumed, ev.map(|ev| self.materialize(input, ev))))
137    }
138
139    /// `parse` without the borrowed event: the event description `Ev` holds only
140    /// indices, which lets adapters store it and build the `Event` later.
141    pub(crate) fn step(&mut self, input: &[u8], eof: bool) -> Result<(usize, Option<Ev>), Error> {
142        let step = match self.state {
143            State::NewPart => {
144                self.state = State::Headers;
145                return Ok((0, Some(Ev::PartStart)));
146            }
147            State::Finished => return Ok((input.len(), Some(Ev::Finished))),
148            State::Failed(e) => Err(e),
149            _ if input.is_empty() && eof => Err(Error::UnexpectedEof),
150            _ if input.is_empty() => Ok((0, None)),
151            State::Preamble { at_start } => Ok(self.preamble(input, eof, at_start)),
152            State::Headers => self.headers(input),
153            State::Body => Ok(self.body(input, eof)),
154        };
155        step.inspect_err(|&e| self.state = State::Failed(e))
156    }
157
158    /// Turns an event description into the borrowed [`Event`]. `input` must be
159    /// the slice that was passed to the `step` that produced `ev`.
160    pub(crate) fn materialize<'i>(&self, input: &'i [u8], ev: Ev) -> Event<'i, '_> {
161        match ev {
162            Ev::PartStart => Event::PartStart,
163            Ev::HeadersEnd => Event::HeadersEnd,
164            Ev::PartEnd => Event::PartEnd,
165            Ev::Finished => Event::Finished,
166            Ev::Data { end } => Event::Data(input.get(..end).unwrap_or(input)),
167            Ev::Header {
168                name_end,
169                value_start,
170                value_end,
171            } => Event::Header {
172                name: self.line.get(..name_end).unwrap_or(&[]),
173                value: self.line.get(value_start..value_end).unwrap_or(&[]),
174            },
175        }
176    }
177
178    /// §10 Preamble. `input` is non-empty.
179    fn preamble(&mut self, input: &[u8], eof: bool, at_start: bool) -> (usize, Option<Ev>) {
180        let (consumed, ev) = match scan(input, eof, at_start, self.boundary) {
181            Found::Delim { at, len, close } => {
182                let (state, ev) = if close {
183                    (State::Finished, Ev::Finished)
184                } else {
185                    (State::Headers, Ev::PartStart)
186                };
187                self.state = state;
188                return (at.saturating_add(len), Some(ev));
189            }
190            Found::HeldBack { at } => (at, None),
191            Found::Nothing => (input.len(), None),
192        };
193        if consumed > 0 {
194            self.state = State::Preamble { at_start: false };
195        }
196        (consumed, ev)
197    }
198
199    /// §10 Headers. `input` is non-empty; `line_len` and `header_count` are 0 at
200    /// the start of every part.
201    fn headers(&mut self, input: &[u8]) -> Result<(usize, Option<Ev>), Error> {
202        let lf = input.iter().position(|&b| b == b'\n');
203        let take = lf.map_or(input.len(), |i| i.saturating_add(1));
204        let chunk = input.get(..take).unwrap_or(input);
205        let new_len = self.line_len.saturating_add(chunk.len());
206        if new_len > LINE {
207            return Err(Error::HeaderTooLong); // 5.1
208        }
209        for (dst, &src) in self.line.iter_mut().skip(self.line_len).zip(chunk) {
210            *dst = src;
211        }
212        self.line_len = new_len;
213        if lf.is_none() {
214            return Ok((take, None));
215        }
216        // A complete line is in `self.line[..new_len]`; it is consumed either way.
217        self.line_len = 0;
218        let line = self.line.get(..new_len).unwrap_or(&[]);
219        let Some(content) = line.strip_suffix(b"\r\n") else {
220            return Err(Error::Header); // 5.2 bare LF
221        };
222        if content.is_empty() {
223            self.state = State::Body; // 5.3
224            self.header_count = 0;
225            return Ok((take, Some(Ev::HeadersEnd)));
226        }
227        self.header_count = self.header_count.saturating_add(1);
228        if self.header_count > MAX_HEADERS {
229            return Err(Error::TooManyHeaders); // 5.4
230        }
231        let Some(colon) = content.iter().position(|&b| b == b':') else {
232            return Err(Error::Header); // 5.5
233        };
234        if !is_token(content.get(..colon).unwrap_or(&[])) {
235            return Err(Error::Header); // 5.5
236        }
237        if content.contains(&b'\r') {
238            return Err(Error::Header); // 5.6
239        }
240        let value_from = colon.saturating_add(1);
241        let (start, end) = ows_trimmed_range(content.get(value_from..).unwrap_or(&[]));
242        Ok((
243            take,
244            Some(Ev::Header {
245                name_end: colon,
246                value_start: value_from.saturating_add(start),
247                value_end: value_from.saturating_add(end),
248            }),
249        ))
250    }
251
252    /// §10 Body. `input` is non-empty.
253    fn body(&mut self, input: &[u8], eof: bool) -> (usize, Option<Ev>) {
254        let end = match scan(input, eof, false, self.boundary) {
255            Found::Delim { at: 0, len, close } => {
256                self.state = if close {
257                    State::Finished
258                } else {
259                    State::NewPart
260                };
261                return (len, Some(Ev::PartEnd));
262            }
263            Found::Delim { at, .. } | Found::HeldBack { at } => at,
264            Found::Nothing => input.len(),
265        };
266        (end, (end > 0).then_some(Ev::Data { end }))
267    }
268}
269
270/// Borrow-free description of an event; turned into an [`Event`] by
271/// `Parser::materialize`.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub(crate) enum Ev {
274    PartStart,
275    Header {
276        name_end: usize,
277        value_start: usize,
278        value_end: usize,
279    },
280    HeadersEnd,
281    Data {
282        end: usize,
283    },
284    PartEnd,
285    Finished,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289enum Found {
290    /// A resolved delimiter starting at `at` with total length `len`.
291    Delim { at: usize, len: usize, close: bool },
292    /// A candidate at `at` that needs more bytes (never with `eof`).
293    HeldBack { at: usize },
294    /// No delimiter (`classify`: definitely not one here; `scan`: none anywhere).
295    Nothing,
296}
297
298/// Rule 3: classifies `s` against `lead ++ boundary` followed by a part or close
299/// suffix. `HeldBack` means undecidable with the bytes available.
300fn classify(s: &[u8], lead: &[u8], boundary: &[u8]) -> Found {
301    let full = lead.len().saturating_add(boundary.len());
302    if !s
303        .iter()
304        .zip(lead.iter().chain(boundary))
305        .all(|(a, b)| a == b)
306    {
307        return Found::Nothing;
308    }
309    let Some(tail) = s.get(full..) else {
310        return Found::HeldBack { at: 0 };
311    };
312    let pad = tail
313        .iter()
314        .take(MAX_PADDING + 1)
315        .take_while(|&&b| is_ows(b))
316        .count();
317    let (len, close) = match (
318        tail.first(),
319        tail.get(1),
320        tail.get(pad),
321        tail.get(pad.saturating_add(1)),
322    ) {
323        (Some(b'-'), Some(b'-'), ..) => (2, true),
324        (None | Some(b'-'), None, ..) | (_, _, None | Some(b'\r'), None) if pad <= MAX_PADDING => {
325            return Found::HeldBack { at: 0 }
326        }
327        (Some(b'-'), ..) => return Found::Nothing,
328        (.., Some(b'\r'), Some(b'\n')) if pad <= MAX_PADDING => (pad.saturating_add(2), false),
329        _ => return Found::Nothing,
330    };
331    Found::Delim {
332        at: 0,
333        len: full.saturating_add(len),
334        close,
335    }
336}
337
338/// §9: finds and resolves the first candidate delimiter in `input`. With
339/// `allow_d0` the dash-boundary form is tried at offset 0 first.
340fn scan(input: &[u8], eof: bool, allow_d0: bool, boundary: &[u8]) -> Found {
341    let mut from = 0usize;
342    let mut try_d0 = allow_d0 && input.first() == Some(&b'-');
343    loop {
344        let (at, lead): (usize, &[u8]) = if try_d0 {
345            (0, b"--")
346        } else {
347            match input
348                .get(from..)
349                .and_then(|r| r.iter().position(|&b| b == b'\r'))
350            {
351                Some(off) => (from.saturating_add(off), b"\r\n--"),
352                None => return Found::Nothing,
353            }
354        };
355        try_d0 = false;
356        match classify(input.get(at..).unwrap_or(&[]), lead, boundary) {
357            Found::Delim { len, close, .. } => return Found::Delim { at, len, close },
358            Found::HeldBack { .. } if !eof => return Found::HeldBack { at },
359            _ => from = at.saturating_add(1),
360        }
361    }
362}
363
364/// Extracts the `boundary` parameter from a `multipart/*` media type (rule 1).
365fn boundary_param(ct: &[u8]) -> Option<&[u8]> {
366    let slash = ct.iter().position(|&b| b == b'/')?;
367    let (mtype, rest) = ct.split_at_checked(slash)?;
368    if !trim_ows(mtype).eq_ignore_ascii_case(b"multipart") {
369        return None;
370    }
371    let rest = rest.get(1..)?;
372    let semi = rest.iter().position(|&b| b == b';').unwrap_or(rest.len());
373    let (subtype, mut params) = rest.split_at_checked(semi)?;
374    if !is_token(trim_ows(subtype)) {
375        return None;
376    }
377    loop {
378        let (attr, value, quoted, rest) = param(params.strip_prefix(b";")?).ok()??;
379        if !quoted && !is_token(value) {
380            return None;
381        }
382        if attr.eq_ignore_ascii_case(b"boundary") {
383            return Some(value);
384        }
385        params = rest;
386    }
387}