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: more than 16 on one header, an empty
572    /// parameter, or a value that holds `name=` (D-136).
573    MalformedParameter,
574    /// A quoted parameter value that contains `\"`.
575    AmbiguousQuote,
576    /// The body ended before the close delimiter.
577    Truncated,
578}
579
580impl SyntaxError {
581    #[cfg(target_has_atomic = "32")]
582    const fn index(self) -> usize {
583        match self {
584            Self::InvalidContentType => 0,
585            Self::MissingBoundary => 1,
586            Self::DuplicateBoundary => 2,
587            Self::InvalidBoundary => 3,
588            Self::NoInitialDelimiter => 4,
589            Self::BoundaryInContent => 5,
590            Self::BareLineFeed => 6,
591            Self::MalformedHeaderLine => 7,
592            Self::HeaderFolding => 8,
593            Self::InvalidHeaderName => 9,
594            Self::ControlInHeader => 10,
595            Self::MissingContentDisposition => 11,
596            Self::DuplicateHeader => 12,
597            Self::NotFormData => 13,
598            Self::MissingName => 14,
599            Self::EmptyName => 15,
600            Self::DuplicateParameter => 16,
601            Self::MalformedParameter => 17,
602            Self::AmbiguousQuote => 18,
603            Self::Truncated => 19,
604        }
605    }
606
607    const fn code(self) -> &'static str {
608        match self {
609            Self::InvalidContentType => "syntax.invalid_content_type",
610            Self::MissingBoundary => "syntax.missing_boundary",
611            Self::DuplicateBoundary => "syntax.duplicate_boundary",
612            Self::InvalidBoundary => "syntax.invalid_boundary",
613            Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
614            Self::BoundaryInContent => "syntax.boundary_in_content",
615            Self::BareLineFeed => "syntax.bare_line_feed",
616            Self::MalformedHeaderLine => "syntax.malformed_header_line",
617            Self::HeaderFolding => "syntax.header_folding",
618            Self::InvalidHeaderName => "syntax.invalid_header_name",
619            Self::ControlInHeader => "syntax.control_in_header",
620            Self::MissingContentDisposition => "syntax.missing_content_disposition",
621            Self::DuplicateHeader => "syntax.duplicate_header",
622            Self::NotFormData => "syntax.not_form_data",
623            Self::MissingName => "syntax.missing_name",
624            Self::EmptyName => "syntax.empty_name",
625            Self::DuplicateParameter => "syntax.duplicate_parameter",
626            Self::MalformedParameter => "syntax.malformed_parameter",
627            Self::AmbiguousQuote => "syntax.ambiguous_quote",
628            Self::Truncated => "syntax.truncated",
629        }
630    }
631
632    const fn describe(self) -> &'static str {
633        match self {
634            Self::InvalidContentType => "malformed Content-Type",
635            Self::MissingBoundary => "multipart/form-data without a boundary",
636            Self::DuplicateBoundary => "two boundary parameters",
637            Self::InvalidBoundary => "invalid multipart boundary",
638            Self::NoInitialDelimiter => {
639                "no multipart delimiter found: the body is not multipart or the boundary is wrong"
640            }
641            Self::BoundaryInContent => "the boundary appears inside a part",
642            Self::BareLineFeed => "LF without CR where CRLF is required",
643            Self::MalformedHeaderLine => "malformed part header line",
644            Self::HeaderFolding => "folded part header line",
645            Self::InvalidHeaderName => "invalid part header name",
646            Self::ControlInHeader => "control byte in a part header",
647            Self::MissingContentDisposition => "part without Content-Disposition",
648            Self::DuplicateHeader => "part header given twice",
649            Self::NotFormData => "Content-Disposition type is not form-data",
650            Self::MissingName => "part without a name",
651            Self::EmptyName => "part with an empty name",
652            Self::DuplicateParameter => "parameter given twice",
653            Self::MalformedParameter => "malformed header parameter",
654            Self::AmbiguousQuote => "quoted value contains \\\"",
655            Self::Truncated => "body ended before the closing boundary",
656        }
657    }
658}
659
660/// Bytes that do not decode.
661#[non_exhaustive]
662#[derive(Clone, Copy, Debug, PartialEq, Eq)]
663pub enum EncodingError {
664    /// `%` not followed by two hex digits, under `PercentPolicy::Refuse`.
665    InvalidPercent,
666    /// Text that is not valid UTF-8.
667    InvalidUtf8,
668    /// A charset other than UTF-8 or US-ASCII.
669    UnsupportedCharset,
670    /// A malformed `filename*`.
671    InvalidExtendedValue,
672}
673
674impl EncodingError {
675    #[cfg(target_has_atomic = "32")]
676    const fn index(self) -> usize {
677        match self {
678            Self::InvalidPercent => 0,
679            Self::InvalidUtf8 => 1,
680            Self::UnsupportedCharset => 2,
681            Self::InvalidExtendedValue => 3,
682        }
683    }
684
685    const fn code(self) -> &'static str {
686        match self {
687            Self::InvalidPercent => "encoding.invalid_percent",
688            Self::InvalidUtf8 => "encoding.invalid_utf8",
689            Self::UnsupportedCharset => "encoding.unsupported_charset",
690            Self::InvalidExtendedValue => "encoding.invalid_extended_value",
691        }
692    }
693
694    const fn describe(self) -> &'static str {
695        match self {
696            Self::InvalidPercent => "invalid percent escape",
697            Self::InvalidUtf8 => "text is not valid UTF-8",
698            Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
699            Self::InvalidExtendedValue => "malformed filename*",
700        }
701    }
702}
703
704/// Well-formed input that a [`Policy`](crate::Policy) refuses.
705#[non_exhaustive]
706#[derive(Clone, Copy, Debug, PartialEq, Eq)]
707pub enum PolicyRefusal {
708    /// A `Content-Transfer-Encoding` other than `7bit`, `8bit`, `binary`.
709    TransferEncoding,
710    /// A part declared `multipart/*`.
711    NestedMultipart,
712    /// An unknown part header under `HeaderPolicy::Refuse`.
713    UnknownHeader,
714    /// Both `filename` and `filename*` in one part.
715    BothFilenames,
716    /// `filename*` under `ExtendedFilenamePolicy::Refuse`.
717    ExtendedFilename,
718    /// A file part before the token field under
719    /// `TokenPlacement::BeforeFirstFile`.
720    TokenFieldLate,
721    /// The token field given twice.
722    DuplicateToken,
723}
724
725impl PolicyRefusal {
726    #[cfg(target_has_atomic = "32")]
727    const fn index(self) -> usize {
728        match self {
729            Self::TransferEncoding => 0,
730            Self::NestedMultipart => 1,
731            Self::UnknownHeader => 2,
732            Self::BothFilenames => 3,
733            Self::ExtendedFilename => 4,
734            Self::TokenFieldLate => 5,
735            Self::DuplicateToken => 6,
736        }
737    }
738
739    const fn code(self) -> &'static str {
740        match self {
741            Self::TransferEncoding => "policy.transfer_encoding",
742            Self::NestedMultipart => "policy.nested_multipart",
743            Self::UnknownHeader => "policy.unknown_header",
744            Self::BothFilenames => "policy.both_filenames",
745            Self::ExtendedFilename => "policy.extended_filename",
746            Self::TokenFieldLate => "policy.token_field_late",
747            Self::DuplicateToken => "policy.duplicate_token",
748        }
749    }
750
751    const fn describe(self) -> &'static str {
752        match self {
753            Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
754            Self::NestedMultipart => "nested multipart part",
755            Self::UnknownHeader => "unknown part header",
756            Self::BothFilenames => "both filename and filename*",
757            Self::ExtendedFilename => "filename* is not accepted",
758            Self::TokenFieldLate => {
759                "file part before the anti-forgery token; treat as a possible forgery"
760            }
761            Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
762        }
763    }
764}
765
766/// Why typed extraction failed.
767#[non_exhaustive]
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
769pub enum ExtractError {
770    /// A name the struct neither declares nor ignores.
771    Unknown,
772    /// A required field is absent.
773    Missing,
774    /// A single-valued field was given twice.
775    Duplicate,
776    /// A value that does not convert; the text says why.
777    Invalid(&'static str),
778    /// A file where text was expected, or the reverse.
779    WrongKind,
780}
781
782impl ExtractError {
783    #[cfg(target_has_atomic = "32")]
784    const fn index(self) -> usize {
785        match self {
786            Self::Unknown => 0,
787            Self::Missing => 1,
788            Self::Duplicate => 2,
789            Self::Invalid(_) => 3,
790            Self::WrongKind => 4,
791        }
792    }
793
794    const fn code(self) -> &'static str {
795        match self {
796            Self::Unknown => "extract.unknown",
797            Self::Missing => "extract.missing",
798            Self::Duplicate => "extract.duplicate",
799            Self::Invalid(_) => "extract.invalid",
800            Self::WrongKind => "extract.wrong_kind",
801        }
802    }
803
804    const fn describe(self) -> &'static str {
805        match self {
806            Self::Unknown => "unknown field",
807            Self::Missing => "missing field",
808            Self::Duplicate => "field given more than once",
809            Self::Invalid(why) => why,
810            Self::WrongKind => "file where text was expected, or text where a file was expected",
811        }
812    }
813}
814
815/// How many codes form-net itself returns, in every feature mode.
816#[cfg(target_has_atomic = "32")]
817pub(crate) const SLOTS: usize = 61;
818
819/// Every code form-net itself returns, at its [`FormError::slot`].
820#[cfg(target_has_atomic = "32")]
821pub(crate) const SLOT_CODES: [&str; SLOTS] = [
822    "limit.body",
823    "limit.fields",
824    "limit.name_length",
825    "limit.value_length",
826    "limit.text_total",
827    "limit.parts",
828    "limit.files",
829    "limit.file_size",
830    "limit.part_header_bytes",
831    "limit.part_header_count",
832    "limit.filename_length",
833    "limit.preamble",
834    "limit.epilogue",
835    "limit.padding",
836    "limit.token_length",
837    "limit.disk",
838    "syntax.invalid_content_type",
839    "syntax.missing_boundary",
840    "syntax.duplicate_boundary",
841    "syntax.invalid_boundary",
842    "syntax.no_initial_delimiter",
843    "syntax.boundary_in_content",
844    "syntax.bare_line_feed",
845    "syntax.malformed_header_line",
846    "syntax.header_folding",
847    "syntax.invalid_header_name",
848    "syntax.control_in_header",
849    "syntax.missing_content_disposition",
850    "syntax.duplicate_header",
851    "syntax.not_form_data",
852    "syntax.missing_name",
853    "syntax.empty_name",
854    "syntax.duplicate_parameter",
855    "syntax.malformed_parameter",
856    "syntax.ambiguous_quote",
857    "syntax.truncated",
858    "encoding.invalid_percent",
859    "encoding.invalid_utf8",
860    "encoding.unsupported_charset",
861    "encoding.invalid_extended_value",
862    "policy.transfer_encoding",
863    "policy.nested_multipart",
864    "policy.unknown_header",
865    "policy.both_filenames",
866    "policy.extended_filename",
867    "policy.token_field_late",
868    "policy.duplicate_token",
869    "extract.unknown",
870    "extract.missing",
871    "extract.duplicate",
872    "extract.invalid",
873    "extract.wrong_kind",
874    "store.quota_exceeded",
875    "store.not_a_directory",
876    "store.destination_exists",
877    "unsupported_media_type",
878    "invalid_limits",
879    "buffer_too_small",
880    "io",
881    "random",
882    "poisoned",
883];
884
885#[cfg(test)]
886pub(crate) mod tests {
887    use super::*;
888    use alloc::string::ToString;
889
890    /// One error of every kind form-net returns (the exhaustive `slot` and
891    /// `index` matches make a new variant a compile error until it has a
892    /// slot; this list makes it a test failure until it is here too).
893    pub(crate) fn every_error() -> alloc::vec::Vec<FormError> {
894        use alloc::vec::Vec;
895        let at = Location::NONE;
896        let mut all = Vec::new();
897        for limit in [
898            Limit::Body,
899            Limit::Fields,
900            Limit::NameLength,
901            Limit::ValueLength,
902            Limit::TextTotal,
903            Limit::Parts,
904            Limit::Files,
905            Limit::FileSize,
906            Limit::PartHeaderBytes,
907            Limit::PartHeaderCount,
908            Limit::FilenameLength,
909            Limit::Preamble,
910            Limit::Epilogue,
911            Limit::Padding,
912            Limit::TokenLength,
913            Limit::Disk,
914        ] {
915            all.push(FormError::limit(limit, at));
916        }
917        for reason in [
918            SyntaxError::InvalidContentType,
919            SyntaxError::MissingBoundary,
920            SyntaxError::DuplicateBoundary,
921            SyntaxError::InvalidBoundary,
922            SyntaxError::NoInitialDelimiter,
923            SyntaxError::BoundaryInContent,
924            SyntaxError::BareLineFeed,
925            SyntaxError::MalformedHeaderLine,
926            SyntaxError::HeaderFolding,
927            SyntaxError::InvalidHeaderName,
928            SyntaxError::ControlInHeader,
929            SyntaxError::MissingContentDisposition,
930            SyntaxError::DuplicateHeader,
931            SyntaxError::NotFormData,
932            SyntaxError::MissingName,
933            SyntaxError::EmptyName,
934            SyntaxError::DuplicateParameter,
935            SyntaxError::MalformedParameter,
936            SyntaxError::AmbiguousQuote,
937            SyntaxError::Truncated,
938        ] {
939            all.push(FormError::syntax(reason, at));
940        }
941        for reason in [
942            EncodingError::InvalidPercent,
943            EncodingError::InvalidUtf8,
944            EncodingError::UnsupportedCharset,
945            EncodingError::InvalidExtendedValue,
946        ] {
947            all.push(FormError::encoding(reason, at));
948        }
949        for reason in [
950            PolicyRefusal::TransferEncoding,
951            PolicyRefusal::NestedMultipart,
952            PolicyRefusal::UnknownHeader,
953            PolicyRefusal::BothFilenames,
954            PolicyRefusal::ExtendedFilename,
955            PolicyRefusal::TokenFieldLate,
956            PolicyRefusal::DuplicateToken,
957        ] {
958            all.push(FormError::policy(reason, at));
959        }
960        for reason in [
961            ExtractError::Unknown,
962            ExtractError::Missing,
963            ExtractError::Duplicate,
964            ExtractError::Invalid("not a number"),
965            ExtractError::WrongKind,
966        ] {
967            all.push(FormError::Extract {
968                reason,
969                field: Some("f"),
970            });
971        }
972        #[cfg(feature = "fs")]
973        for reason in [
974            crate::store::StoreError::QuotaExceeded,
975            crate::store::StoreError::NotADirectory,
976            crate::store::StoreError::DestinationExists,
977        ] {
978            all.push(FormError::Store { reason });
979        }
980        all.push(FormError::UnsupportedMediaType);
981        all.push(FormError::InvalidLimits { limit: Limit::Body });
982        all.push(FormError::BufferTooSmall { needed: 1 });
983        #[cfg(feature = "std")]
984        all.push(FormError::Io {
985            kind: std::io::ErrorKind::Other,
986        });
987        #[cfg(feature = "std")]
988        all.push(FormError::Random);
989        all.push(FormError::Poisoned);
990        all
991    }
992
993    #[cfg(target_has_atomic = "32")]
994    #[test]
995    fn test_every_error_has_its_own_slot_holding_its_code() {
996        let mut seen = [false; SLOTS];
997        for e in every_error() {
998            let slot = e.slot().unwrap();
999            assert_eq!(SLOT_CODES[slot], e.code(), "{e:?}");
1000            assert!(!seen[slot], "slot {slot} twice");
1001            seen[slot] = true;
1002        }
1003        // Every slot is used in the full build; a build without `fs` or
1004        // `std` leaves exactly those slots unused.
1005        let unused: alloc::vec::Vec<&str> = (0..SLOTS)
1006            .filter(|&i| !seen[i])
1007            .map(|i| SLOT_CODES[i])
1008            .collect();
1009        let mut expected = alloc::vec::Vec::new();
1010        if !cfg!(feature = "fs") {
1011            expected.extend([
1012                "store.quota_exceeded",
1013                "store.not_a_directory",
1014                "store.destination_exists",
1015            ]);
1016        }
1017        if !cfg!(feature = "std") {
1018            expected.extend(["io", "random"]);
1019        }
1020        assert_eq!(unused, expected);
1021        for (i, a) in SLOT_CODES.iter().enumerate() {
1022            assert!(!SLOT_CODES[..i].contains(a), "{a} twice");
1023        }
1024        let refused = FormError::Refused {
1025            refusal: Refusal::new(422, "x"),
1026            at: Location::NONE,
1027        };
1028        assert_eq!(refused.slot(), None);
1029    }
1030
1031    #[test]
1032    fn test_status_hint_every_family_maps() {
1033        let at = Location::NONE;
1034        assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
1035        assert_eq!(
1036            FormError::syntax(SyntaxError::Truncated, at).status_hint(),
1037            400
1038        );
1039        assert_eq!(
1040            FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
1041            415
1042        );
1043        assert_eq!(
1044            FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
1045            400
1046        );
1047        assert_eq!(
1048            FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
1049            415
1050        );
1051        assert_eq!(
1052            FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
1053            400
1054        );
1055        let extract = FormError::Extract {
1056            reason: ExtractError::Missing,
1057            field: Some("email"),
1058        };
1059        assert_eq!(extract.status_hint(), 422);
1060        assert_eq!(
1061            FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
1062            500
1063        );
1064        assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
1065    }
1066
1067    #[test]
1068    fn test_status_hint_refusal_out_of_range_reports_400() {
1069        let at = Location::NONE;
1070        let ok = FormError::Refused {
1071            refusal: Refusal::new(422, "x"),
1072            at,
1073        };
1074        assert_eq!(ok.status_hint(), 422);
1075        for status in [0, 200, 399, 600, 999] {
1076            let e = FormError::Refused {
1077                refusal: Refusal::new(status, "x"),
1078                at,
1079            };
1080            assert_eq!(e.status_hint(), 400);
1081        }
1082    }
1083
1084    #[test]
1085    fn test_code_refusal_returns_inspector_code() {
1086        let e = FormError::Refused {
1087            refusal: Refusal::new(422, "customs.unexpected_field"),
1088            at: Location::NONE,
1089        };
1090        assert_eq!(e.code(), "customs.unexpected_field");
1091    }
1092
1093    #[test]
1094    fn test_display_never_contains_the_field_value() {
1095        let e = FormError::Extract {
1096            reason: ExtractError::Invalid("not a number"),
1097            field: Some("age"),
1098        };
1099        assert_eq!(e.to_string(), "not a number (field `age`)");
1100        let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
1101        assert_eq!(
1102            e.to_string(),
1103            "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
1104        );
1105    }
1106
1107    #[test]
1108    fn test_shifted_moves_input_errors_only() {
1109        let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
1110        assert_eq!(e.location().map(|l| l.offset), Some(12));
1111        assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
1112    }
1113
1114    #[test]
1115    fn test_form_error_is_small_and_copy() {
1116        fn is_copy<T: Copy>() {}
1117        is_copy::<FormError>();
1118        assert!(core::mem::size_of::<FormError>() <= 48);
1119    }
1120}