Skip to main content

form_net/
error.rs

1//! The error type: one `Copy` enum for the whole crate, its reason enums,
2//! and where an error was found.
3//!
4//! No error ever holds input bytes, a field value, a file name, or a path, so
5//! every error is safe to log.
6
7use core::fmt;
8
9use crate::hook::Refusal;
10
11/// Everything that can go wrong in form-net.
12///
13/// Input errors (`Limit`, `Syntax`, `Encoding`, `Policy`) carry a
14/// [`Location`]; caller mistakes (`InvalidLimits`, `BufferTooSmall`,
15/// `Poisoned`) and environment failures (`Io`, `Random`) do not.
16/// [`status_hint`](Self::status_hint) maps every error to an HTTP status and
17/// [`code`](Self::code) to a stable name.
18///
19/// # Examples
20///
21/// ```
22/// use form_net::{FormError, Limit};
23///
24/// let err = form_net::parse_urlencoded(&vec![b'a'; 11 << 20]).unwrap_err();
25/// assert!(matches!(err, FormError::Limit { limit: Limit::Body, .. }));
26/// assert_eq!((err.status_hint(), err.code()), (413, "limit.body"));
27/// ```
28#[non_exhaustive]
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum FormError {
31    /// A budget of [`Limits`](crate::Limits) was exceeded.
32    Limit {
33        /// Which limit.
34        limit: Limit,
35        /// Where.
36        at: Location,
37    },
38    /// The input breaks the grammar.
39    Syntax {
40        /// What was wrong.
41        reason: SyntaxError,
42        /// Where.
43        at: Location,
44    },
45    /// Bytes that do not decode: percent escapes, UTF-8, charsets.
46    Encoding {
47        /// What was wrong.
48        reason: EncodingError,
49        /// Where.
50        at: Location,
51    },
52    /// The input is well formed but a [`Policy`](crate::Policy) refuses it.
53    Policy {
54        /// Which rule.
55        reason: PolicyRefusal,
56        /// Where.
57        at: Location,
58    },
59    /// An inspector refused the input.
60    Refused {
61        /// The inspector's status and code.
62        refusal: Refusal,
63        /// Where.
64        at: Location,
65    },
66    /// Typed extraction failed.
67    Extract {
68        /// What was wrong.
69        reason: ExtractError,
70        /// The Rust-side field name, when there is one.
71        field: Option<&'static str>,
72    },
73    /// The content type is not one form-net parses.
74    UnsupportedMediaType,
75    /// [`Limits::validate`](crate::Limits::validate) refused the
76    /// configuration.
77    InvalidLimits {
78        /// The field it refused.
79        limit: Limit,
80    },
81    /// A buffer the caller provided is too short.
82    BufferTooSmall {
83        /// The length it must have.
84        needed: usize,
85    },
86    /// The reader, the writer, or the file system failed.
87    #[cfg(feature = "std")]
88    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
89    Io {
90        /// The kind of failure; never a path or an OS message.
91        kind: std::io::ErrorKind,
92    },
93    /// The operating system's random source failed.
94    #[cfg(feature = "std")]
95    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
96    Random,
97    /// Storage refused: the shared budget is full, a path is not a
98    /// directory, or a `persist` destination exists.
99    #[cfg(feature = "fs")]
100    #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
101    Store {
102        /// What went wrong.
103        reason: crate::store::StoreError,
104    },
105    /// A parser was called again after an error, without `reset`.
106    Poisoned,
107}
108
109impl FormError {
110    pub(crate) const fn limit(limit: Limit, at: Location) -> Self {
111        Self::Limit { limit, at }
112    }
113
114    pub(crate) const fn syntax(reason: SyntaxError, at: Location) -> Self {
115        Self::Syntax { reason, at }
116    }
117
118    pub(crate) const fn encoding(reason: EncodingError, at: Location) -> Self {
119        Self::Encoding { reason, at }
120    }
121
122    pub(crate) const fn policy(reason: PolicyRefusal, at: Location) -> Self {
123        Self::Policy { reason, at }
124    }
125
126    /// The same error with its location moved `by` bytes later, for errors
127    /// found inside a component that starts at `by`.
128    #[must_use]
129    pub(crate) const fn shifted(self, by: u64) -> Self {
130        match self {
131            Self::Limit { limit, at } => Self::Limit {
132                limit,
133                at: at.shifted(by),
134            },
135            Self::Syntax { reason, at } => Self::Syntax {
136                reason,
137                at: at.shifted(by),
138            },
139            Self::Encoding { reason, at } => Self::Encoding {
140                reason,
141                at: at.shifted(by),
142            },
143            Self::Policy { reason, at } => Self::Policy {
144                reason,
145                at: at.shifted(by),
146            },
147            Self::Refused { refusal, at } => Self::Refused {
148                refusal,
149                at: at.shifted(by),
150            },
151            other => other,
152        }
153    }
154
155    /// The HTTP status a server should answer with.
156    ///
157    /// 413 for every limit, 400 for syntax and most encoding and policy
158    /// errors, 415 for unsupported media types, charsets, transfer encodings,
159    /// and nested multipart, 422 for extraction errors, 503 when the shared
160    /// spill budget is full, 500 for caller mistakes and environment
161    /// failures, and the inspector's own status for a refusal (400 if that
162    /// status is outside 400 to 599).
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use form_net::FormError;
168    ///
169    /// assert_eq!(FormError::UnsupportedMediaType.status_hint(), 415);
170    /// assert_eq!(FormError::Poisoned.status_hint(), 500);
171    /// ```
172    #[must_use]
173    pub const fn status_hint(&self) -> u16 {
174        match self {
175            Self::Limit { .. } => 413,
176            Self::Syntax { .. } => 400,
177            Self::Encoding { reason, .. } => match reason {
178                EncodingError::UnsupportedCharset => 415,
179                _ => 400,
180            },
181            Self::Policy { reason, .. } => match reason {
182                PolicyRefusal::TransferEncoding | PolicyRefusal::NestedMultipart => 415,
183                _ => 400,
184            },
185            Self::Refused { refusal, .. } => {
186                if refusal.status >= 400 && refusal.status <= 599 {
187                    refusal.status
188                } else {
189                    400
190                }
191            }
192            Self::Extract { .. } => 422,
193            Self::UnsupportedMediaType => 415,
194            Self::InvalidLimits { .. } | Self::BufferTooSmall { .. } | Self::Poisoned => 500,
195            #[cfg(feature = "std")]
196            Self::Io { .. } | Self::Random => 500,
197            #[cfg(feature = "fs")]
198            Self::Store { reason } => match reason {
199                crate::store::StoreError::QuotaExceeded => 503,
200                _ => 500,
201            },
202        }
203    }
204
205    /// A stable, machine-readable name for the error: the variant and the
206    /// reason in snake case, joined with a dot (`limit.parts`,
207    /// `syntax.boundary_in_content`). A refusal returns the inspector's own
208    /// [`Refusal::code`]. Codes never change meaning after 1.0.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use form_net::FormError;
214    ///
215    /// assert_eq!(FormError::UnsupportedMediaType.code(), "unsupported_media_type");
216    /// let err = form_net::parse_urlencoded(b"a=%zz").unwrap_err();
217    /// assert_eq!(err.code(), "encoding.invalid_percent");
218    /// ```
219    #[must_use]
220    pub const fn code(&self) -> &'static str {
221        match self {
222            Self::Limit { limit, .. } => limit.code(),
223            Self::Syntax { reason, .. } => reason.code(),
224            Self::Encoding { reason, .. } => reason.code(),
225            Self::Policy { reason, .. } => reason.code(),
226            Self::Refused { refusal, .. } => refusal.code,
227            Self::Extract { reason, .. } => reason.code(),
228            Self::UnsupportedMediaType => "unsupported_media_type",
229            Self::InvalidLimits { .. } => "invalid_limits",
230            Self::BufferTooSmall { .. } => "buffer_too_small",
231            #[cfg(feature = "std")]
232            Self::Io { .. } => "io",
233            #[cfg(feature = "std")]
234            Self::Random => "random",
235            #[cfg(feature = "fs")]
236            Self::Store { reason } => reason.code(),
237            Self::Poisoned => "poisoned",
238        }
239    }
240
241    /// The error's counter in a [`RefusalDigest`](crate::RefusalDigest):
242    /// its index in [`SLOT_CODES`], or `None` for an inspector's refusal,
243    /// whose code is the inspector's own.
244    #[cfg(target_has_atomic = "32")]
245    pub(crate) const fn slot(&self) -> Option<usize> {
246        let slot = match self {
247            Self::Limit { limit, .. } => limit.index(),
248            Self::Syntax { reason, .. } => reason.index().wrapping_add(16),
249            Self::Encoding { reason, .. } => reason.index().wrapping_add(36),
250            Self::Policy { reason, .. } => reason.index().wrapping_add(40),
251            Self::Extract { reason, .. } => reason.index().wrapping_add(47),
252            #[cfg(feature = "fs")]
253            Self::Store { reason } => reason.index().wrapping_add(52),
254            Self::UnsupportedMediaType => 55,
255            Self::InvalidLimits { .. } => 56,
256            Self::BufferTooSmall { .. } => 57,
257            #[cfg(feature = "std")]
258            Self::Io { .. } => 58,
259            #[cfg(feature = "std")]
260            Self::Random => 59,
261            Self::Poisoned => 60,
262            Self::Refused { .. } => return None,
263        };
264        Some(slot)
265    }
266
267    /// Where the error was found, for input errors and refusals.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// let err = form_net::parse_urlencoded(b"ok=1&bad=%zz").unwrap_err();
273    /// assert_eq!(err.location().map(|l| l.offset), Some(9));
274    /// assert_eq!(form_net::FormError::Poisoned.location(), None);
275    /// ```
276    #[must_use]
277    pub const fn location(&self) -> Option<Location> {
278        match self {
279            Self::Limit { at, .. }
280            | Self::Syntax { at, .. }
281            | Self::Encoding { at, .. }
282            | Self::Policy { at, .. }
283            | Self::Refused { at, .. } => Some(*at),
284            _ => None,
285        }
286    }
287}
288
289impl fmt::Display for FormError {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            Self::Limit { limit, at } => {
293                write!(f, "{} ({at}): {}", limit.describe(), limit.action())
294            }
295            Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
296            Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
297            Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
298            Self::Refused { refusal, at } => {
299                write!(f, "refused by the inspector: {} ({at})", refusal.code)
300            }
301            Self::Extract { reason, field } => match field {
302                Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
303                None => f.write_str(reason.describe()),
304            },
305            Self::UnsupportedMediaType => f.write_str(
306                "unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
307            ),
308            Self::InvalidLimits { limit } => write!(
309                f,
310                "invalid limits: {} cannot be satisfied by any body; fix the configuration",
311                limit.field_name()
312            ),
313            Self::BufferTooSmall { needed } => write!(
314                f,
315                "buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
316            ),
317            #[cfg(feature = "std")]
318            Self::Io { kind } => write!(f, "I/O failure: {kind}"),
319            #[cfg(feature = "std")]
320            Self::Random => f.write_str(
321                "the operating system's random source failed; no predictable fallback is used",
322            ),
323            #[cfg(feature = "fs")]
324            Self::Store { reason } => write!(f, "storage: {}", reason.describe()),
325            Self::Poisoned => {
326                f.write_str("parser used after an error; call reset before parsing again")
327            }
328        }
329    }
330}
331
332impl core::error::Error for FormError {}
333
334/// Where an error was found.
335///
336/// # Examples
337///
338/// ```
339/// let err = form_net::parse_urlencoded(b"a=1&b=%").unwrap_err();
340/// let at = err.location().unwrap();
341/// assert_eq!((at.part, at.offset), (None, 6));
342/// ```
343#[non_exhaustive]
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
345pub struct Location {
346    /// The multipart part index (from 0), when the error is inside a part.
347    pub part: Option<u32>,
348    /// The byte offset in the body (or in the component, for `decode_into`).
349    pub offset: u64,
350}
351
352impl Location {
353    /// No location: configuration errors.
354    pub const NONE: Location = Location {
355        part: None,
356        offset: 0,
357    };
358
359    pub(crate) const fn at(offset: u64) -> Self {
360        Self { part: None, offset }
361    }
362
363    pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
364        Self { part, offset }
365    }
366
367    const fn shifted(self, by: u64) -> Self {
368        Self {
369            part: self.part,
370            offset: self.offset.saturating_add(by),
371        }
372    }
373}
374
375impl fmt::Display for Location {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        match self.part {
378            Some(part) => write!(f, "part {part}, byte {}", self.offset),
379            None => write!(f, "byte {}", self.offset),
380        }
381    }
382}
383
384/// Which budget of [`Limits`](crate::Limits) was exceeded.
385#[non_exhaustive]
386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387pub enum Limit {
388    /// `max_body_bytes`.
389    Body,
390    /// `max_fields`.
391    Fields,
392    /// `max_name_bytes`.
393    NameLength,
394    /// `max_value_bytes`.
395    ValueLength,
396    /// `max_text_bytes`.
397    TextTotal,
398    /// `max_parts`.
399    Parts,
400    /// `max_files`.
401    Files,
402    /// `max_file_bytes`.
403    FileSize,
404    /// `max_part_header_bytes`.
405    PartHeaderBytes,
406    /// `max_part_headers`.
407    PartHeaderCount,
408    /// `max_filename_bytes`.
409    FilenameLength,
410    /// `max_preamble_bytes`.
411    Preamble,
412    /// `max_epilogue_bytes`.
413    Epilogue,
414    /// `max_padding_bytes`.
415    Padding,
416    /// `max_token_bytes`.
417    TokenLength,
418    /// The per-request disk limit of a store.
419    Disk,
420}
421
422impl Limit {
423    #[cfg(target_has_atomic = "32")]
424    const fn index(self) -> usize {
425        match self {
426            Self::Body => 0,
427            Self::Fields => 1,
428            Self::NameLength => 2,
429            Self::ValueLength => 3,
430            Self::TextTotal => 4,
431            Self::Parts => 5,
432            Self::Files => 6,
433            Self::FileSize => 7,
434            Self::PartHeaderBytes => 8,
435            Self::PartHeaderCount => 9,
436            Self::FilenameLength => 10,
437            Self::Preamble => 11,
438            Self::Epilogue => 12,
439            Self::Padding => 13,
440            Self::TokenLength => 14,
441            Self::Disk => 15,
442        }
443    }
444
445    const fn code(self) -> &'static str {
446        match self {
447            Self::Body => "limit.body",
448            Self::Fields => "limit.fields",
449            Self::NameLength => "limit.name_length",
450            Self::ValueLength => "limit.value_length",
451            Self::TextTotal => "limit.text_total",
452            Self::Parts => "limit.parts",
453            Self::Files => "limit.files",
454            Self::FileSize => "limit.file_size",
455            Self::PartHeaderBytes => "limit.part_header_bytes",
456            Self::PartHeaderCount => "limit.part_header_count",
457            Self::FilenameLength => "limit.filename_length",
458            Self::Preamble => "limit.preamble",
459            Self::Epilogue => "limit.epilogue",
460            Self::Padding => "limit.padding",
461            Self::TokenLength => "limit.token_length",
462            Self::Disk => "limit.disk",
463        }
464    }
465
466    const fn field_name(self) -> &'static str {
467        match self {
468            Self::Body => "max_body_bytes",
469            Self::Fields => "max_fields",
470            Self::NameLength => "max_name_bytes",
471            Self::ValueLength => "max_value_bytes",
472            Self::TextTotal => "max_text_bytes",
473            Self::Parts => "max_parts",
474            Self::Files => "max_files",
475            Self::FileSize => "max_file_bytes",
476            Self::PartHeaderBytes => "max_part_header_bytes",
477            Self::PartHeaderCount => "max_part_headers",
478            Self::FilenameLength => "max_filename_bytes",
479            Self::Preamble => "max_preamble_bytes",
480            Self::Epilogue => "max_epilogue_bytes",
481            Self::Padding => "max_padding_bytes",
482            Self::TokenLength => "max_token_bytes",
483            Self::Disk => "max_disk_bytes",
484        }
485    }
486
487    const fn describe(self) -> &'static str {
488        match self {
489            Self::Body => "body too large",
490            Self::Fields => "too many fields",
491            Self::NameLength => "field name too long",
492            Self::ValueLength => "field value too long",
493            Self::TextTotal => "text fields too large in total",
494            Self::Parts => "too many parts",
495            Self::Files => "too many files",
496            Self::FileSize => "file too large",
497            Self::PartHeaderBytes => "part headers too large",
498            Self::PartHeaderCount => "too many part header lines",
499            Self::FilenameLength => "file name too long",
500            Self::Preamble => "too many bytes before the first boundary",
501            Self::Epilogue => "too many bytes after the last boundary",
502            Self::Padding => "too much padding after a boundary",
503            Self::TokenLength => "anti-forgery token too long",
504            Self::Disk => "too many bytes stored on disk for one request",
505        }
506    }
507
508    const fn action(self) -> &'static str {
509        match self {
510            Self::Body => "raise max_body_bytes if this route accepts larger bodies",
511            Self::Fields => "raise max_fields if this route accepts more fields",
512            Self::NameLength => "raise max_name_bytes if this route uses longer names",
513            Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
514            Self::TextTotal => "raise max_text_bytes if this route accepts more text",
515            Self::Parts => "raise max_parts if this route accepts more parts",
516            Self::Files => "raise max_files if this route accepts more files",
517            Self::FileSize => "raise max_file_bytes if this route accepts larger files",
518            Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
519            Self::PartHeaderCount => "raise max_part_headers if real clients need it",
520            Self::FilenameLength => "raise max_filename_bytes if real clients need it",
521            Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
522            Self::Padding => "the body is not what browsers send",
523            Self::TokenLength => "treat the request as forged",
524            Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
525        }
526    }
527}
528
529/// What part of the grammar the input broke.
530#[non_exhaustive]
531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
532pub enum SyntaxError {
533    /// A `Content-Type` value (of the request or of a part) is malformed.
534    InvalidContentType,
535    /// `multipart/form-data` without a `boundary` parameter.
536    MissingBoundary,
537    /// Two `boundary` parameters.
538    DuplicateBoundary,
539    /// A boundary that is empty, longer than 70 bytes, has a byte outside
540    /// RFC 2046 `bchars`, or ends with a space.
541    InvalidBoundary,
542    /// The body ended without any delimiter.
543    NoInitialDelimiter,
544    /// A full delimiter followed by something other than `--`, padding, or
545    /// CRLF; `--boundary` after a lone CR or LF, or as the first bytes of a
546    /// part's data (D-113); or writer data that contains any of these.
547    BoundaryInContent,
548    /// LF without CR where CRLF is required.
549    BareLineFeed,
550    /// A part header line without a colon.
551    MalformedHeaderLine,
552    /// A folded part header line.
553    HeaderFolding,
554    /// A part header name that is not a token, or has whitespace before the
555    /// colon.
556    InvalidHeaderName,
557    /// A control byte other than HTAB in a part header value.
558    ControlInHeader,
559    /// A part without `Content-Disposition`.
560    MissingContentDisposition,
561    /// A known part header given twice.
562    DuplicateHeader,
563    /// A `Content-Disposition` type other than `form-data`.
564    NotFormData,
565    /// A `Content-Disposition` without `name`.
566    MissingName,
567    /// An empty `name`.
568    EmptyName,
569    /// A parameter given twice.
570    DuplicateParameter,
571    /// Malformed parameters (or more than 16 on one header).
572    MalformedParameter,
573    /// A quoted parameter value that contains `\"`.
574    AmbiguousQuote,
575    /// The body ended before the close delimiter.
576    Truncated,
577}
578
579impl SyntaxError {
580    #[cfg(target_has_atomic = "32")]
581    const fn index(self) -> usize {
582        match self {
583            Self::InvalidContentType => 0,
584            Self::MissingBoundary => 1,
585            Self::DuplicateBoundary => 2,
586            Self::InvalidBoundary => 3,
587            Self::NoInitialDelimiter => 4,
588            Self::BoundaryInContent => 5,
589            Self::BareLineFeed => 6,
590            Self::MalformedHeaderLine => 7,
591            Self::HeaderFolding => 8,
592            Self::InvalidHeaderName => 9,
593            Self::ControlInHeader => 10,
594            Self::MissingContentDisposition => 11,
595            Self::DuplicateHeader => 12,
596            Self::NotFormData => 13,
597            Self::MissingName => 14,
598            Self::EmptyName => 15,
599            Self::DuplicateParameter => 16,
600            Self::MalformedParameter => 17,
601            Self::AmbiguousQuote => 18,
602            Self::Truncated => 19,
603        }
604    }
605
606    const fn code(self) -> &'static str {
607        match self {
608            Self::InvalidContentType => "syntax.invalid_content_type",
609            Self::MissingBoundary => "syntax.missing_boundary",
610            Self::DuplicateBoundary => "syntax.duplicate_boundary",
611            Self::InvalidBoundary => "syntax.invalid_boundary",
612            Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
613            Self::BoundaryInContent => "syntax.boundary_in_content",
614            Self::BareLineFeed => "syntax.bare_line_feed",
615            Self::MalformedHeaderLine => "syntax.malformed_header_line",
616            Self::HeaderFolding => "syntax.header_folding",
617            Self::InvalidHeaderName => "syntax.invalid_header_name",
618            Self::ControlInHeader => "syntax.control_in_header",
619            Self::MissingContentDisposition => "syntax.missing_content_disposition",
620            Self::DuplicateHeader => "syntax.duplicate_header",
621            Self::NotFormData => "syntax.not_form_data",
622            Self::MissingName => "syntax.missing_name",
623            Self::EmptyName => "syntax.empty_name",
624            Self::DuplicateParameter => "syntax.duplicate_parameter",
625            Self::MalformedParameter => "syntax.malformed_parameter",
626            Self::AmbiguousQuote => "syntax.ambiguous_quote",
627            Self::Truncated => "syntax.truncated",
628        }
629    }
630
631    const fn describe(self) -> &'static str {
632        match self {
633            Self::InvalidContentType => "malformed Content-Type",
634            Self::MissingBoundary => "multipart/form-data without a boundary",
635            Self::DuplicateBoundary => "two boundary parameters",
636            Self::InvalidBoundary => "invalid multipart boundary",
637            Self::NoInitialDelimiter => {
638                "no multipart delimiter found: the body is not multipart or the boundary is wrong"
639            }
640            Self::BoundaryInContent => "the boundary appears inside a part",
641            Self::BareLineFeed => "LF without CR where CRLF is required",
642            Self::MalformedHeaderLine => "malformed part header line",
643            Self::HeaderFolding => "folded part header line",
644            Self::InvalidHeaderName => "invalid part header name",
645            Self::ControlInHeader => "control byte in a part header",
646            Self::MissingContentDisposition => "part without Content-Disposition",
647            Self::DuplicateHeader => "part header given twice",
648            Self::NotFormData => "Content-Disposition type is not form-data",
649            Self::MissingName => "part without a name",
650            Self::EmptyName => "part with an empty name",
651            Self::DuplicateParameter => "parameter given twice",
652            Self::MalformedParameter => "malformed header parameter",
653            Self::AmbiguousQuote => "quoted value contains \\\"",
654            Self::Truncated => "body ended before the closing boundary",
655        }
656    }
657}
658
659/// Bytes that do not decode.
660#[non_exhaustive]
661#[derive(Clone, Copy, Debug, PartialEq, Eq)]
662pub enum EncodingError {
663    /// `%` not followed by two hex digits, under `PercentPolicy::Refuse`.
664    InvalidPercent,
665    /// Text that is not valid UTF-8.
666    InvalidUtf8,
667    /// A charset other than UTF-8 or US-ASCII.
668    UnsupportedCharset,
669    /// A malformed `filename*`.
670    InvalidExtendedValue,
671}
672
673impl EncodingError {
674    #[cfg(target_has_atomic = "32")]
675    const fn index(self) -> usize {
676        match self {
677            Self::InvalidPercent => 0,
678            Self::InvalidUtf8 => 1,
679            Self::UnsupportedCharset => 2,
680            Self::InvalidExtendedValue => 3,
681        }
682    }
683
684    const fn code(self) -> &'static str {
685        match self {
686            Self::InvalidPercent => "encoding.invalid_percent",
687            Self::InvalidUtf8 => "encoding.invalid_utf8",
688            Self::UnsupportedCharset => "encoding.unsupported_charset",
689            Self::InvalidExtendedValue => "encoding.invalid_extended_value",
690        }
691    }
692
693    const fn describe(self) -> &'static str {
694        match self {
695            Self::InvalidPercent => "invalid percent escape",
696            Self::InvalidUtf8 => "text is not valid UTF-8",
697            Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
698            Self::InvalidExtendedValue => "malformed filename*",
699        }
700    }
701}
702
703/// Well-formed input that a [`Policy`](crate::Policy) refuses.
704#[non_exhaustive]
705#[derive(Clone, Copy, Debug, PartialEq, Eq)]
706pub enum PolicyRefusal {
707    /// A `Content-Transfer-Encoding` other than `7bit`, `8bit`, `binary`.
708    TransferEncoding,
709    /// A part declared `multipart/*`.
710    NestedMultipart,
711    /// An unknown part header under `HeaderPolicy::Refuse`.
712    UnknownHeader,
713    /// Both `filename` and `filename*` in one part.
714    BothFilenames,
715    /// `filename*` under `ExtendedFilenamePolicy::Refuse`.
716    ExtendedFilename,
717    /// A file part before the token field under
718    /// `TokenPlacement::BeforeFirstFile`.
719    TokenFieldLate,
720    /// The token field given twice.
721    DuplicateToken,
722}
723
724impl PolicyRefusal {
725    #[cfg(target_has_atomic = "32")]
726    const fn index(self) -> usize {
727        match self {
728            Self::TransferEncoding => 0,
729            Self::NestedMultipart => 1,
730            Self::UnknownHeader => 2,
731            Self::BothFilenames => 3,
732            Self::ExtendedFilename => 4,
733            Self::TokenFieldLate => 5,
734            Self::DuplicateToken => 6,
735        }
736    }
737
738    const fn code(self) -> &'static str {
739        match self {
740            Self::TransferEncoding => "policy.transfer_encoding",
741            Self::NestedMultipart => "policy.nested_multipart",
742            Self::UnknownHeader => "policy.unknown_header",
743            Self::BothFilenames => "policy.both_filenames",
744            Self::ExtendedFilename => "policy.extended_filename",
745            Self::TokenFieldLate => "policy.token_field_late",
746            Self::DuplicateToken => "policy.duplicate_token",
747        }
748    }
749
750    const fn describe(self) -> &'static str {
751        match self {
752            Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
753            Self::NestedMultipart => "nested multipart part",
754            Self::UnknownHeader => "unknown part header",
755            Self::BothFilenames => "both filename and filename*",
756            Self::ExtendedFilename => "filename* is not accepted",
757            Self::TokenFieldLate => {
758                "file part before the anti-forgery token; treat as a possible forgery"
759            }
760            Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
761        }
762    }
763}
764
765/// Why typed extraction failed.
766#[non_exhaustive]
767#[derive(Clone, Copy, Debug, PartialEq, Eq)]
768pub enum ExtractError {
769    /// A name the struct neither declares nor ignores.
770    Unknown,
771    /// A required field is absent.
772    Missing,
773    /// A single-valued field was given twice.
774    Duplicate,
775    /// A value that does not convert; the text says why.
776    Invalid(&'static str),
777    /// A file where text was expected, or the reverse.
778    WrongKind,
779}
780
781impl ExtractError {
782    #[cfg(target_has_atomic = "32")]
783    const fn index(self) -> usize {
784        match self {
785            Self::Unknown => 0,
786            Self::Missing => 1,
787            Self::Duplicate => 2,
788            Self::Invalid(_) => 3,
789            Self::WrongKind => 4,
790        }
791    }
792
793    const fn code(self) -> &'static str {
794        match self {
795            Self::Unknown => "extract.unknown",
796            Self::Missing => "extract.missing",
797            Self::Duplicate => "extract.duplicate",
798            Self::Invalid(_) => "extract.invalid",
799            Self::WrongKind => "extract.wrong_kind",
800        }
801    }
802
803    const fn describe(self) -> &'static str {
804        match self {
805            Self::Unknown => "unknown field",
806            Self::Missing => "missing field",
807            Self::Duplicate => "field given more than once",
808            Self::Invalid(why) => why,
809            Self::WrongKind => "file where text was expected, or text where a file was expected",
810        }
811    }
812}
813
814/// How many codes form-net itself returns, in every feature mode.
815#[cfg(target_has_atomic = "32")]
816pub(crate) const SLOTS: usize = 61;
817
818/// Every code form-net itself returns, at its [`FormError::slot`].
819#[cfg(target_has_atomic = "32")]
820pub(crate) const SLOT_CODES: [&str; SLOTS] = [
821    "limit.body",
822    "limit.fields",
823    "limit.name_length",
824    "limit.value_length",
825    "limit.text_total",
826    "limit.parts",
827    "limit.files",
828    "limit.file_size",
829    "limit.part_header_bytes",
830    "limit.part_header_count",
831    "limit.filename_length",
832    "limit.preamble",
833    "limit.epilogue",
834    "limit.padding",
835    "limit.token_length",
836    "limit.disk",
837    "syntax.invalid_content_type",
838    "syntax.missing_boundary",
839    "syntax.duplicate_boundary",
840    "syntax.invalid_boundary",
841    "syntax.no_initial_delimiter",
842    "syntax.boundary_in_content",
843    "syntax.bare_line_feed",
844    "syntax.malformed_header_line",
845    "syntax.header_folding",
846    "syntax.invalid_header_name",
847    "syntax.control_in_header",
848    "syntax.missing_content_disposition",
849    "syntax.duplicate_header",
850    "syntax.not_form_data",
851    "syntax.missing_name",
852    "syntax.empty_name",
853    "syntax.duplicate_parameter",
854    "syntax.malformed_parameter",
855    "syntax.ambiguous_quote",
856    "syntax.truncated",
857    "encoding.invalid_percent",
858    "encoding.invalid_utf8",
859    "encoding.unsupported_charset",
860    "encoding.invalid_extended_value",
861    "policy.transfer_encoding",
862    "policy.nested_multipart",
863    "policy.unknown_header",
864    "policy.both_filenames",
865    "policy.extended_filename",
866    "policy.token_field_late",
867    "policy.duplicate_token",
868    "extract.unknown",
869    "extract.missing",
870    "extract.duplicate",
871    "extract.invalid",
872    "extract.wrong_kind",
873    "store.quota_exceeded",
874    "store.not_a_directory",
875    "store.destination_exists",
876    "unsupported_media_type",
877    "invalid_limits",
878    "buffer_too_small",
879    "io",
880    "random",
881    "poisoned",
882];
883
884#[cfg(test)]
885pub(crate) mod tests {
886    use super::*;
887    use alloc::string::ToString;
888
889    /// One error of every kind form-net returns (the exhaustive `slot` and
890    /// `index` matches make a new variant a compile error until it has a
891    /// slot; this list makes it a test failure until it is here too).
892    pub(crate) fn every_error() -> alloc::vec::Vec<FormError> {
893        use alloc::vec::Vec;
894        let at = Location::NONE;
895        let mut all = Vec::new();
896        for limit in [
897            Limit::Body,
898            Limit::Fields,
899            Limit::NameLength,
900            Limit::ValueLength,
901            Limit::TextTotal,
902            Limit::Parts,
903            Limit::Files,
904            Limit::FileSize,
905            Limit::PartHeaderBytes,
906            Limit::PartHeaderCount,
907            Limit::FilenameLength,
908            Limit::Preamble,
909            Limit::Epilogue,
910            Limit::Padding,
911            Limit::TokenLength,
912            Limit::Disk,
913        ] {
914            all.push(FormError::limit(limit, at));
915        }
916        for reason in [
917            SyntaxError::InvalidContentType,
918            SyntaxError::MissingBoundary,
919            SyntaxError::DuplicateBoundary,
920            SyntaxError::InvalidBoundary,
921            SyntaxError::NoInitialDelimiter,
922            SyntaxError::BoundaryInContent,
923            SyntaxError::BareLineFeed,
924            SyntaxError::MalformedHeaderLine,
925            SyntaxError::HeaderFolding,
926            SyntaxError::InvalidHeaderName,
927            SyntaxError::ControlInHeader,
928            SyntaxError::MissingContentDisposition,
929            SyntaxError::DuplicateHeader,
930            SyntaxError::NotFormData,
931            SyntaxError::MissingName,
932            SyntaxError::EmptyName,
933            SyntaxError::DuplicateParameter,
934            SyntaxError::MalformedParameter,
935            SyntaxError::AmbiguousQuote,
936            SyntaxError::Truncated,
937        ] {
938            all.push(FormError::syntax(reason, at));
939        }
940        for reason in [
941            EncodingError::InvalidPercent,
942            EncodingError::InvalidUtf8,
943            EncodingError::UnsupportedCharset,
944            EncodingError::InvalidExtendedValue,
945        ] {
946            all.push(FormError::encoding(reason, at));
947        }
948        for reason in [
949            PolicyRefusal::TransferEncoding,
950            PolicyRefusal::NestedMultipart,
951            PolicyRefusal::UnknownHeader,
952            PolicyRefusal::BothFilenames,
953            PolicyRefusal::ExtendedFilename,
954            PolicyRefusal::TokenFieldLate,
955            PolicyRefusal::DuplicateToken,
956        ] {
957            all.push(FormError::policy(reason, at));
958        }
959        for reason in [
960            ExtractError::Unknown,
961            ExtractError::Missing,
962            ExtractError::Duplicate,
963            ExtractError::Invalid("not a number"),
964            ExtractError::WrongKind,
965        ] {
966            all.push(FormError::Extract {
967                reason,
968                field: Some("f"),
969            });
970        }
971        #[cfg(feature = "fs")]
972        for reason in [
973            crate::store::StoreError::QuotaExceeded,
974            crate::store::StoreError::NotADirectory,
975            crate::store::StoreError::DestinationExists,
976        ] {
977            all.push(FormError::Store { reason });
978        }
979        all.push(FormError::UnsupportedMediaType);
980        all.push(FormError::InvalidLimits { limit: Limit::Body });
981        all.push(FormError::BufferTooSmall { needed: 1 });
982        #[cfg(feature = "std")]
983        all.push(FormError::Io {
984            kind: std::io::ErrorKind::Other,
985        });
986        #[cfg(feature = "std")]
987        all.push(FormError::Random);
988        all.push(FormError::Poisoned);
989        all
990    }
991
992    #[cfg(target_has_atomic = "32")]
993    #[test]
994    fn test_every_error_has_its_own_slot_holding_its_code() {
995        let mut seen = [false; SLOTS];
996        for e in every_error() {
997            let slot = e.slot().unwrap();
998            assert_eq!(SLOT_CODES[slot], e.code(), "{e:?}");
999            assert!(!seen[slot], "slot {slot} twice");
1000            seen[slot] = true;
1001        }
1002        // Every slot is used in the full build; a build without `fs` or
1003        // `std` leaves exactly those slots unused.
1004        let unused: alloc::vec::Vec<&str> = (0..SLOTS)
1005            .filter(|&i| !seen[i])
1006            .map(|i| SLOT_CODES[i])
1007            .collect();
1008        let mut expected = alloc::vec::Vec::new();
1009        if !cfg!(feature = "fs") {
1010            expected.extend([
1011                "store.quota_exceeded",
1012                "store.not_a_directory",
1013                "store.destination_exists",
1014            ]);
1015        }
1016        if !cfg!(feature = "std") {
1017            expected.extend(["io", "random"]);
1018        }
1019        assert_eq!(unused, expected);
1020        for (i, a) in SLOT_CODES.iter().enumerate() {
1021            assert!(!SLOT_CODES[..i].contains(a), "{a} twice");
1022        }
1023        let refused = FormError::Refused {
1024            refusal: Refusal::new(422, "x"),
1025            at: Location::NONE,
1026        };
1027        assert_eq!(refused.slot(), None);
1028    }
1029
1030    #[test]
1031    fn test_status_hint_every_family_maps() {
1032        let at = Location::NONE;
1033        assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
1034        assert_eq!(
1035            FormError::syntax(SyntaxError::Truncated, at).status_hint(),
1036            400
1037        );
1038        assert_eq!(
1039            FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
1040            415
1041        );
1042        assert_eq!(
1043            FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
1044            400
1045        );
1046        assert_eq!(
1047            FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
1048            415
1049        );
1050        assert_eq!(
1051            FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
1052            400
1053        );
1054        let extract = FormError::Extract {
1055            reason: ExtractError::Missing,
1056            field: Some("email"),
1057        };
1058        assert_eq!(extract.status_hint(), 422);
1059        assert_eq!(
1060            FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
1061            500
1062        );
1063        assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
1064    }
1065
1066    #[test]
1067    fn test_status_hint_refusal_out_of_range_reports_400() {
1068        let at = Location::NONE;
1069        let ok = FormError::Refused {
1070            refusal: Refusal::new(422, "x"),
1071            at,
1072        };
1073        assert_eq!(ok.status_hint(), 422);
1074        for status in [0, 200, 399, 600, 999] {
1075            let e = FormError::Refused {
1076                refusal: Refusal::new(status, "x"),
1077                at,
1078            };
1079            assert_eq!(e.status_hint(), 400);
1080        }
1081    }
1082
1083    #[test]
1084    fn test_code_refusal_returns_inspector_code() {
1085        let e = FormError::Refused {
1086            refusal: Refusal::new(422, "customs.unexpected_field"),
1087            at: Location::NONE,
1088        };
1089        assert_eq!(e.code(), "customs.unexpected_field");
1090    }
1091
1092    #[test]
1093    fn test_display_never_contains_the_field_value() {
1094        let e = FormError::Extract {
1095            reason: ExtractError::Invalid("not a number"),
1096            field: Some("age"),
1097        };
1098        assert_eq!(e.to_string(), "not a number (field `age`)");
1099        let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
1100        assert_eq!(
1101            e.to_string(),
1102            "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
1103        );
1104    }
1105
1106    #[test]
1107    fn test_shifted_moves_input_errors_only() {
1108        let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
1109        assert_eq!(e.location().map(|l| l.offset), Some(12));
1110        assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
1111    }
1112
1113    #[test]
1114    fn test_form_error_is_small_and_copy() {
1115        fn is_copy<T: Copy>() {}
1116        is_copy::<FormError>();
1117        assert!(core::mem::size_of::<FormError>() <= 48);
1118    }
1119}