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    /// Where the error was found, for input errors and refusals.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// let err = form_net::parse_urlencoded(b"ok=1&bad=%zz").unwrap_err();
247    /// assert_eq!(err.location().map(|l| l.offset), Some(9));
248    /// assert_eq!(form_net::FormError::Poisoned.location(), None);
249    /// ```
250    #[must_use]
251    pub const fn location(&self) -> Option<Location> {
252        match self {
253            Self::Limit { at, .. }
254            | Self::Syntax { at, .. }
255            | Self::Encoding { at, .. }
256            | Self::Policy { at, .. }
257            | Self::Refused { at, .. } => Some(*at),
258            _ => None,
259        }
260    }
261}
262
263impl fmt::Display for FormError {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        match self {
266            Self::Limit { limit, at } => {
267                write!(f, "{} ({at}): {}", limit.describe(), limit.action())
268            }
269            Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
270            Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
271            Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
272            Self::Refused { refusal, at } => {
273                write!(f, "refused by the inspector: {} ({at})", refusal.code)
274            }
275            Self::Extract { reason, field } => match field {
276                Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
277                None => f.write_str(reason.describe()),
278            },
279            Self::UnsupportedMediaType => f.write_str(
280                "unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
281            ),
282            Self::InvalidLimits { limit } => write!(
283                f,
284                "invalid limits: {} cannot be satisfied by any body; fix the configuration",
285                limit.field_name()
286            ),
287            Self::BufferTooSmall { needed } => write!(
288                f,
289                "buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
290            ),
291            #[cfg(feature = "std")]
292            Self::Io { kind } => write!(f, "I/O failure: {kind}"),
293            #[cfg(feature = "std")]
294            Self::Random => f.write_str(
295                "the operating system's random source failed; no predictable fallback is used",
296            ),
297            #[cfg(feature = "fs")]
298            Self::Store { reason } => write!(f, "storage: {}", reason.describe()),
299            Self::Poisoned => {
300                f.write_str("parser used after an error; call reset before parsing again")
301            }
302        }
303    }
304}
305
306impl core::error::Error for FormError {}
307
308/// Where an error was found.
309///
310/// # Examples
311///
312/// ```
313/// let err = form_net::parse_urlencoded(b"a=1&b=%").unwrap_err();
314/// let at = err.location().unwrap();
315/// assert_eq!((at.part, at.offset), (None, 6));
316/// ```
317#[non_exhaustive]
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub struct Location {
320    /// The multipart part index (from 0), when the error is inside a part.
321    pub part: Option<u32>,
322    /// The byte offset in the body (or in the component, for `decode_into`).
323    pub offset: u64,
324}
325
326impl Location {
327    /// No location: configuration errors.
328    pub const NONE: Location = Location {
329        part: None,
330        offset: 0,
331    };
332
333    pub(crate) const fn at(offset: u64) -> Self {
334        Self { part: None, offset }
335    }
336
337    pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
338        Self { part, offset }
339    }
340
341    const fn shifted(self, by: u64) -> Self {
342        Self {
343            part: self.part,
344            offset: self.offset.saturating_add(by),
345        }
346    }
347}
348
349impl fmt::Display for Location {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        match self.part {
352            Some(part) => write!(f, "part {part}, byte {}", self.offset),
353            None => write!(f, "byte {}", self.offset),
354        }
355    }
356}
357
358/// Which budget of [`Limits`](crate::Limits) was exceeded.
359#[non_exhaustive]
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum Limit {
362    /// `max_body_bytes`.
363    Body,
364    /// `max_fields`.
365    Fields,
366    /// `max_name_bytes`.
367    NameLength,
368    /// `max_value_bytes`.
369    ValueLength,
370    /// `max_text_bytes`.
371    TextTotal,
372    /// `max_parts`.
373    Parts,
374    /// `max_files`.
375    Files,
376    /// `max_file_bytes`.
377    FileSize,
378    /// `max_part_header_bytes`.
379    PartHeaderBytes,
380    /// `max_part_headers`.
381    PartHeaderCount,
382    /// `max_filename_bytes`.
383    FilenameLength,
384    /// `max_preamble_bytes`.
385    Preamble,
386    /// `max_epilogue_bytes`.
387    Epilogue,
388    /// `max_padding_bytes`.
389    Padding,
390    /// `max_token_bytes`.
391    TokenLength,
392    /// The per-request disk limit of a store.
393    Disk,
394}
395
396impl Limit {
397    const fn code(self) -> &'static str {
398        match self {
399            Self::Body => "limit.body",
400            Self::Fields => "limit.fields",
401            Self::NameLength => "limit.name_length",
402            Self::ValueLength => "limit.value_length",
403            Self::TextTotal => "limit.text_total",
404            Self::Parts => "limit.parts",
405            Self::Files => "limit.files",
406            Self::FileSize => "limit.file_size",
407            Self::PartHeaderBytes => "limit.part_header_bytes",
408            Self::PartHeaderCount => "limit.part_header_count",
409            Self::FilenameLength => "limit.filename_length",
410            Self::Preamble => "limit.preamble",
411            Self::Epilogue => "limit.epilogue",
412            Self::Padding => "limit.padding",
413            Self::TokenLength => "limit.token_length",
414            Self::Disk => "limit.disk",
415        }
416    }
417
418    const fn field_name(self) -> &'static str {
419        match self {
420            Self::Body => "max_body_bytes",
421            Self::Fields => "max_fields",
422            Self::NameLength => "max_name_bytes",
423            Self::ValueLength => "max_value_bytes",
424            Self::TextTotal => "max_text_bytes",
425            Self::Parts => "max_parts",
426            Self::Files => "max_files",
427            Self::FileSize => "max_file_bytes",
428            Self::PartHeaderBytes => "max_part_header_bytes",
429            Self::PartHeaderCount => "max_part_headers",
430            Self::FilenameLength => "max_filename_bytes",
431            Self::Preamble => "max_preamble_bytes",
432            Self::Epilogue => "max_epilogue_bytes",
433            Self::Padding => "max_padding_bytes",
434            Self::TokenLength => "max_token_bytes",
435            Self::Disk => "max_disk_bytes",
436        }
437    }
438
439    const fn describe(self) -> &'static str {
440        match self {
441            Self::Body => "body too large",
442            Self::Fields => "too many fields",
443            Self::NameLength => "field name too long",
444            Self::ValueLength => "field value too long",
445            Self::TextTotal => "text fields too large in total",
446            Self::Parts => "too many parts",
447            Self::Files => "too many files",
448            Self::FileSize => "file too large",
449            Self::PartHeaderBytes => "part headers too large",
450            Self::PartHeaderCount => "too many part header lines",
451            Self::FilenameLength => "file name too long",
452            Self::Preamble => "too many bytes before the first boundary",
453            Self::Epilogue => "too many bytes after the last boundary",
454            Self::Padding => "too much padding after a boundary",
455            Self::TokenLength => "anti-forgery token too long",
456            Self::Disk => "too many bytes stored on disk for one request",
457        }
458    }
459
460    const fn action(self) -> &'static str {
461        match self {
462            Self::Body => "raise max_body_bytes if this route accepts larger bodies",
463            Self::Fields => "raise max_fields if this route accepts more fields",
464            Self::NameLength => "raise max_name_bytes if this route uses longer names",
465            Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
466            Self::TextTotal => "raise max_text_bytes if this route accepts more text",
467            Self::Parts => "raise max_parts if this route accepts more parts",
468            Self::Files => "raise max_files if this route accepts more files",
469            Self::FileSize => "raise max_file_bytes if this route accepts larger files",
470            Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
471            Self::PartHeaderCount => "raise max_part_headers if real clients need it",
472            Self::FilenameLength => "raise max_filename_bytes if real clients need it",
473            Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
474            Self::Padding => "the body is not what browsers send",
475            Self::TokenLength => "treat the request as forged",
476            Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
477        }
478    }
479}
480
481/// What part of the grammar the input broke.
482#[non_exhaustive]
483#[derive(Clone, Copy, Debug, PartialEq, Eq)]
484pub enum SyntaxError {
485    /// A `Content-Type` value (of the request or of a part) is malformed.
486    InvalidContentType,
487    /// `multipart/form-data` without a `boundary` parameter.
488    MissingBoundary,
489    /// Two `boundary` parameters.
490    DuplicateBoundary,
491    /// A boundary that is empty, longer than 70 bytes, has a byte outside
492    /// RFC 2046 `bchars`, or ends with a space.
493    InvalidBoundary,
494    /// The body ended without any delimiter.
495    NoInitialDelimiter,
496    /// A full delimiter followed by something other than `--`, padding, or
497    /// CRLF; or writer data that contains the delimiter.
498    BoundaryInContent,
499    /// LF without CR where CRLF is required.
500    BareLineFeed,
501    /// A part header line without a colon.
502    MalformedHeaderLine,
503    /// A folded part header line.
504    HeaderFolding,
505    /// A part header name that is not a token, or has whitespace before the
506    /// colon.
507    InvalidHeaderName,
508    /// A control byte other than HTAB in a part header value.
509    ControlInHeader,
510    /// A part without `Content-Disposition`.
511    MissingContentDisposition,
512    /// A known part header given twice.
513    DuplicateHeader,
514    /// A `Content-Disposition` type other than `form-data`.
515    NotFormData,
516    /// A `Content-Disposition` without `name`.
517    MissingName,
518    /// An empty `name`.
519    EmptyName,
520    /// A parameter given twice.
521    DuplicateParameter,
522    /// Malformed parameters (or more than 16 on one header).
523    MalformedParameter,
524    /// A quoted parameter value that contains `\"`.
525    AmbiguousQuote,
526    /// The body ended before the close delimiter.
527    Truncated,
528}
529
530impl SyntaxError {
531    const fn code(self) -> &'static str {
532        match self {
533            Self::InvalidContentType => "syntax.invalid_content_type",
534            Self::MissingBoundary => "syntax.missing_boundary",
535            Self::DuplicateBoundary => "syntax.duplicate_boundary",
536            Self::InvalidBoundary => "syntax.invalid_boundary",
537            Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
538            Self::BoundaryInContent => "syntax.boundary_in_content",
539            Self::BareLineFeed => "syntax.bare_line_feed",
540            Self::MalformedHeaderLine => "syntax.malformed_header_line",
541            Self::HeaderFolding => "syntax.header_folding",
542            Self::InvalidHeaderName => "syntax.invalid_header_name",
543            Self::ControlInHeader => "syntax.control_in_header",
544            Self::MissingContentDisposition => "syntax.missing_content_disposition",
545            Self::DuplicateHeader => "syntax.duplicate_header",
546            Self::NotFormData => "syntax.not_form_data",
547            Self::MissingName => "syntax.missing_name",
548            Self::EmptyName => "syntax.empty_name",
549            Self::DuplicateParameter => "syntax.duplicate_parameter",
550            Self::MalformedParameter => "syntax.malformed_parameter",
551            Self::AmbiguousQuote => "syntax.ambiguous_quote",
552            Self::Truncated => "syntax.truncated",
553        }
554    }
555
556    const fn describe(self) -> &'static str {
557        match self {
558            Self::InvalidContentType => "malformed Content-Type",
559            Self::MissingBoundary => "multipart/form-data without a boundary",
560            Self::DuplicateBoundary => "two boundary parameters",
561            Self::InvalidBoundary => "invalid multipart boundary",
562            Self::NoInitialDelimiter => {
563                "no multipart delimiter found: the body is not multipart or the boundary is wrong"
564            }
565            Self::BoundaryInContent => "the boundary appears inside a part",
566            Self::BareLineFeed => "LF without CR where CRLF is required",
567            Self::MalformedHeaderLine => "malformed part header line",
568            Self::HeaderFolding => "folded part header line",
569            Self::InvalidHeaderName => "invalid part header name",
570            Self::ControlInHeader => "control byte in a part header",
571            Self::MissingContentDisposition => "part without Content-Disposition",
572            Self::DuplicateHeader => "part header given twice",
573            Self::NotFormData => "Content-Disposition type is not form-data",
574            Self::MissingName => "part without a name",
575            Self::EmptyName => "part with an empty name",
576            Self::DuplicateParameter => "parameter given twice",
577            Self::MalformedParameter => "malformed header parameter",
578            Self::AmbiguousQuote => "quoted value contains \\\"",
579            Self::Truncated => "body ended before the closing boundary",
580        }
581    }
582}
583
584/// Bytes that do not decode.
585#[non_exhaustive]
586#[derive(Clone, Copy, Debug, PartialEq, Eq)]
587pub enum EncodingError {
588    /// `%` not followed by two hex digits, under `PercentPolicy::Refuse`.
589    InvalidPercent,
590    /// Text that is not valid UTF-8.
591    InvalidUtf8,
592    /// A charset other than UTF-8 or US-ASCII.
593    UnsupportedCharset,
594    /// A malformed `filename*`.
595    InvalidExtendedValue,
596}
597
598impl EncodingError {
599    const fn code(self) -> &'static str {
600        match self {
601            Self::InvalidPercent => "encoding.invalid_percent",
602            Self::InvalidUtf8 => "encoding.invalid_utf8",
603            Self::UnsupportedCharset => "encoding.unsupported_charset",
604            Self::InvalidExtendedValue => "encoding.invalid_extended_value",
605        }
606    }
607
608    const fn describe(self) -> &'static str {
609        match self {
610            Self::InvalidPercent => "invalid percent escape",
611            Self::InvalidUtf8 => "text is not valid UTF-8",
612            Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
613            Self::InvalidExtendedValue => "malformed filename*",
614        }
615    }
616}
617
618/// Well-formed input that a [`Policy`](crate::Policy) refuses.
619#[non_exhaustive]
620#[derive(Clone, Copy, Debug, PartialEq, Eq)]
621pub enum PolicyRefusal {
622    /// A `Content-Transfer-Encoding` other than `7bit`, `8bit`, `binary`.
623    TransferEncoding,
624    /// A part declared `multipart/*`.
625    NestedMultipart,
626    /// An unknown part header under `HeaderPolicy::Refuse`.
627    UnknownHeader,
628    /// Both `filename` and `filename*` in one part.
629    BothFilenames,
630    /// `filename*` under `ExtendedFilenamePolicy::Refuse`.
631    ExtendedFilename,
632    /// A file part before the token field under
633    /// `TokenPlacement::BeforeFirstFile`.
634    TokenFieldLate,
635    /// The token field given twice.
636    DuplicateToken,
637}
638
639impl PolicyRefusal {
640    const fn code(self) -> &'static str {
641        match self {
642            Self::TransferEncoding => "policy.transfer_encoding",
643            Self::NestedMultipart => "policy.nested_multipart",
644            Self::UnknownHeader => "policy.unknown_header",
645            Self::BothFilenames => "policy.both_filenames",
646            Self::ExtendedFilename => "policy.extended_filename",
647            Self::TokenFieldLate => "policy.token_field_late",
648            Self::DuplicateToken => "policy.duplicate_token",
649        }
650    }
651
652    const fn describe(self) -> &'static str {
653        match self {
654            Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
655            Self::NestedMultipart => "nested multipart part",
656            Self::UnknownHeader => "unknown part header",
657            Self::BothFilenames => "both filename and filename*",
658            Self::ExtendedFilename => "filename* is not accepted",
659            Self::TokenFieldLate => {
660                "file part before the anti-forgery token; treat as a possible forgery"
661            }
662            Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
663        }
664    }
665}
666
667/// Why typed extraction failed.
668#[non_exhaustive]
669#[derive(Clone, Copy, Debug, PartialEq, Eq)]
670pub enum ExtractError {
671    /// A name the struct neither declares nor ignores.
672    Unknown,
673    /// A required field is absent.
674    Missing,
675    /// A single-valued field was given twice.
676    Duplicate,
677    /// A value that does not convert; the text says why.
678    Invalid(&'static str),
679    /// A file where text was expected, or the reverse.
680    WrongKind,
681}
682
683impl ExtractError {
684    const fn code(self) -> &'static str {
685        match self {
686            Self::Unknown => "extract.unknown",
687            Self::Missing => "extract.missing",
688            Self::Duplicate => "extract.duplicate",
689            Self::Invalid(_) => "extract.invalid",
690            Self::WrongKind => "extract.wrong_kind",
691        }
692    }
693
694    const fn describe(self) -> &'static str {
695        match self {
696            Self::Unknown => "unknown field",
697            Self::Missing => "missing field",
698            Self::Duplicate => "field given more than once",
699            Self::Invalid(why) => why,
700            Self::WrongKind => "file where text was expected, or text where a file was expected",
701        }
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use alloc::string::ToString;
709
710    #[test]
711    fn test_status_hint_every_family_maps() {
712        let at = Location::NONE;
713        assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
714        assert_eq!(
715            FormError::syntax(SyntaxError::Truncated, at).status_hint(),
716            400
717        );
718        assert_eq!(
719            FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
720            415
721        );
722        assert_eq!(
723            FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
724            400
725        );
726        assert_eq!(
727            FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
728            415
729        );
730        assert_eq!(
731            FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
732            400
733        );
734        let extract = FormError::Extract {
735            reason: ExtractError::Missing,
736            field: Some("email"),
737        };
738        assert_eq!(extract.status_hint(), 422);
739        assert_eq!(
740            FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
741            500
742        );
743        assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
744    }
745
746    #[test]
747    fn test_status_hint_refusal_out_of_range_reports_400() {
748        let at = Location::NONE;
749        let ok = FormError::Refused {
750            refusal: Refusal::new(422, "x"),
751            at,
752        };
753        assert_eq!(ok.status_hint(), 422);
754        for status in [0, 200, 399, 600, 999] {
755            let e = FormError::Refused {
756                refusal: Refusal::new(status, "x"),
757                at,
758            };
759            assert_eq!(e.status_hint(), 400);
760        }
761    }
762
763    #[test]
764    fn test_code_refusal_returns_inspector_code() {
765        let e = FormError::Refused {
766            refusal: Refusal::new(422, "customs.unexpected_field"),
767            at: Location::NONE,
768        };
769        assert_eq!(e.code(), "customs.unexpected_field");
770    }
771
772    #[test]
773    fn test_display_never_contains_the_field_value() {
774        let e = FormError::Extract {
775            reason: ExtractError::Invalid("not a number"),
776            field: Some("age"),
777        };
778        assert_eq!(e.to_string(), "not a number (field `age`)");
779        let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
780        assert_eq!(
781            e.to_string(),
782            "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
783        );
784    }
785
786    #[test]
787    fn test_shifted_moves_input_errors_only() {
788        let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
789        assert_eq!(e.location().map(|l| l.offset), Some(12));
790        assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
791    }
792
793    #[test]
794    fn test_form_error_is_small_and_copy() {
795        fn is_copy<T: Copy>() {}
796        is_copy::<FormError>();
797        assert!(core::mem::size_of::<FormError>() <= 48);
798    }
799}