Skip to main content

s3s_multipart/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023-2026 The s3s Authors
3
4//! Asynchronous streaming parser for `multipart/form-data`.
5//!
6//! The parser consumes any [`futures_core::Stream`] of [`bytes::Bytes`]
7//! chunks and yields individual parts together with their headers and data.
8//!
9//! # Compatibility notes
10//!
11//! This crate follows RFC 2046 section 5.1 and RFC 7578, with a small set
12//! of deliberate differences. Every item in this list is documented at the
13//! corresponding API item and has a regression test.
14//!
15//! - Parts carrying more header fields than the parser reads are accepted:
16//!   the surplus fields are ignored. RFC 7578 section 4.8 defines exactly
17//!   `Content-Disposition`, `Content-Type` and the deprecated
18//!   `Content-Transfer-Encoding`, requires any other header field to be
19//!   ignored, and a conforming part carries at most those three — so for a
20//!   conforming part the limit can only ever drop fields that such a part
21//!   cannot have. A part that carries more than three fields *and* puts
22//!   `Content-Disposition` after them loses that field, because the parser
23//!   reads the first three fields and ignores the rest: section 4.8 asks for
24//!   the surplus fields to be ignored, not for the parser to keep looking.
25//! - A preamble before the first boundary and transport padding after a
26//!   boundary line are accepted.
27//! - Field lines are parsed with the strict header grammar: a folded field
28//!   (a continuation line starting with optional whitespace) and a field name
29//!   with leading optional whitespace are both rejected with
30//!   [`Error::InvalidFormat`], because the block is read by `httparse` rather
31//!   than by the lenient line-folding rules of RFC 7230 section 3.2.4, which
32//!   let a server reject a folded field anyway.
33//! - Empty part headers and empty part bodies are valid.
34//! - `Content-Disposition` parsing is tolerant: parameter names and the
35//!   `form-data` token are case-insensitive, parameter order is free, and
36//!   both quoted and token values are accepted.
37//! - `Content-Disposition` `name` and `filename` are returned as raw bytes:
38//!   no UTF-8 validation is performed, and callers decide how to convert or
39//!   reject invalid sequences. Parsing is best-effort, so a malformed
40//!   parameter tail is ignored instead of failing the parse.
41//! - Quoted `Content-Disposition` parameter values keep their backslash
42//!   escape sequences: `quoted-pair` is not decoded (`\"` stays `\"`), because
43//!   returned values are always sub-slices of the header value. Callers that
44//!   need the decoded value have to decode it themselves.
45//! - The boundary is validated when it is constructed, according to RFC 2046
46//!   section 5.1.1 (1 to 70 characters, only `bchars`, and no trailing
47//!   whitespace). A boundary that other parsers accept but RFC 2046 forbids is
48//!   rejected, and there is no unchecked constructor.
49//! - A part data stream taken with `take_data_stream` ends at the closing
50//!   delimiter without interpreting what follows. Converting it with
51//!   `into_final` yields a `FinalPartDataStream` that enforces the strict
52//!   closing delimiter and rejects an epilogue, because callers need an
53//!   exact content length.
54//! - The internal buffer limit bounds the part header block, not the delivery:
55//!   a block that ends within the limit is accepted whichever chunk carried it,
56//!   and one that grows past the limit without a terminator is rejected with
57//!   `HeaderSizeExceeded`. Data chunks yielded while streaming part data are
58//!   never retained and are not charged to the limit, and the parse never
59//!   depends on where the transport split the body. The buffer can still hold
60//!   one arriving chunk beyond the limit, because the bytes that follow the
61//!   terminator are retained until the header block is consumed.
62//! - A chunk that carries no bytes is not progress and is not handed out:
63//!   the parser skips such chunks, yields to the executor after a bounded
64//!   number of them within one poll, and reports a stream failure once a
65//!   consecutive run grows past a fixed bound, so a stream that never sends
66//!   anything cannot keep the parser busy forever.
67
68#![deny(missing_docs)]
69#![deny(clippy::expect_used, clippy::panic, clippy::unreachable, clippy::unwrap_used)]
70#![deny(clippy::missing_panics_doc)]
71
72// Internal visibility: every module below is private and none of them is
73// re-exported wholesale, so items that live in them are declared `pub` — for a
74// crate-root child module that is the same reach as `pub(super)`, just shorter.
75// Members of types that ARE re-exported must stay `pub(super)`: an inherent
76// method or a field of a public type is public API even when its impl block
77// lives in a private module, which rustc reports as `missing_docs` (the crate
78// denies it) and as `private_interfaces` when the member type is internal.
79mod error;
80pub use self::error::Error;
81
82mod boundary;
83pub use self::boundary::Boundary;
84
85mod content_disposition;
86pub use self::content_disposition::{ContentDisposition, parse_content_disposition};
87
88mod buffer;
89
90mod delimiter;
91mod header;
92mod utils;
93
94mod multipart;
95pub use self::multipart::Multipart;
96
97mod part;
98pub use self::part::Part;
99
100mod part_data_stream;
101pub use self::part_data_stream::PartDataStream;
102
103mod final_part_data_stream;
104pub use self::final_part_data_stream::FinalPartDataStream;