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")]
96 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
97 BodyRead {
98 kind: std::io::ErrorKind,
100 },
101 #[cfg(feature = "std")]
103 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
104 Random,
105 #[cfg(feature = "fs")]
108 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
109 Store {
110 reason: crate::store::StoreError,
112 },
113 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 #[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 #[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 #[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 #[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 #[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#[non_exhaustive]
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub struct Location {
362 pub part: Option<u32>,
364 pub offset: u64,
366}
367
368impl Location {
369 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#[non_exhaustive]
413#[derive(Clone, Copy, Debug, PartialEq, Eq)]
414pub enum Limit {
415 Body,
417 Fields,
419 NameLength,
421 ValueLength,
423 TextTotal,
425 Parts,
427 Files,
429 FileSize,
431 PartHeaderBytes,
433 PartHeaderCount,
435 FilenameLength,
437 Preamble,
439 Epilogue,
441 Padding,
443 TokenLength,
445 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#[non_exhaustive]
568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
569pub enum SyntaxError {
570 InvalidContentType,
572 MissingBoundary,
574 DuplicateBoundary,
576 InvalidBoundary,
579 NoInitialDelimiter,
581 BoundaryInContent,
585 BareLineFeed,
587 MalformedHeaderLine,
589 HeaderFolding,
591 InvalidHeaderName,
594 ControlInHeader,
596 MissingContentDisposition,
598 DuplicateHeader,
600 NotFormData,
602 MissingName,
604 EmptyName,
606 DuplicateParameter,
608 MalformedParameter,
611 AmbiguousQuote,
613 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#[non_exhaustive]
710#[derive(Clone, Copy, Debug, PartialEq, Eq)]
711pub enum EncodingError {
712 InvalidPercent,
714 InvalidUtf8,
716 UnsupportedCharset,
718 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#[non_exhaustive]
766#[derive(Clone, Copy, Debug, PartialEq, Eq)]
767pub enum PolicyRefusal {
768 TransferEncoding,
770 NestedMultipart,
772 UnknownHeader,
774 BothFilenames,
776 ExtendedFilename,
778 TokenFieldLate,
781 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#[non_exhaustive]
840#[derive(Clone, Copy, Debug, PartialEq, Eq)]
841pub enum ExtractError {
842 Unknown,
844 Missing,
846 Duplicate,
848 Invalid(&'static str),
850 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#[cfg(target_has_atomic = "32")]
889pub(crate) const SLOTS: usize = 62;
890
891#[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 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 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}