1use core::fmt;
8
9use crate::hook::Refusal;
10
11#[non_exhaustive]
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum FormError {
31 Limit {
33 limit: Limit,
35 at: Location,
37 },
38 Syntax {
40 reason: SyntaxError,
42 at: Location,
44 },
45 Encoding {
47 reason: EncodingError,
49 at: Location,
51 },
52 Policy {
54 reason: PolicyRefusal,
56 at: Location,
58 },
59 Refused {
61 refusal: Refusal,
63 at: Location,
65 },
66 Extract {
68 reason: ExtractError,
70 field: Option<&'static str>,
72 },
73 UnsupportedMediaType,
75 InvalidLimits {
78 limit: Limit,
80 },
81 BufferTooSmall {
83 needed: usize,
85 },
86 #[cfg(feature = "std")]
88 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
89 Io {
90 kind: std::io::ErrorKind,
92 },
93 #[cfg(feature = "std")]
95 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
96 Random,
97 #[cfg(feature = "fs")]
100 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
101 Store {
102 reason: crate::store::StoreError,
104 },
105 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 #[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 #[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 #[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 #[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#[non_exhaustive]
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub struct Location {
320 pub part: Option<u32>,
322 pub offset: u64,
324}
325
326impl Location {
327 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#[non_exhaustive]
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum Limit {
362 Body,
364 Fields,
366 NameLength,
368 ValueLength,
370 TextTotal,
372 Parts,
374 Files,
376 FileSize,
378 PartHeaderBytes,
380 PartHeaderCount,
382 FilenameLength,
384 Preamble,
386 Epilogue,
388 Padding,
390 TokenLength,
392 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#[non_exhaustive]
483#[derive(Clone, Copy, Debug, PartialEq, Eq)]
484pub enum SyntaxError {
485 InvalidContentType,
487 MissingBoundary,
489 DuplicateBoundary,
491 InvalidBoundary,
494 NoInitialDelimiter,
496 BoundaryInContent,
500 BareLineFeed,
502 MalformedHeaderLine,
504 HeaderFolding,
506 InvalidHeaderName,
509 ControlInHeader,
511 MissingContentDisposition,
513 DuplicateHeader,
515 NotFormData,
517 MissingName,
519 EmptyName,
521 DuplicateParameter,
523 MalformedParameter,
525 AmbiguousQuote,
527 Truncated,
529}
530
531impl SyntaxError {
532 const fn code(self) -> &'static str {
533 match self {
534 Self::InvalidContentType => "syntax.invalid_content_type",
535 Self::MissingBoundary => "syntax.missing_boundary",
536 Self::DuplicateBoundary => "syntax.duplicate_boundary",
537 Self::InvalidBoundary => "syntax.invalid_boundary",
538 Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
539 Self::BoundaryInContent => "syntax.boundary_in_content",
540 Self::BareLineFeed => "syntax.bare_line_feed",
541 Self::MalformedHeaderLine => "syntax.malformed_header_line",
542 Self::HeaderFolding => "syntax.header_folding",
543 Self::InvalidHeaderName => "syntax.invalid_header_name",
544 Self::ControlInHeader => "syntax.control_in_header",
545 Self::MissingContentDisposition => "syntax.missing_content_disposition",
546 Self::DuplicateHeader => "syntax.duplicate_header",
547 Self::NotFormData => "syntax.not_form_data",
548 Self::MissingName => "syntax.missing_name",
549 Self::EmptyName => "syntax.empty_name",
550 Self::DuplicateParameter => "syntax.duplicate_parameter",
551 Self::MalformedParameter => "syntax.malformed_parameter",
552 Self::AmbiguousQuote => "syntax.ambiguous_quote",
553 Self::Truncated => "syntax.truncated",
554 }
555 }
556
557 const fn describe(self) -> &'static str {
558 match self {
559 Self::InvalidContentType => "malformed Content-Type",
560 Self::MissingBoundary => "multipart/form-data without a boundary",
561 Self::DuplicateBoundary => "two boundary parameters",
562 Self::InvalidBoundary => "invalid multipart boundary",
563 Self::NoInitialDelimiter => {
564 "no multipart delimiter found: the body is not multipart or the boundary is wrong"
565 }
566 Self::BoundaryInContent => "the boundary appears inside a part",
567 Self::BareLineFeed => "LF without CR where CRLF is required",
568 Self::MalformedHeaderLine => "malformed part header line",
569 Self::HeaderFolding => "folded part header line",
570 Self::InvalidHeaderName => "invalid part header name",
571 Self::ControlInHeader => "control byte in a part header",
572 Self::MissingContentDisposition => "part without Content-Disposition",
573 Self::DuplicateHeader => "part header given twice",
574 Self::NotFormData => "Content-Disposition type is not form-data",
575 Self::MissingName => "part without a name",
576 Self::EmptyName => "part with an empty name",
577 Self::DuplicateParameter => "parameter given twice",
578 Self::MalformedParameter => "malformed header parameter",
579 Self::AmbiguousQuote => "quoted value contains \\\"",
580 Self::Truncated => "body ended before the closing boundary",
581 }
582 }
583}
584
585#[non_exhaustive]
587#[derive(Clone, Copy, Debug, PartialEq, Eq)]
588pub enum EncodingError {
589 InvalidPercent,
591 InvalidUtf8,
593 UnsupportedCharset,
595 InvalidExtendedValue,
597}
598
599impl EncodingError {
600 const fn code(self) -> &'static str {
601 match self {
602 Self::InvalidPercent => "encoding.invalid_percent",
603 Self::InvalidUtf8 => "encoding.invalid_utf8",
604 Self::UnsupportedCharset => "encoding.unsupported_charset",
605 Self::InvalidExtendedValue => "encoding.invalid_extended_value",
606 }
607 }
608
609 const fn describe(self) -> &'static str {
610 match self {
611 Self::InvalidPercent => "invalid percent escape",
612 Self::InvalidUtf8 => "text is not valid UTF-8",
613 Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
614 Self::InvalidExtendedValue => "malformed filename*",
615 }
616 }
617}
618
619#[non_exhaustive]
621#[derive(Clone, Copy, Debug, PartialEq, Eq)]
622pub enum PolicyRefusal {
623 TransferEncoding,
625 NestedMultipart,
627 UnknownHeader,
629 BothFilenames,
631 ExtendedFilename,
633 TokenFieldLate,
636 DuplicateToken,
638}
639
640impl PolicyRefusal {
641 const fn code(self) -> &'static str {
642 match self {
643 Self::TransferEncoding => "policy.transfer_encoding",
644 Self::NestedMultipart => "policy.nested_multipart",
645 Self::UnknownHeader => "policy.unknown_header",
646 Self::BothFilenames => "policy.both_filenames",
647 Self::ExtendedFilename => "policy.extended_filename",
648 Self::TokenFieldLate => "policy.token_field_late",
649 Self::DuplicateToken => "policy.duplicate_token",
650 }
651 }
652
653 const fn describe(self) -> &'static str {
654 match self {
655 Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
656 Self::NestedMultipart => "nested multipart part",
657 Self::UnknownHeader => "unknown part header",
658 Self::BothFilenames => "both filename and filename*",
659 Self::ExtendedFilename => "filename* is not accepted",
660 Self::TokenFieldLate => {
661 "file part before the anti-forgery token; treat as a possible forgery"
662 }
663 Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
664 }
665 }
666}
667
668#[non_exhaustive]
670#[derive(Clone, Copy, Debug, PartialEq, Eq)]
671pub enum ExtractError {
672 Unknown,
674 Missing,
676 Duplicate,
678 Invalid(&'static str),
680 WrongKind,
682}
683
684impl ExtractError {
685 const fn code(self) -> &'static str {
686 match self {
687 Self::Unknown => "extract.unknown",
688 Self::Missing => "extract.missing",
689 Self::Duplicate => "extract.duplicate",
690 Self::Invalid(_) => "extract.invalid",
691 Self::WrongKind => "extract.wrong_kind",
692 }
693 }
694
695 const fn describe(self) -> &'static str {
696 match self {
697 Self::Unknown => "unknown field",
698 Self::Missing => "missing field",
699 Self::Duplicate => "field given more than once",
700 Self::Invalid(why) => why,
701 Self::WrongKind => "file where text was expected, or text where a file was expected",
702 }
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use alloc::string::ToString;
710
711 #[test]
712 fn test_status_hint_every_family_maps() {
713 let at = Location::NONE;
714 assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
715 assert_eq!(
716 FormError::syntax(SyntaxError::Truncated, at).status_hint(),
717 400
718 );
719 assert_eq!(
720 FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
721 415
722 );
723 assert_eq!(
724 FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
725 400
726 );
727 assert_eq!(
728 FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
729 415
730 );
731 assert_eq!(
732 FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
733 400
734 );
735 let extract = FormError::Extract {
736 reason: ExtractError::Missing,
737 field: Some("email"),
738 };
739 assert_eq!(extract.status_hint(), 422);
740 assert_eq!(
741 FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
742 500
743 );
744 assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
745 }
746
747 #[test]
748 fn test_status_hint_refusal_out_of_range_reports_400() {
749 let at = Location::NONE;
750 let ok = FormError::Refused {
751 refusal: Refusal::new(422, "x"),
752 at,
753 };
754 assert_eq!(ok.status_hint(), 422);
755 for status in [0, 200, 399, 600, 999] {
756 let e = FormError::Refused {
757 refusal: Refusal::new(status, "x"),
758 at,
759 };
760 assert_eq!(e.status_hint(), 400);
761 }
762 }
763
764 #[test]
765 fn test_code_refusal_returns_inspector_code() {
766 let e = FormError::Refused {
767 refusal: Refusal::new(422, "customs.unexpected_field"),
768 at: Location::NONE,
769 };
770 assert_eq!(e.code(), "customs.unexpected_field");
771 }
772
773 #[test]
774 fn test_display_never_contains_the_field_value() {
775 let e = FormError::Extract {
776 reason: ExtractError::Invalid("not a number"),
777 field: Some("age"),
778 };
779 assert_eq!(e.to_string(), "not a number (field `age`)");
780 let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
781 assert_eq!(
782 e.to_string(),
783 "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
784 );
785 }
786
787 #[test]
788 fn test_shifted_moves_input_errors_only() {
789 let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
790 assert_eq!(e.location().map(|l| l.offset), Some(12));
791 assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
792 }
793
794 #[test]
795 fn test_form_error_is_small_and_copy() {
796 fn is_copy<T: Copy>() {}
797 is_copy::<FormError>();
798 assert!(core::mem::size_of::<FormError>() <= 48);
799 }
800}