Skip to main content

form_net/
limits.rs

1//! Budgets ([`Limits`]) and behaviours ([`Policy`]).
2//!
3//! Both are small `Copy` values with `const` setters, so a route's
4//! configuration is a `const` checked once at startup.
5
6use crate::error::{FormError, Limit};
7
8/// The bytes the multipart parser keeps for the `_charset_` field's value.
9pub(crate) const CHARSET_CAPTURE: usize = 32;
10
11/// Every budget form-net enforces.
12///
13/// Each budget is checked before the bytes it governs are buffered, stored,
14/// or passed on. A limit of 0 means none allowed. A limit larger than the one
15/// containing it is capped by it (the effective value is the smaller), so a
16/// route sets only the limits it cares about.
17///
18/// | Field | Default |
19/// |---|---|
20/// | `max_body_bytes` | 10 MiB |
21/// | `max_fields` | 1,000 |
22/// | `max_name_bytes` | 256 |
23/// | `max_value_bytes` | 1 MiB |
24/// | `max_text_bytes` | 2 MiB |
25/// | `max_parts` | 100 |
26/// | `max_files` | 20 |
27/// | `max_file_bytes` | 10 MiB |
28/// | `max_part_header_bytes` | 8 KiB |
29/// | `max_part_headers` | 8 |
30/// | `max_filename_bytes` | 1,024 |
31/// | `max_preamble_bytes` | 1 KiB |
32/// | `max_epilogue_bytes` | 1 KiB |
33/// | `max_padding_bytes` | 32 |
34/// | `sniff_bytes` | 512 |
35/// | `max_token_bytes` | 128 |
36///
37/// # Examples
38///
39/// ```
40/// use form_net::Limits;
41///
42/// // A login route: a small body, a few fields, no files.
43/// const LOGIN: Limits = Limits::DEFAULT
44///     .with_max_body_bytes(4 * 1024)
45///     .with_max_fields(8)
46///     .with_max_files(0);
47/// assert!(LOGIN.validate().is_ok());
48/// ```
49#[non_exhaustive]
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct Limits {
52    /// Every byte fed, both formats, preamble and epilogue included.
53    pub max_body_bytes: u64,
54    /// urlencoded pairs; multipart text fields.
55    pub max_fields: u32,
56    /// Raw (encoded) name length. Capped by `max_body_bytes`.
57    pub max_name_bytes: u32,
58    /// One urlencoded value or one multipart text field. Capped by
59    /// `max_text_bytes`.
60    pub max_value_bytes: u32,
61    /// All text values of one body together. Capped by `max_body_bytes`.
62    pub max_text_bytes: u64,
63    /// Multipart parts of either kind.
64    pub max_parts: u32,
65    /// Multipart file parts.
66    pub max_files: u32,
67    /// One file part. Capped by `max_body_bytes`.
68    pub max_file_bytes: u64,
69    /// One part's header block, CRLF CRLF included (at least 64).
70    pub max_part_header_bytes: u32,
71    /// Header lines per part.
72    pub max_part_headers: u16,
73    /// A raw `filename`, or a decoded `filename*`. Capped by
74    /// `max_part_header_bytes`.
75    pub max_filename_bytes: u32,
76    /// Bytes before the first delimiter. Capped by `max_body_bytes`.
77    pub max_preamble_bytes: u32,
78    /// Bytes after the close delimiter. Capped by `max_body_bytes`.
79    pub max_epilogue_bytes: u32,
80    /// Transport padding after a delimiter.
81    pub max_padding_bytes: u8,
82    /// The sniff window per file part (0: off). Capped by `max_file_bytes`.
83    pub sniff_bytes: u32,
84    /// The captured anti-forgery token. Capped by `max_value_bytes`.
85    pub max_token_bytes: u16,
86}
87
88macro_rules! setter {
89    ($(#[$doc:meta])* $name:ident, $field:ident, $ty:ty) => {
90        $(#[$doc])*
91        #[must_use]
92        pub const fn $name(mut self, value: $ty) -> Self {
93            self.$field = value;
94            self
95        }
96    };
97}
98
99const fn min_u64(a: u64, b: u64) -> u64 {
100    if a < b { a } else { b }
101}
102
103/// `value` as `usize`, or `usize::MAX` when it does not fit.
104#[cfg(feature = "alloc")]
105const fn to_usize(value: u64) -> usize {
106    if value > usize::MAX as u64 {
107        usize::MAX
108    } else {
109        value as usize
110    }
111}
112
113const fn add_usize(a: usize, b: usize) -> usize {
114    match a.checked_add(b) {
115        Some(v) => v,
116        None => usize::MAX,
117    }
118}
119
120impl Limits {
121    /// The defaults in the table above.
122    pub const DEFAULT: Limits = Limits {
123        max_body_bytes: 10 * 1024 * 1024,
124        max_fields: 1_000,
125        max_name_bytes: 256,
126        max_value_bytes: 1024 * 1024,
127        max_text_bytes: 2 * 1024 * 1024,
128        max_parts: 100,
129        max_files: 20,
130        max_file_bytes: 10 * 1024 * 1024,
131        max_part_header_bytes: 8 * 1024,
132        max_part_headers: 8,
133        max_filename_bytes: 1_024,
134        max_preamble_bytes: 1_024,
135        max_epilogue_bytes: 1_024,
136        max_padding_bytes: 32,
137        sniff_bytes: 512,
138        max_token_bytes: 128,
139    };
140
141    setter!(
142        /// Sets `max_body_bytes`.
143        with_max_body_bytes, max_body_bytes, u64);
144    setter!(
145        /// Sets `max_fields`.
146        with_max_fields, max_fields, u32);
147    setter!(
148        /// Sets `max_name_bytes`.
149        with_max_name_bytes, max_name_bytes, u32);
150    setter!(
151        /// Sets `max_value_bytes`.
152        with_max_value_bytes, max_value_bytes, u32);
153    setter!(
154        /// Sets `max_text_bytes`.
155        with_max_text_bytes, max_text_bytes, u64);
156    setter!(
157        /// Sets `max_parts`.
158        with_max_parts, max_parts, u32);
159    setter!(
160        /// Sets `max_files`.
161        with_max_files, max_files, u32);
162    setter!(
163        /// Sets `max_file_bytes`.
164        with_max_file_bytes, max_file_bytes, u64);
165    setter!(
166        /// Sets `max_part_header_bytes`.
167        with_max_part_header_bytes, max_part_header_bytes, u32);
168    setter!(
169        /// Sets `max_part_headers`.
170        with_max_part_headers, max_part_headers, u16);
171    setter!(
172        /// Sets `max_filename_bytes`.
173        with_max_filename_bytes, max_filename_bytes, u32);
174    setter!(
175        /// Sets `max_preamble_bytes`.
176        with_max_preamble_bytes, max_preamble_bytes, u32);
177    setter!(
178        /// Sets `max_epilogue_bytes`.
179        with_max_epilogue_bytes, max_epilogue_bytes, u32);
180    setter!(
181        /// Sets `max_padding_bytes`.
182        with_max_padding_bytes, max_padding_bytes, u8);
183    setter!(
184        /// Sets `sniff_bytes`.
185        with_sniff_bytes, sniff_bytes, u32);
186    setter!(
187        /// Sets `max_token_bytes`.
188        with_max_token_bytes, max_token_bytes, u16);
189
190    /// Refuses the values no body could satisfy, so a misconfiguration is
191    /// found at startup: a `max_part_header_bytes` below 64 (the shortest
192    /// useful `Content-Disposition` line must fit), or buffer sizes that do
193    /// not fit in `usize`. Every parser constructor and every `*_with`
194    /// function calls it.
195    ///
196    /// # Errors
197    ///
198    /// [`FormError::InvalidLimits`] naming the refused field.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use form_net::Limits;
204    ///
205    /// assert!(Limits::DEFAULT.validate().is_ok());
206    /// let broken = Limits::DEFAULT.with_max_part_header_bytes(16);
207    /// assert_eq!(broken.validate().unwrap_err().code(), "invalid_limits");
208    /// ```
209    pub fn validate(&self) -> Result<(), FormError> {
210        if self.max_part_header_bytes < 64 {
211            return Err(FormError::InvalidLimits {
212                limit: Limit::PartHeaderBytes,
213            });
214        }
215        let parts = [
216            u64::from(self.max_part_header_bytes),
217            u64::from(self.name()),
218            u64::from(self.filename()),
219            u64::from(self.token()),
220            CHARSET_CAPTURE as u64,
221        ];
222        let total = parts.iter().fold(0u64, |a, &b| a.saturating_add(b));
223        if total > usize::MAX as u64 {
224            return Err(FormError::InvalidLimits {
225                limit: Limit::PartHeaderBytes,
226            });
227        }
228        let url = u64::from(self.name())
229            .saturating_add(u64::from(self.value()))
230            .saturating_add(1);
231        if url > usize::MAX as u64 {
232            return Err(FormError::InvalidLimits {
233                limit: Limit::ValueLength,
234            });
235        }
236        Ok(())
237    }
238
239    /// Validates these limits together with a policy: a token field with an
240    /// effective `max_token_bytes` of 0 can never be captured.
241    pub(crate) fn validate_with(&self, policy: &crate::Policy) -> Result<(), FormError> {
242        self.validate()?;
243        if policy.token_field.is_some() && self.token() == 0 {
244            return Err(FormError::InvalidLimits {
245                limit: Limit::TokenLength,
246            });
247        }
248        Ok(())
249    }
250
251    /// The buffer length the multipart parser needs with these limits and
252    /// this policy: the header block, the decoded field name, the decoded
253    /// `filename*` (only under `ExtendedFilenamePolicy::AcceptAlone`), the
254    /// token (only with a token field), and 32 bytes for `_charset_`.
255    ///
256    /// A `const fn`, so a stack buffer can be sized at compile time.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use form_net::{Limits, Policy};
262    ///
263    /// const LIMITS: Limits = Limits::DEFAULT.with_max_part_header_bytes(1024);
264    /// const LEN: usize = LIMITS.multipart_buffer_len(&Policy::DEFAULT);
265    /// assert_eq!(LEN, 1024 + 256 + 32);
266    /// let buffer = [0u8; LEN];
267    /// ```
268    #[must_use]
269    pub const fn multipart_buffer_len(&self, policy: &crate::Policy) -> usize {
270        let mut len = add_usize(self.max_part_header_bytes as usize, self.name() as usize);
271        if matches!(
272            policy.extended_filename,
273            crate::ExtendedFilenamePolicy::AcceptAlone
274        ) {
275            len = add_usize(len, self.filename() as usize);
276        }
277        if policy.token_field.is_some() {
278            len = add_usize(len, self.token() as usize);
279        }
280        add_usize(len, CHARSET_CAPTURE)
281    }
282
283    /// The scratch length the streaming urlencoded parser needs:
284    /// `max_name_bytes + max_value_bytes + 1` (effective values).
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use form_net::Limits;
290    ///
291    /// const LIMITS: Limits = Limits::DEFAULT.with_max_text_bytes(1000);
292    /// assert_eq!(LIMITS.urlencoded_buffer_len(), 256 + 1000 + 1);
293    /// ```
294    #[must_use]
295    pub const fn urlencoded_buffer_len(&self) -> usize {
296        add_usize(add_usize(self.name() as usize, self.value() as usize), 1)
297    }
298
299    /// The worst-case bytes one parse with these limits can hold in memory,
300    /// for admission control before a body is read (FR-40, D-093).
301    ///
302    /// The sum of: the per-request state (1 KiB: an `UploadSession` holds
303    /// the parser, the driver, and the upload in 952 bytes on 64-bit
304    /// targets); the parser buffer and the driver's
305    /// copy of the current part head (each `multipart_buffer_len` under the
306    /// most demanding policy); the sniff window; the field being received
307    /// (`max_value_bytes`); the text kept (`max_text_bytes`) and its names
308    /// (`max_fields × max_name_bytes`, capped by `max_body_bytes`); 96 bytes
309    /// of index per field and 320 per part; owned copies of part names, file
310    /// names, and content types (`max_parts × max_part_header_bytes`, capped
311    /// by `max_body_bytes`); the files kept in memory,
312    /// `max_files × min(file_in_memory_bytes, max_file_bytes)`, capped by
313    /// twice `max_body_bytes` (a buffer that grew by doubling can hold twice
314    /// its bytes); and half the largest growing buffer (a buffer that
315    /// doubles holds its old half while it moves). `file_in_memory_bytes`
316    /// is the most bytes of one file a store keeps in memory
317    /// (`Store::memory_bound` passes it). The caller's own read buffer is
318    /// not included. All values are effective values; the arithmetic
319    /// saturates. `tests/memory_bound.rs` measures it against hostile bodies.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use form_net::Limits;
325    ///
326    /// const LOGIN: Limits = Limits::DEFAULT
327    ///     .with_max_body_bytes(4 * 1024)
328    ///     .with_max_fields(8)
329    ///     .with_max_parts(8)
330    ///     .with_max_files(0)
331    ///     .with_max_part_header_bytes(1024);
332    /// assert!(LOGIN.memory_bound(64 * 1024) < 32 * 1024);
333    /// // The defaults with files over 64 KiB spilled: about 6.5 MiB.
334    /// assert!(Limits::DEFAULT.memory_bound(64 * 1024) < 7 << 20);
335    /// ```
336    #[must_use]
337    pub const fn memory_bound(&self, file_in_memory_bytes: u64) -> u64 {
338        // Held by `test_the_api_size_table_holds`: every per-request
339        // struct fits (0.9: this was 256, below the parser's own 384).
340        const PARSER_STATE: u64 = 1024;
341        const FIELD_INDEX: u64 = 96;
342        const PART_INDEX: u64 = 320;
343        let head = (self.max_part_header_bytes as u64)
344            .saturating_add(self.name() as u64)
345            .saturating_add(self.filename() as u64)
346            .saturating_add(self.token() as u64)
347            .saturating_add(CHARSET_CAPTURE as u64);
348        let parts = self.max_parts as u64;
349        let copies = min_u64(
350            parts.saturating_mul(self.max_part_header_bytes as u64),
351            self.body(),
352        );
353        let sniff = min_u64(self.sniff_bytes as u64, self.file());
354        let file = min_u64(file_in_memory_bytes, self.file());
355        let files = min_u64(
356            (self.max_files as u64).saturating_mul(file),
357            self.body().saturating_mul(2),
358        );
359        let text = self.text_and_names();
360        let growth = if file > text { file } else { text } / 2;
361        PARSER_STATE
362            .saturating_add(head.saturating_mul(2))
363            .saturating_add(sniff)
364            .saturating_add(self.value() as u64)
365            .saturating_add(text)
366            .saturating_add((self.max_fields as u64).saturating_mul(FIELD_INDEX))
367            .saturating_add(parts.saturating_mul(PART_INDEX))
368            .saturating_add(copies)
369            .saturating_add(files)
370            .saturating_add(growth)
371    }
372
373    /// The text values and field names one body can hold: `max_text_bytes`
374    /// plus `max_fields × max_name_bytes`, the latter capped by
375    /// `max_body_bytes` (effective values).
376    pub(crate) const fn text_and_names(&self) -> u64 {
377        let names = min_u64(
378            (self.max_fields as u64).saturating_mul(self.name() as u64),
379            self.body(),
380        );
381        self.text().saturating_add(names)
382    }
383
384    // Effective values: each capped by the limit that contains it.
385
386    pub(crate) const fn body(&self) -> u64 {
387        self.max_body_bytes
388    }
389
390    pub(crate) const fn text(&self) -> u64 {
391        min_u64(self.max_text_bytes, self.body())
392    }
393
394    pub(crate) const fn value(&self) -> u32 {
395        min_u64(self.max_value_bytes as u64, self.text()) as u32
396    }
397
398    pub(crate) const fn name(&self) -> u32 {
399        min_u64(self.max_name_bytes as u64, self.body()) as u32
400    }
401
402    pub(crate) const fn file(&self) -> u64 {
403        min_u64(self.max_file_bytes, self.body())
404    }
405
406    #[cfg(feature = "alloc")]
407    pub(crate) const fn sniff(&self) -> u32 {
408        min_u64(self.sniff_bytes as u64, self.file()) as u32
409    }
410
411    pub(crate) const fn filename(&self) -> u32 {
412        min_u64(
413            self.max_filename_bytes as u64,
414            self.max_part_header_bytes as u64,
415        ) as u32
416    }
417
418    pub(crate) const fn token(&self) -> u16 {
419        min_u64(self.max_token_bytes as u64, self.value() as u64) as u16
420    }
421
422    pub(crate) const fn preamble(&self) -> u64 {
423        min_u64(self.max_preamble_bytes as u64, self.body())
424    }
425
426    pub(crate) const fn epilogue(&self) -> u64 {
427        min_u64(self.max_epilogue_bytes as u64, self.body())
428    }
429
430    #[cfg(feature = "alloc")]
431    pub(crate) const fn body_usize(&self) -> usize {
432        to_usize(self.body())
433    }
434}
435
436impl Default for Limits {
437    fn default() -> Self {
438        Self::DEFAULT
439    }
440}
441
442/// The behaviours a route may choose. The defaults are the strict ones;
443/// every alternative is named after what it allows.
444///
445/// # Examples
446///
447/// ```
448/// use form_net::{EmptyFilePolicy, HeaderPolicy, Policy, TokenField};
449///
450/// const UPLOADS: Policy = Policy::DEFAULT
451///     .with_token_field(TokenField::new("csrf_token"))
452///     .with_empty_files(EmptyFilePolicy::Skip)
453///     .with_unknown_headers(HeaderPolicy::Refuse);
454/// assert_eq!(UPLOADS.token_field.map(|t| t.name()), Some("csrf_token"));
455/// ```
456#[non_exhaustive]
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct Policy {
459    /// A malformed `%` in urlencoded input. Default `Refuse`.
460    pub percent: PercentPolicy,
461    /// `filename*` in a part. Default `Refuse`.
462    pub extended_filename: ExtendedFilenamePolicy,
463    /// Part headers other than the three known ones. Default `Ignore`.
464    pub unknown_headers: HeaderPolicy,
465    /// A file input left empty. Default `Report`.
466    pub empty_files: EmptyFilePolicy,
467    /// The anti-forgery token field to capture. Default none.
468    pub token_field: Option<TokenField>,
469}
470
471impl Policy {
472    /// The strict defaults.
473    pub const DEFAULT: Policy = Policy {
474        percent: PercentPolicy::Refuse,
475        extended_filename: ExtendedFilenamePolicy::Refuse,
476        unknown_headers: HeaderPolicy::Ignore,
477        empty_files: EmptyFilePolicy::Report,
478        token_field: None,
479    };
480
481    /// Sets `percent`.
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// use form_net::{Limits, PercentPolicy, Policy};
487    ///
488    /// let lenient = Policy::DEFAULT.with_percent(PercentPolicy::Literal);
489    /// let form = form_net::parse_urlencoded_with(b"rate=100%", &Limits::DEFAULT, &lenient)?;
490    /// assert_eq!(form.field("rate"), Some("100%"));
491    /// assert!(form_net::parse_urlencoded(b"rate=100%").is_err());
492    /// # Ok::<(), form_net::FormError>(())
493    /// ```
494    #[must_use]
495    pub const fn with_percent(mut self, policy: PercentPolicy) -> Self {
496        self.percent = policy;
497        self
498    }
499
500    /// Sets `extended_filename`.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// # const CT: &[u8] = b"multipart/form-data; boundary=X";
506    /// use form_net::{ExtendedFilenamePolicy, Limits, Policy};
507    ///
508    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename*=UTF-8''a.txt\r\n\r\nhi\r\n--X--";
509    /// let accept = Policy::DEFAULT.with_extended_filename(ExtendedFilenamePolicy::AcceptAlone);
510    /// let form = form_net::parse_multipart_with(CT, body, &Limits::DEFAULT, &accept)?;
511    /// assert!(form.file("f").is_some());
512    /// let err = form_net::parse_multipart(CT, body).unwrap_err();
513    /// assert_eq!(err.code(), "policy.extended_filename");
514    /// # Ok::<(), form_net::FormError>(())
515    /// ```
516    #[must_use]
517    pub const fn with_extended_filename(mut self, policy: ExtendedFilenamePolicy) -> Self {
518        self.extended_filename = policy;
519        self
520    }
521
522    /// Sets `unknown_headers`.
523    ///
524    /// # Examples
525    ///
526    /// ```
527    /// # const CT: &[u8] = b"multipart/form-data; boundary=X";
528    /// use form_net::{HeaderPolicy, Limits, Policy};
529    ///
530    /// let body = b"--X\r\nContent-Disposition: form-data; name=a\r\nX-Note: hi\r\n\r\n1\r\n--X--";
531    /// assert_eq!(form_net::parse_multipart(CT, body)?.field("a"), Some("1"));
532    /// let strict = Policy::DEFAULT.with_unknown_headers(HeaderPolicy::Refuse);
533    /// let err = form_net::parse_multipart_with(CT, body, &Limits::DEFAULT, &strict).unwrap_err();
534    /// assert_eq!(err.code(), "policy.unknown_header");
535    /// # Ok::<(), form_net::FormError>(())
536    /// ```
537    #[must_use]
538    pub const fn with_unknown_headers(mut self, policy: HeaderPolicy) -> Self {
539        self.unknown_headers = policy;
540        self
541    }
542
543    /// Sets `empty_files`.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// # const CT: &[u8] = b"multipart/form-data; boundary=X";
549    /// use form_net::{EmptyFilePolicy, Limits, Policy};
550    ///
551    /// // A file input the user left empty.
552    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=\"\"\r\n\r\n\r\n--X--";
553    /// assert_eq!(form_net::parse_multipart(CT, body)?.len(), 1);
554    /// let skip = Policy::DEFAULT.with_empty_files(EmptyFilePolicy::Skip);
555    /// assert_eq!(form_net::parse_multipart_with(CT, body, &Limits::DEFAULT, &skip)?.len(), 0);
556    /// # Ok::<(), form_net::FormError>(())
557    /// ```
558    #[must_use]
559    pub const fn with_empty_files(mut self, policy: EmptyFilePolicy) -> Self {
560        self.empty_files = policy;
561        self
562    }
563
564    /// Sets the token field to capture.
565    ///
566    /// # Examples
567    ///
568    /// ```
569    /// # const CT: &[u8] = b"multipart/form-data; boundary=X";
570    /// use form_net::{Limits, Policy, TokenField};
571    ///
572    /// let guarded = Policy::DEFAULT.with_token_field(TokenField::new("csrf"));
573    /// // A file part before the token: refused.
574    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a.txt\r\n\r\nhi\r\n\
575    ///              --X\r\nContent-Disposition: form-data; name=csrf\r\n\r\nt0k\r\n--X--";
576    /// let err = form_net::parse_multipart_with(CT, body, &Limits::DEFAULT, &guarded).unwrap_err();
577    /// assert_eq!(err.code(), "policy.token_field_late");
578    /// # Ok::<(), form_net::FormError>(())
579    /// ```
580    #[must_use]
581    pub const fn with_token_field(mut self, field: TokenField) -> Self {
582        self.token_field = Some(field);
583        self
584    }
585}
586
587impl Default for Policy {
588    fn default() -> Self {
589        Self::DEFAULT
590    }
591}
592
593/// What a `%` not followed by two hex digits means in urlencoded input.
594///
595/// # Examples
596///
597/// ```
598/// use form_net::{decode_into, PercentPolicy};
599///
600/// let mut out = [0u8; 8];
601/// assert!(decode_into(b"100%", &mut out, PercentPolicy::Refuse).is_err());
602/// assert_eq!(decode_into(b"100%", &mut out, PercentPolicy::Literal)?, "100%");
603/// # Ok::<(), form_net::FormError>(())
604/// ```
605#[non_exhaustive]
606#[derive(Clone, Copy, Debug, PartialEq, Eq)]
607pub enum PercentPolicy {
608    /// Refuse it (default).
609    Refuse,
610    /// Keep it as a literal byte, as WHATWG percent-decode does.
611    Literal,
612}
613
614/// What a `filename*` parameter (RFC 8187) in a part means. Browsers never
615/// send it; RFC 7578 forbids senders from using it.
616///
617/// Both `filename` and `filename*` in one part are refused under every
618/// policy.
619///
620/// # Examples
621///
622/// ```
623/// use form_net::{ExtendedFilenamePolicy, Policy};
624///
625/// let accept = Policy::DEFAULT.with_extended_filename(ExtendedFilenamePolicy::AcceptAlone);
626/// assert_eq!(Policy::DEFAULT.extended_filename, ExtendedFilenamePolicy::Refuse);
627/// assert_ne!(accept, Policy::DEFAULT);
628/// ```
629#[non_exhaustive]
630#[derive(Clone, Copy, Debug, PartialEq, Eq)]
631pub enum ExtendedFilenamePolicy {
632    /// Refuse any `filename*` (default).
633    Refuse,
634    /// Accept and decode it (UTF-8 only) when it is the only file name.
635    AcceptAlone,
636}
637
638/// Part headers other than `Content-Disposition`, `Content-Type`, and
639/// `Content-Transfer-Encoding`.
640///
641/// # Examples
642///
643/// ```
644/// use form_net::{HeaderPolicy, Policy};
645///
646/// assert_eq!(Policy::DEFAULT.unknown_headers, HeaderPolicy::Ignore);
647/// ```
648#[non_exhaustive]
649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
650pub enum HeaderPolicy {
651    /// Skip them, as RFC 7578 section 4.8 requires (default). They still
652    /// count toward the header budgets.
653    Ignore,
654    /// Refuse them.
655    Refuse,
656}
657
658/// A file input left empty (`filename=""` and no data).
659///
660/// # Examples
661///
662/// ```
663/// use form_net::{EmptyFilePolicy, Policy};
664///
665/// assert_eq!(Policy::DEFAULT.empty_files, EmptyFilePolicy::Report);
666/// ```
667#[non_exhaustive]
668#[derive(Clone, Copy, Debug, PartialEq, Eq)]
669pub enum EmptyFilePolicy {
670    /// Report it as a file part with an empty file name (default).
671    Report,
672    /// Produce nothing for it; it does not count against `max_files`.
673    Skip,
674}
675
676/// The anti-forgery token field to capture from a multipart body.
677///
678/// # Examples
679///
680/// ```
681/// use form_net::{TokenField, TokenPlacement};
682///
683/// let strict = TokenField::new("csrf_token");
684/// assert_eq!(strict.placement(), TokenPlacement::BeforeFirstFile);
685/// assert_eq!(strict.anywhere().placement(), TokenPlacement::Anywhere);
686/// ```
687#[derive(Clone, Copy, Debug, PartialEq, Eq)]
688pub struct TokenField {
689    name: &'static str,
690    placement: TokenPlacement,
691}
692
693impl TokenField {
694    /// A token field named `name` that must come before the first file.
695    ///
696    /// # Examples
697    ///
698    /// ```
699    /// use form_net::{TokenField, TokenPlacement};
700    ///
701    /// const TOKEN: TokenField = TokenField::new("csrf_token");
702    /// assert_eq!(TOKEN.name(), "csrf_token");
703    /// assert_eq!(TOKEN.placement(), TokenPlacement::BeforeFirstFile);
704    /// ```
705    #[must_use]
706    pub const fn new(name: &'static str) -> TokenField {
707        TokenField {
708            name,
709            placement: TokenPlacement::BeforeFirstFile,
710        }
711    }
712
713    /// The same field, allowed anywhere in the body.
714    ///
715    /// # Examples
716    ///
717    /// ```
718    /// # const CT: &[u8] = b"multipart/form-data; boundary=X";
719    /// use form_net::{Limits, Policy, TokenField};
720    ///
721    /// let relaxed = Policy::DEFAULT.with_token_field(TokenField::new("csrf").anywhere());
722    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a.txt\r\n\r\nhi\r\n\
723    ///              --X\r\nContent-Disposition: form-data; name=csrf\r\n\r\nt0k\r\n--X--";
724    /// let form = form_net::parse_multipart_with(CT, body, &Limits::DEFAULT, &relaxed)?;
725    /// assert_eq!(form.field("csrf"), Some("t0k"));
726    /// # Ok::<(), form_net::FormError>(())
727    /// ```
728    #[must_use]
729    pub const fn anywhere(self) -> TokenField {
730        TokenField {
731            name: self.name,
732            placement: TokenPlacement::Anywhere,
733        }
734    }
735
736    /// The field name, matched byte for byte.
737    ///
738    /// # Examples
739    ///
740    /// ```
741    /// let field = form_net::TokenField::new("csrf").anywhere();
742    /// assert_eq!(field.name(), "csrf");
743    /// ```
744    #[must_use]
745    pub const fn name(&self) -> &'static str {
746        self.name
747    }
748
749    /// Where the field must be.
750    ///
751    /// # Examples
752    ///
753    /// ```
754    /// use form_net::{TokenField, TokenPlacement};
755    ///
756    /// assert_eq!(TokenField::new("t").placement(), TokenPlacement::BeforeFirstFile);
757    /// assert_eq!(TokenField::new("t").anywhere().placement(), TokenPlacement::Anywhere);
758    /// ```
759    #[must_use]
760    pub const fn placement(&self) -> TokenPlacement {
761        self.placement
762    }
763}
764
765/// Where the token field must be.
766///
767/// # Examples
768///
769/// ```
770/// use form_net::{Policy, TokenField, TokenPlacement};
771///
772/// let policy = Policy::DEFAULT.with_token_field(TokenField::new("csrf").anywhere());
773/// assert_eq!(policy.token_field.map(|t| t.placement()), Some(TokenPlacement::Anywhere));
774/// ```
775#[non_exhaustive]
776#[derive(Clone, Copy, Debug, PartialEq, Eq)]
777pub enum TokenPlacement {
778    /// Before the first file part (default of [`TokenField::new`]).
779    BeforeFirstFile,
780    /// Anywhere.
781    Anywhere,
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn test_limits_default_validates() {
790        assert!(Limits::DEFAULT.validate().is_ok());
791        assert_eq!(Limits::default(), Limits::DEFAULT);
792    }
793
794    #[test]
795    fn test_limits_header_budget_below_64_refused() {
796        for bytes in [0, 1, 63] {
797            let l = Limits::DEFAULT.with_max_part_header_bytes(bytes);
798            assert_eq!(
799                l.validate(),
800                Err(FormError::InvalidLimits {
801                    limit: Limit::PartHeaderBytes
802                })
803            );
804        }
805        assert!(
806            Limits::DEFAULT
807                .with_max_part_header_bytes(64)
808                .validate()
809                .is_ok()
810        );
811    }
812
813    #[test]
814    fn test_limits_containing_limit_caps_contained() {
815        let l = Limits::DEFAULT.with_max_body_bytes(4096);
816        assert_eq!(l.text(), 4096);
817        assert_eq!(l.value(), 4096);
818        assert_eq!(l.file(), 4096);
819        assert_eq!(l.name(), 256);
820        assert_eq!(l.token(), 128);
821        let l = l.with_max_value_bytes(10);
822        assert_eq!((l.value(), l.token()), (10, 10));
823        let l = Limits::DEFAULT.with_max_part_header_bytes(100);
824        assert_eq!(l.filename(), 100);
825        let l = Limits::DEFAULT.with_sniff_bytes(64).with_max_file_bytes(16);
826        assert!(l.validate().is_ok());
827    }
828
829    #[test]
830    fn test_limits_zero_token_with_token_field_refused() {
831        let policy = Policy::DEFAULT.with_token_field(TokenField::new("t"));
832        let l = Limits::DEFAULT.with_max_token_bytes(0);
833        assert_eq!(
834            l.validate_with(&policy),
835            Err(FormError::InvalidLimits {
836                limit: Limit::TokenLength
837            })
838        );
839        assert!(l.validate_with(&Policy::DEFAULT).is_ok());
840    }
841
842    #[test]
843    fn test_buffer_lengths_follow_policy() {
844        let l = Limits::DEFAULT;
845        assert_eq!(l.multipart_buffer_len(&Policy::DEFAULT), 8192 + 256 + 32);
846        let p = Policy::DEFAULT
847            .with_extended_filename(ExtendedFilenamePolicy::AcceptAlone)
848            .with_token_field(TokenField::new("t"));
849        assert_eq!(l.multipart_buffer_len(&p), 8192 + 256 + 1024 + 128 + 32);
850        assert_eq!(l.urlencoded_buffer_len(), 256 + 1024 * 1024 + 1);
851    }
852
853    #[test]
854    fn test_setters_set_one_field_each() {
855        let l = Limits::DEFAULT
856            .with_max_body_bytes(1)
857            .with_max_fields(2)
858            .with_max_name_bytes(3)
859            .with_max_value_bytes(4)
860            .with_max_text_bytes(5)
861            .with_max_parts(6)
862            .with_max_files(7)
863            .with_max_file_bytes(8)
864            .with_max_part_header_bytes(9)
865            .with_max_part_headers(10)
866            .with_max_filename_bytes(11)
867            .with_max_preamble_bytes(12)
868            .with_max_epilogue_bytes(13)
869            .with_max_padding_bytes(14)
870            .with_sniff_bytes(15)
871            .with_max_token_bytes(16);
872        assert_eq!(
873            (
874                l.max_body_bytes,
875                l.max_fields,
876                l.max_name_bytes,
877                l.max_value_bytes
878            ),
879            (1, 2, 3, 4)
880        );
881        assert_eq!(
882            (l.max_text_bytes, l.max_parts, l.max_files, l.max_file_bytes),
883            (5, 6, 7, 8)
884        );
885        assert_eq!(
886            (
887                l.max_part_header_bytes,
888                l.max_part_headers,
889                l.max_filename_bytes,
890                l.max_preamble_bytes
891            ),
892            (9, 10, 11, 12)
893        );
894        assert_eq!(
895            (
896                l.max_epilogue_bytes,
897                l.max_padding_bytes,
898                l.sniff_bytes,
899                l.max_token_bytes
900            ),
901            (13, 14, 15, 16)
902        );
903    }
904
905    #[test]
906    fn test_policy_setters() {
907        let p = Policy::DEFAULT
908            .with_percent(PercentPolicy::Literal)
909            .with_extended_filename(ExtendedFilenamePolicy::AcceptAlone)
910            .with_unknown_headers(HeaderPolicy::Refuse)
911            .with_empty_files(EmptyFilePolicy::Skip)
912            .with_token_field(TokenField::new("t").anywhere());
913        assert_eq!(p.percent, PercentPolicy::Literal);
914        assert_eq!(p.extended_filename, ExtendedFilenamePolicy::AcceptAlone);
915        assert_eq!(p.unknown_headers, HeaderPolicy::Refuse);
916        assert_eq!(p.empty_files, EmptyFilePolicy::Skip);
917        assert_eq!(
918            p.token_field.map(|t| (t.name(), t.placement())),
919            Some(("t", TokenPlacement::Anywhere))
920        );
921        assert_eq!(Policy::default(), Policy::DEFAULT);
922    }
923}