Skip to main content

form_net/
lib.rs

1//! # form_net
2//!
3//! Parses what a browser sends when a form is submitted:
4//! `application/x-www-form-urlencoded` fields and `multipart/form-data`
5//! uploads. It does no I/O: feed it bytes, get fields and parts.
6//!
7//! Every budget (field count, name and value length, part count, part
8//! header size, file count and size, total size) is checked before the bytes
9//! it governs are buffered. Input that two parsers could read differently is
10//! refused with a precise error, never repaired. No error, `Debug`, or
11//! `Display` output ever contains a field value or a file name.
12//!
13//! ## Tier 1
14//!
15//! ```
16//! // A urlencoded body
17//! let form = form_net::parse_urlencoded(b"email=a%40b.example&remember=on")?;
18//! assert_eq!(form.field("email"), Some("a@b.example"));
19//!
20//! // A query string
21//! let query = form_net::parse_query(b"?q=rust+forms&page=2")?;
22//! assert_eq!(query.field("q"), Some("rust forms"));
23//!
24//! // Building one
25//! assert_eq!(form_net::serialize_urlencoded([("q", "rust forms")]), "q=rust+forms");
26//!
27//! // A buffered multipart body
28//! let body = b"--X\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nHoliday\r\n--X--";
29//! let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
30//! assert_eq!(mp.field("title"), Some("Holiday"));
31//! # Ok::<(), form_net::FormError>(())
32//! ```
33//!
34//! A typed form, from either encoding:
35//!
36//! ```
37//! use form_net::FromForm;
38//!
39//! form_net::form! {
40//!     pub struct Login {
41//!         pub email: String,
42//!         pub password: form_net::Secret,
43//!         pub remember: bool,
44//!     }
45//!     ignore = ["csrf_token"];
46//! }
47//!
48//! let body = b"email=a%40b.example&password=pw&remember=on&csrf_token=t";
49//! let login = Login::from_body(b"application/x-www-form-urlencoded", body)?;
50//! assert_eq!((login.email.as_str(), login.remember), ("a@b.example", true));
51//! # Ok::<(), form_net::FormError>(())
52//! ```
53//!
54//! An upload read from any `std::io::Read`, large files spilled to private
55//! temporary files that are deleted on drop (feature `fs`):
56//!
57//! ```
58//! # #[cfg(feature = "fs")] {
59//! let body = b"--X\r\nContent-Disposition: form-data; name=doc; filename=a.txt\r\n\r\nhello\r\n--X--";
60//! let upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
61//! assert_eq!(upload.file("doc").map(|f| f.len()), Some(5));
62//! # }
63//! # Ok::<(), form_net::FormError>(())
64//! ```
65//!
66//! ## Tiers 2 and 3
67//!
68//! Explicit [`Limits`] and [`Policy`] per route (`parse_urlencoded_with`,
69//! `parse_multipart_with`); an [`Inspector`] that sees every part head, the
70//! first bytes of every file, and every field before anything is stored;
71//! a [`Sink`] that decides where file bytes go, driven by the [`Driver`]; the
72//! allocation-free core ([`raw_pairs`], [`decode_into`], [`encode_into`],
73//! [`UrlencodedParser`], [`Boundary`], [`MultipartParser`],
74//! [`encode_part_head`]), which builds without `alloc`.
75//!
76//! ## Features
77//!
78//! | Feature | Default | Adds |
79//! |---|---|---|
80//! | (none) | | the allocation-free core; `no_std` without `alloc` |
81//! | `alloc` | | owned results, the Tier 1 functions, `form!`, `Driver`, `MemorySink` |
82//! | `std` | yes | `MultipartReader`, `MultipartWriter`, `FormError::Io`, `FormError::Random`; implies `alloc` |
83//! | `random` | | `Boundary::generate`, `MultipartWriter::new`; implies `std` |
84//! | `fs` | | `Store`, `StoredFile`, `Upload`, `UploadSession`, `read_multipart`, `SpillQuota`, `sweep`, `FileName::sanitized`; implies `random` |
85//! | `unicode` | | `FileName::nfc`, and NFC in `FileName::sanitized`; implies `alloc` |
86//!
87//! The full contract is `docs/API.md` in the
88//! [repository](https://github.com/jamesgober/form-net), with
89//! `docs/ARCHITECTURE.md`, `docs/PLATFORM-NOTES.md`, and
90//! `docs/PERFORMANCE.md` (measurements against the other Rust form parsers).
91//! This release (0.11) is feature complete, optimised, hardened, polished, and in beta.
92
93#![no_std]
94#![doc(html_root_url = "https://docs.rs/form-net")]
95#![cfg_attr(docsrs, feature(doc_cfg))]
96#![forbid(unsafe_code)]
97#![deny(missing_docs)]
98#![cfg_attr(
99    not(test),
100    deny(
101        clippy::unwrap_used,
102        clippy::expect_used,
103        clippy::panic,
104        clippy::indexing_slicing,
105        clippy::arithmetic_side_effects,
106        clippy::todo,
107        clippy::unimplemented,
108        clippy::unreachable,
109        clippy::dbg_macro,
110        clippy::print_stdout,
111        clippy::print_stderr
112    )
113)]
114
115#[cfg(any(feature = "alloc", test))]
116extern crate alloc;
117#[cfg(any(feature = "std", test))]
118extern crate std;
119
120#[cfg(target_has_atomic = "32")]
121mod digest;
122mod error;
123#[cfg(feature = "alloc")]
124mod extract;
125mod hook;
126#[cfg(feature = "std")]
127mod io;
128mod kind;
129mod limits;
130mod multipart;
131#[cfg(feature = "random")]
132mod random;
133mod scan;
134#[cfg(feature = "fs")]
135mod store;
136mod urlencoded;
137mod wipe;
138
139#[cfg(feature = "alloc")]
140#[doc(hidden)]
141pub use alloc::vec::Vec as __Vec;
142#[cfg(target_has_atomic = "32")]
143#[cfg_attr(docsrs, doc(cfg(target_has_atomic = "32")))]
144pub use digest::RefusalDigest;
145pub use error::{
146    EncodingError, ExtractError, FormError, Limit, Location, PolicyRefusal, SyntaxError,
147};
148#[cfg(feature = "alloc")]
149#[doc(hidden)]
150pub use extract::{__push_field_error, __repeated_wire_name, __unknown, __value_error};
151#[cfg(feature = "alloc")]
152#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
153pub use extract::{
154    FileData, FileValue, FormBuilder, FormErrors, FromField, FromForm, Secret, ValueError,
155};
156#[cfg(feature = "alloc")]
157#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
158pub use hook::MemorySink;
159pub use hook::{DiscardSink, Inspector, NoInspector, Refusal, Sink, TokenCheck, Verdict};
160pub use kind::FormKind;
161pub use limits::{
162    EmptyFilePolicy, ExtendedFilenamePolicy, HeaderPolicy, Limits, PercentPolicy, Policy,
163    TokenField, TokenPlacement,
164};
165pub use multipart::boundary::{Boundary, MAX_BOUNDARY_LEN, MAX_DELIMITER_LEN};
166#[cfg(feature = "alloc")]
167#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
168pub use multipart::driver::{Driver, Progress};
169pub use multipart::filename::{FileName, FileNameSource};
170#[cfg(feature = "alloc")]
171#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
172pub use multipart::owned_head::OwnedPartHead;
173pub use multipart::parser::{
174    Charset, ContentType, Headers, MultipartEvent, MultipartParser, MultipartStep, PartHead,
175    PartKind, PartSummary,
176};
177#[cfg(feature = "std")]
178#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
179pub use multipart::writer::MultipartWriter;
180pub use multipart::writer::encode_part_head;
181#[cfg(feature = "alloc")]
182#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
183pub use multipart::{
184    Multipart, Part, parse_multipart, parse_multipart_inspected, parse_multipart_with,
185};
186pub use urlencoded::decode::decode_into;
187pub use urlencoded::encode::encode_into;
188#[cfg(feature = "alloc")]
189#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
190pub use urlencoded::encode::{Serializer, serialize_urlencoded};
191pub use urlencoded::raw::{RawPair, RawPairs, raw_pairs};
192pub use urlencoded::stream::{UrlencodedEvent, UrlencodedParser, UrlencodedStep};
193#[cfg(feature = "alloc")]
194#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
195pub use urlencoded::{
196    Form, Pairs, pairs, parse_query, parse_urlencoded, parse_urlencoded_inspected,
197    parse_urlencoded_with,
198};
199
200#[cfg(feature = "std")]
201#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
202pub use io::{MultipartReader, PartReader};
203#[cfg(feature = "fs")]
204#[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
205pub use store::{
206    PersistError, SpillBudget, SpillMode, SpillQuota, Store, StoreError, StoreSink, StoreStats,
207    StoredFile, StoredReader, Upload, UploadSession, read_multipart, read_multipart_inspected,
208    read_multipart_with, sweep,
209};
210
211/// Crate version string, populated by Cargo at build time.
212pub const VERSION: &str = env!("CARGO_PKG_VERSION");
213
214#[cfg(test)]
215mod tests {
216    use super::VERSION;
217
218    #[test]
219    fn test_version_matches_manifest() {
220        assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
221        assert_eq!(VERSION.split('.').count(), 3, "expected MAJOR.MINOR.PATCH");
222    }
223
224    /// The size table of `docs/API.md` (64-bit targets), held by a test.
225    #[test]
226    #[cfg(target_pointer_width = "64")]
227    fn test_the_api_size_table_holds() {
228        use core::mem::size_of;
229        assert_eq!(size_of::<super::Boundary>(), 149);
230        assert!(size_of::<super::FormError>() <= 48);
231        assert_eq!(size_of::<super::Limits>(), 72);
232        assert_eq!(size_of::<super::RawPair<'static>>(), 48);
233        assert!(size_of::<super::MultipartParser<&mut [u8]>>() <= 384);
234        // `Limits::memory_bound` counts 1 KiB of per-request state.
235        #[cfg(feature = "alloc")]
236        assert!(size_of::<super::MultipartParser<alloc::vec::Vec<u8>>>() <= 1024);
237        #[cfg(feature = "fs")]
238        assert!(size_of::<super::UploadSession>() <= 1024);
239    }
240
241    #[test]
242    fn test_public_types_are_send_and_sync() {
243        fn send_sync<T: Send + Sync>() {}
244        send_sync::<super::Limits>();
245        send_sync::<super::Policy>();
246        send_sync::<super::Boundary>();
247        send_sync::<super::FormError>();
248        send_sync::<super::RefusalDigest<2>>();
249        send_sync::<super::MultipartParser<&mut [u8]>>();
250        #[cfg(feature = "alloc")]
251        {
252            send_sync::<super::MultipartParser<alloc::vec::Vec<u8>>>();
253            send_sync::<super::Form>();
254            send_sync::<super::Multipart<'static>>();
255            send_sync::<super::OwnedPartHead>();
256            send_sync::<super::MemorySink>();
257            send_sync::<super::Driver<alloc::vec::Vec<u8>, super::MemorySink>>();
258            send_sync::<super::FileData>();
259            send_sync::<super::Secret>();
260        }
261        #[cfg(feature = "std")]
262        send_sync::<super::MultipartReader<std::fs::File>>();
263        #[cfg(feature = "fs")]
264        {
265            send_sync::<super::Store>();
266            send_sync::<super::StoreSink>();
267            send_sync::<super::StoredFile>();
268            send_sync::<super::Upload>();
269            send_sync::<super::UploadSession>();
270        }
271    }
272}
273
274/// Every example of `docs/API.md`, compiled and run as a doctest (D-100).
275/// They use every feature, so they run with `--all-features`.
276#[cfg(all(doctest, feature = "fs", feature = "unicode"))]
277#[doc = include_str!("../docs/API.md")]
278struct ApiExamples;