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