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;