Skip to main content

base_web_content/
content_parser.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::ParameterRules;
5use crate::form::Form;
6use crate::{
7    Accepted, ContentError, ContentErrorKind, ContentEvent, ContentFormat, ContentHandles,
8    ContentParsed, Rejected,
9};
10use base_web_media_type::MediaType;
11use std::fmt;
12use std::marker::PhantomData;
13
14mod multipart_body;
15mod url_encoded_body;
16
17use multipart_body::MultipartBody;
18use url_encoded_body::UrlEncodedBody;
19
20/// A sans-I/O parser for a request's content, into the struct `T` declares, fed bytes as they arrive.
21///
22/// The format is chosen by the request's media type. Every field and upload is matched against the declaration as it arrives: a name the declaration does not accept, and any limit exceeded, stop parsing with a fatal [`ContentError`] before any more content is read. Fields that arrived but cannot be accepted, and required fields that did not, are collected as [`Violation`](crate::Violation)s and returned together when parsing finishes.
23///
24/// The caller owns the buffer, as with `base-web-multipart`'s `MultipartParser`. 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. When the input ends, the caller gives what remains to [`Self::parse_end`] until it reports [`ContentEvent::End`]; then [`Self::finish`] returns the struct, or why it was refused. The result, events, violations, and errors are the same however the body is split, except that an upload's content may arrive in different pieces.
25///
26/// Fields other than uploads are buffered within their limits. An accepted upload's content is reported to the caller, borrowed from the input, to store wherever it chooses; when the upload ends, the caller gives the parser a handle of type `H` for it, which the result holds in its [`Upload`](crate::Upload). Uploads are stored before the whole body has been validated, so when parsing fails, [`Rejected`] returns every handle the caller gave, for the caller to remove what it stored. An upload whose content was still arriving when parsing failed has no handle, and the caller must remove what it stored of it. The handles are only returned by [`Self::finish`], so the caller must finish the parser however parsing ends: after a fatal error from [`Self::parse`] or [`Self::parse_end`], which [`Self::finish`] reports again, and when the input stops early, such as when the client disconnects or the request is cancelled, which it refuses as [`ContentErrorKind::UnexpectedEnd`]. Dropping the parser drops the handles it holds.
27///
28/// ```
29/// use base_web_content::{Content, ContentEvent, ContentParser, Upload};
30/// use base_web_media_type::MediaType;
31///
32/// #[derive(Content)]
33/// struct Comment {
34///     body: String,
35///     #[content(accept("text/*"))]
36///     attachment: Option<Upload<usize>>,
37/// }
38///
39/// let media_type = MediaType::parse("multipart/form-data; boundary=b")?;
40/// let body = b"--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nHello!\r\n--b\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSome notes.\r\n--b--\r\n";
41///
42/// let mut parser = ContentParser::<Comment, usize>::new(Some(&media_type))?;
43/// let mut stored: Vec<Vec<u8>> = Vec::new();
44/// // The whole body is in memory, so its end is known.
45/// let mut input = &body[..];
46/// loop {
47///     // After a fatal error, `finish` reports it, with every handle given so far, for the caller to remove what it stored.
48///     let Ok(parsed) = parser.parse_end(input) else { break };
49///     input = &input[parsed.consumed..];
50///     match parsed.event {
51///         // Begin storing an upload, such as in a temporary file.
52///         ContentEvent::Upload(_) => stored.push(Vec::new()),
53///         ContentEvent::UploadContent(content) => stored.last_mut().unwrap().extend_from_slice(content),
54///         // Give the parser a handle for what was stored.
55///         ContentEvent::UploadEnd => parser.store(stored.len() - 1),
56///         ContentEvent::End => break,
57///         ContentEvent::Incomplete => unreachable!("parse_end is given the rest of the input"),
58///     }
59/// }
60///
61/// let comment = parser.finish().expect("the comment is accepted").content;
62/// assert_eq!(comment.body, "Hello!");
63/// let attachment = comment.attachment.expect("an attachment");
64/// assert_eq!(stored[*attachment.handle()], b"Some notes.");
65/// # Ok::<(), Box<dyn std::error::Error>>(())
66/// ```
67pub struct ContentParser<T, H = ()> {
68    form: Form<H>,
69    format: Format,
70    body_size_limit: usize,
71    /// The bytes consumed before the current call, which never exceed the body size limit.
72    offset: usize,
73    /// The error the parser reported, or will report on the next call, which it reports for every later call.
74    failed: Option<ContentError>,
75    /// Whether the parser has reported the end of the body.
76    ended: bool,
77    declaration: PhantomData<fn() -> T>,
78}
79
80/// The format being parsed, which drives the form with the fields it finds.
81enum Format {
82    MultipartFormData(Box<MultipartBody>),
83    UrlEncoded(UrlEncodedBody),
84}
85
86/// What one step of a format found: the event, the bytes it consumed, and an error to report on the next call, after content within a limit that the bytes after it exceed.
87struct Stepped<'a> {
88    consumed: usize,
89    event: ContentEvent<'a>,
90    deferred: Option<ContentError>,
91}
92
93impl<'a> Stepped<'a> {
94    fn new(consumed: usize, event: ContentEvent<'a>) -> Self {
95        Self {
96            consumed,
97            event,
98            deferred: None,
99        }
100    }
101}
102
103impl<T: ContentHandles<H>, H> ContentParser<T, H> {
104    /// A parser for content with this media type, the request's `Content-Type`, if it has one.
105    ///
106    /// `multipart/form-data` is parsed with `base-web-multipart`, reading part names and filenames under the [`ParameterRules`] its media type gives, which [`Self::with_parameter_rules`] can override. `application/x-www-form-urlencoded` is parsed with `base-web-url`, in UTF-8 or US-ASCII as its `charset` parameter says.
107    ///
108    /// # Errors
109    ///
110    /// A [`ContentError`] at offset 0 when the media type is missing, is not a format this crate parses, is a format `T` does not accept, is `multipart/form-data` without a valid boundary, or is `application/x-www-form-urlencoded` with a `charset` other than `UTF-8` or `US-ASCII`.
111    pub fn new(media_type: Option<&MediaType>) -> Result<Self, ContentError> {
112        let error = |kind| ContentError::new(kind, 0);
113        let media_type = media_type.ok_or(error(ContentErrorKind::MissingMediaType))?;
114        let format = ContentFormat::from_media_type(media_type)
115            .ok_or(error(ContentErrorKind::UnsupportedMediaType))?;
116        let declaration = &T::DECLARATION;
117        let body_size_limit = declaration
118            .body_size_limit(format)
119            .ok_or(error(ContentErrorKind::UnacceptedFormat))?;
120        let format = match format {
121            ContentFormat::MultipartFormData => Format::MultipartFormData(Box::new(
122                MultipartBody::new(media_type, &declaration.limits).map_err(error)?,
123            )),
124            ContentFormat::UrlEncoded => {
125                Format::UrlEncoded(UrlEncodedBody::new(media_type, declaration).map_err(error)?)
126            }
127        };
128        Ok(Self {
129            form: Form::new(declaration),
130            format,
131            body_size_limit,
132            offset: 0,
133            failed: None,
134            ended: false,
135            declaration: PhantomData,
136        })
137    }
138}
139
140impl<T, H> ContentParser<T, H> {
141    /// The parser, reading part names and filenames under these rules rather than those of the media type. URL-encoded content has no parameters, so its parser is unchanged.
142    // SECURITY: Under `ParameterRules::Mime`, an extended `name*` or `filename*` takes the place of `name` or `filename`, as RFC 2231 says, so a reader that ignores extended parameters can see another field name or filename for the same part. This is accepted because the rules are an explicit opt-in, for clients known to send RFC 2231 parameters, while the default for `multipart/form-data`, `ParameterRules::FormData`, refuses every extended parameter, as RFC 7578 requires.
143    pub fn with_parameter_rules(mut self, rules: ParameterRules) -> Self {
144        match &mut self.format {
145            Format::MultipartFormData(body) => body.set_parameter_rules(rules),
146            Format::UrlEncoded(_) => {}
147        }
148        self
149    }
150
151    /// The format being parsed.
152    pub fn format(&self) -> ContentFormat {
153        match self.format {
154            Format::MultipartFormData(_) => ContentFormat::MultipartFormData,
155            Format::UrlEncoded(_) => ContentFormat::UrlEncoded,
156        }
157    }
158
159    /// The largest body the declaration could need in this format. Parsing refuses a longer body with [`ContentErrorKind::BodySizeLimit`].
160    pub fn body_size_limit(&self) -> usize {
161        self.body_size_limit
162    }
163
164    /// 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 [`ContentEvent::Incomplete`], since a multipart part's header section, and a URL-encoded name, are read whole.
165    pub fn minimum_buffer_size(&self) -> usize {
166        match &self.format {
167            Format::MultipartFormData(body) => body.minimum_buffer_size(),
168            Format::UrlEncoded(body) => body.minimum_buffer_size(),
169        }
170    }
171
172    /// Parse the next event from `input`, which must begin with the bytes the previous call did not consume, while more input may follow. Once the input has ended, call [`Self::parse_end`] instead.
173    ///
174    /// # Errors
175    ///
176    /// A fatal [`ContentError`] when the body is malformed, has a name the declaration does not accept, or exceeds a limit. Once the parser reports an error, it reports the same error for every later call.
177    ///
178    /// # Panics
179    ///
180    /// When an upload ended and its handle has not been given with [`Self::store`].
181    pub fn parse<'a>(&mut self, input: &'a [u8]) -> Result<ContentParsed<'a>, ContentError> {
182        self.run(input, false)
183    }
184
185    /// Parse the next event from `input`, which must be all of the input that remains after the bytes already consumed, once the request's body has ended. Call it until it reports [`ContentEvent::End`], handling uploads as [`Self::parse`] reports them; it never reports [`ContentEvent::Incomplete`]. URL-encoded content ends only where its input does, so its last pair is only complete here.
186    ///
187    /// # Errors
188    ///
189    /// As for [`Self::parse`], and [`ContentErrorKind::UnexpectedEnd`] when the content is not complete, such as a multipart body without its closing boundary. An error in what follows a multipart body's closing boundary, the epilogue, is reported by the call after the one that reports [`ContentEvent::End`], and by [`Self::finish`].
190    ///
191    /// # Panics
192    ///
193    /// When an upload ended and its handle has not been given with [`Self::store`].
194    pub fn parse_end<'a>(&mut self, input: &'a [u8]) -> Result<ContentParsed<'a>, ContentError> {
195        self.run(input, true)
196    }
197
198    fn run<'a>(&mut self, input: &'a [u8], end: bool) -> Result<ContentParsed<'a>, ContentError> {
199        if let Some(error) = self.failed {
200            return Err(error);
201        }
202        assert!(
203            !self.form.is_unstored(),
204            "an upload ended, and its handle must be given to `store` before parsing on"
205        );
206        // Bytes past the largest body the declaration could need are never parsed, so a longer body is refused at that byte, however it is split.
207        let allowed = input.len().min(self.body_size_limit - self.offset);
208        // A body cut short at the maximum has not ended there, so it is parsed as if more may follow, and refused below.
209        let ended_input = end;
210        let end = end && allowed == input.len();
211        let (form, input_allowed, offset) = (&mut self.form, &input[..allowed], self.offset);
212        let stepped = match &mut self.format {
213            Format::MultipartFormData(body) => {
214                body.parse(form, input_allowed, offset, end, ended_input)
215            }
216            Format::UrlEncoded(body) => body.parse(form, input_allowed, offset, end),
217        };
218        let beyond =
219            ContentError::new(ContentErrorKind::BodySizeLimit, self.body_size_limit as u64);
220        let ended = self.ended;
221        let stepped = stepped.and_then(|mut stepped| {
222            if allowed == input.len() {
223                return Ok(stepped);
224            }
225            match stepped.event {
226                // The parser needs more than the bytes allowed, or has consumed every byte after the end it reported, and more follow.
227                ContentEvent::Incomplete => Err(beyond),
228                ContentEvent::End if ended && stepped.consumed == allowed => Err(beyond),
229                // The end is reported where the body reaches it, and the bytes beyond the maximum refused after it, as they would be if the body were split there; once the input has ended, the next call would be the last, so the error is reported then.
230                ContentEvent::End if ended_input => {
231                    stepped.deferred = stepped.deferred.or(Some(beyond));
232                    Ok(stepped)
233                }
234                _ => Ok(stepped),
235            }
236        });
237        match stepped {
238            Ok(stepped) => {
239                self.offset += stepped.consumed;
240                self.failed = stepped.deferred;
241                self.ended |= stepped.event == ContentEvent::End;
242                Ok(ContentParsed {
243                    consumed: stepped.consumed,
244                    event: stepped.event,
245                })
246            }
247            Err(error) => {
248                self.failed = Some(error);
249                Err(error)
250            }
251        }
252    }
253
254    /// Give the handle for what the caller stored of the upload that just ended.
255    ///
256    /// # Panics
257    ///
258    /// When no upload has ended since the last handle was given.
259    pub fn store(&mut self, handle: H) {
260        self.form.store(handle);
261    }
262
263    /// Finish once the input has ended and [`Self::parse_end`] has reported [`ContentEvent::End`]: the declared struct, or why it was refused, with every handle the caller gave. Call it however parsing ended, after a fatal error or when the input stopped early too, since only it returns the handles.
264    ///
265    /// # Errors
266    ///
267    /// [`Rejected`], with a fatal [`ContentError`] when parsing failed or the body ended early, or every [`Violation`](crate::Violation) when it parsed but some fields cannot be accepted.
268    // SECURITY: An upload whose content was still arriving when parsing failed, or that ended without the caller giving its handle, has no handle in `Rejected`. This does not leak storage: the caller began storing it at `ContentEvent::Upload` and still holds that storage, since it never gave the parser a handle for it, and the documentation above and the guide tell it to remove what it stored of an upload still arriving.
269    pub fn finish(self) -> Result<Accepted<T, H>, Rejected<H>>
270    where
271        T: ContentHandles<H>,
272    {
273        let error = self.failed.or_else(|| match &self.format {
274            Format::MultipartFormData(body) => body.finish().err(),
275            Format::UrlEncoded(body) => body.finish(self.offset).err(),
276        });
277        self.form.finish(error)
278    }
279}
280
281/// Shows the format, the offset, and any error, but no content.
282impl<T, H> fmt::Debug for ContentParser<T, H> {
283    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284        formatter
285            .debug_struct("ContentParser")
286            .field("format", &self.format())
287            .field("offset", &self.offset)
288            .field("body_size_limit", &self.body_size_limit)
289            .field("failed", &self.failed)
290            .finish_non_exhaustive()
291    }
292}