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 #[cfg(target_has_atomic = "32")]
245 pub(crate) const fn slot(&self) -> Option<usize> {
246 let slot = match self {
247 Self::Limit { limit, .. } => limit.index(),
248 Self::Syntax { reason, .. } => reason.index().wrapping_add(16),
249 Self::Encoding { reason, .. } => reason.index().wrapping_add(36),
250 Self::Policy { reason, .. } => reason.index().wrapping_add(40),
251 Self::Extract { reason, .. } => reason.index().wrapping_add(47),
252 #[cfg(feature = "fs")]
253 Self::Store { reason } => reason.index().wrapping_add(52),
254 Self::UnsupportedMediaType => 55,
255 Self::InvalidLimits { .. } => 56,
256 Self::BufferTooSmall { .. } => 57,
257 #[cfg(feature = "std")]
258 Self::Io { .. } => 58,
259 #[cfg(feature = "std")]
260 Self::Random => 59,
261 Self::Poisoned => 60,
262 Self::Refused { .. } => return None,
263 };
264 Some(slot)
265 }
266
267 #[must_use]
277 pub const fn location(&self) -> Option<Location> {
278 match self {
279 Self::Limit { at, .. }
280 | Self::Syntax { at, .. }
281 | Self::Encoding { at, .. }
282 | Self::Policy { at, .. }
283 | Self::Refused { at, .. } => Some(*at),
284 _ => None,
285 }
286 }
287}
288
289impl fmt::Display for FormError {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 Self::Limit { limit, at } => {
293 write!(f, "{} ({at}): {}", limit.describe(), limit.action())
294 }
295 Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
296 Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
297 Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
298 Self::Refused { refusal, at } => {
299 write!(f, "refused by the inspector: {} ({at})", refusal.code)
300 }
301 Self::Extract { reason, field } => match field {
302 Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
303 None => f.write_str(reason.describe()),
304 },
305 Self::UnsupportedMediaType => f.write_str(
306 "unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
307 ),
308 Self::InvalidLimits { limit } => write!(
309 f,
310 "invalid limits: {} cannot be satisfied by any body; fix the configuration",
311 limit.field_name()
312 ),
313 Self::BufferTooSmall { needed } => write!(
314 f,
315 "buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
316 ),
317 #[cfg(feature = "std")]
318 Self::Io { kind } => write!(f, "I/O failure: {kind}"),
319 #[cfg(feature = "std")]
320 Self::Random => f.write_str(
321 "the operating system's random source failed; no predictable fallback is used",
322 ),
323 #[cfg(feature = "fs")]
324 Self::Store { reason } => write!(f, "storage: {}", reason.describe()),
325 Self::Poisoned => {
326 f.write_str("parser used after an error; call reset before parsing again")
327 }
328 }
329 }
330}
331
332impl core::error::Error for FormError {}
333
334#[non_exhaustive]
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
345pub struct Location {
346 pub part: Option<u32>,
348 pub offset: u64,
350}
351
352impl Location {
353 pub const NONE: Location = Location {
355 part: None,
356 offset: 0,
357 };
358
359 pub(crate) const fn at(offset: u64) -> Self {
360 Self { part: None, offset }
361 }
362
363 pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
364 Self { part, offset }
365 }
366
367 const fn shifted(self, by: u64) -> Self {
368 Self {
369 part: self.part,
370 offset: self.offset.saturating_add(by),
371 }
372 }
373}
374
375impl fmt::Display for Location {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 match self.part {
378 Some(part) => write!(f, "part {part}, byte {}", self.offset),
379 None => write!(f, "byte {}", self.offset),
380 }
381 }
382}
383
384#[non_exhaustive]
386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387pub enum Limit {
388 Body,
390 Fields,
392 NameLength,
394 ValueLength,
396 TextTotal,
398 Parts,
400 Files,
402 FileSize,
404 PartHeaderBytes,
406 PartHeaderCount,
408 FilenameLength,
410 Preamble,
412 Epilogue,
414 Padding,
416 TokenLength,
418 Disk,
420}
421
422impl Limit {
423 #[cfg(target_has_atomic = "32")]
424 const fn index(self) -> usize {
425 match self {
426 Self::Body => 0,
427 Self::Fields => 1,
428 Self::NameLength => 2,
429 Self::ValueLength => 3,
430 Self::TextTotal => 4,
431 Self::Parts => 5,
432 Self::Files => 6,
433 Self::FileSize => 7,
434 Self::PartHeaderBytes => 8,
435 Self::PartHeaderCount => 9,
436 Self::FilenameLength => 10,
437 Self::Preamble => 11,
438 Self::Epilogue => 12,
439 Self::Padding => 13,
440 Self::TokenLength => 14,
441 Self::Disk => 15,
442 }
443 }
444
445 const fn code(self) -> &'static str {
446 match self {
447 Self::Body => "limit.body",
448 Self::Fields => "limit.fields",
449 Self::NameLength => "limit.name_length",
450 Self::ValueLength => "limit.value_length",
451 Self::TextTotal => "limit.text_total",
452 Self::Parts => "limit.parts",
453 Self::Files => "limit.files",
454 Self::FileSize => "limit.file_size",
455 Self::PartHeaderBytes => "limit.part_header_bytes",
456 Self::PartHeaderCount => "limit.part_header_count",
457 Self::FilenameLength => "limit.filename_length",
458 Self::Preamble => "limit.preamble",
459 Self::Epilogue => "limit.epilogue",
460 Self::Padding => "limit.padding",
461 Self::TokenLength => "limit.token_length",
462 Self::Disk => "limit.disk",
463 }
464 }
465
466 const fn field_name(self) -> &'static str {
467 match self {
468 Self::Body => "max_body_bytes",
469 Self::Fields => "max_fields",
470 Self::NameLength => "max_name_bytes",
471 Self::ValueLength => "max_value_bytes",
472 Self::TextTotal => "max_text_bytes",
473 Self::Parts => "max_parts",
474 Self::Files => "max_files",
475 Self::FileSize => "max_file_bytes",
476 Self::PartHeaderBytes => "max_part_header_bytes",
477 Self::PartHeaderCount => "max_part_headers",
478 Self::FilenameLength => "max_filename_bytes",
479 Self::Preamble => "max_preamble_bytes",
480 Self::Epilogue => "max_epilogue_bytes",
481 Self::Padding => "max_padding_bytes",
482 Self::TokenLength => "max_token_bytes",
483 Self::Disk => "max_disk_bytes",
484 }
485 }
486
487 const fn describe(self) -> &'static str {
488 match self {
489 Self::Body => "body too large",
490 Self::Fields => "too many fields",
491 Self::NameLength => "field name too long",
492 Self::ValueLength => "field value too long",
493 Self::TextTotal => "text fields too large in total",
494 Self::Parts => "too many parts",
495 Self::Files => "too many files",
496 Self::FileSize => "file too large",
497 Self::PartHeaderBytes => "part headers too large",
498 Self::PartHeaderCount => "too many part header lines",
499 Self::FilenameLength => "file name too long",
500 Self::Preamble => "too many bytes before the first boundary",
501 Self::Epilogue => "too many bytes after the last boundary",
502 Self::Padding => "too much padding after a boundary",
503 Self::TokenLength => "anti-forgery token too long",
504 Self::Disk => "too many bytes stored on disk for one request",
505 }
506 }
507
508 const fn action(self) -> &'static str {
509 match self {
510 Self::Body => "raise max_body_bytes if this route accepts larger bodies",
511 Self::Fields => "raise max_fields if this route accepts more fields",
512 Self::NameLength => "raise max_name_bytes if this route uses longer names",
513 Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
514 Self::TextTotal => "raise max_text_bytes if this route accepts more text",
515 Self::Parts => "raise max_parts if this route accepts more parts",
516 Self::Files => "raise max_files if this route accepts more files",
517 Self::FileSize => "raise max_file_bytes if this route accepts larger files",
518 Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
519 Self::PartHeaderCount => "raise max_part_headers if real clients need it",
520 Self::FilenameLength => "raise max_filename_bytes if real clients need it",
521 Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
522 Self::Padding => "the body is not what browsers send",
523 Self::TokenLength => "treat the request as forged",
524 Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
525 }
526 }
527}
528
529#[non_exhaustive]
531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
532pub enum SyntaxError {
533 InvalidContentType,
535 MissingBoundary,
537 DuplicateBoundary,
539 InvalidBoundary,
542 NoInitialDelimiter,
544 BoundaryInContent,
548 BareLineFeed,
550 MalformedHeaderLine,
552 HeaderFolding,
554 InvalidHeaderName,
557 ControlInHeader,
559 MissingContentDisposition,
561 DuplicateHeader,
563 NotFormData,
565 MissingName,
567 EmptyName,
569 DuplicateParameter,
571 MalformedParameter,
574 AmbiguousQuote,
576 Truncated,
578}
579
580impl SyntaxError {
581 #[cfg(target_has_atomic = "32")]
582 const fn index(self) -> usize {
583 match self {
584 Self::InvalidContentType => 0,
585 Self::MissingBoundary => 1,
586 Self::DuplicateBoundary => 2,
587 Self::InvalidBoundary => 3,
588 Self::NoInitialDelimiter => 4,
589 Self::BoundaryInContent => 5,
590 Self::BareLineFeed => 6,
591 Self::MalformedHeaderLine => 7,
592 Self::HeaderFolding => 8,
593 Self::InvalidHeaderName => 9,
594 Self::ControlInHeader => 10,
595 Self::MissingContentDisposition => 11,
596 Self::DuplicateHeader => 12,
597 Self::NotFormData => 13,
598 Self::MissingName => 14,
599 Self::EmptyName => 15,
600 Self::DuplicateParameter => 16,
601 Self::MalformedParameter => 17,
602 Self::AmbiguousQuote => 18,
603 Self::Truncated => 19,
604 }
605 }
606
607 const fn code(self) -> &'static str {
608 match self {
609 Self::InvalidContentType => "syntax.invalid_content_type",
610 Self::MissingBoundary => "syntax.missing_boundary",
611 Self::DuplicateBoundary => "syntax.duplicate_boundary",
612 Self::InvalidBoundary => "syntax.invalid_boundary",
613 Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
614 Self::BoundaryInContent => "syntax.boundary_in_content",
615 Self::BareLineFeed => "syntax.bare_line_feed",
616 Self::MalformedHeaderLine => "syntax.malformed_header_line",
617 Self::HeaderFolding => "syntax.header_folding",
618 Self::InvalidHeaderName => "syntax.invalid_header_name",
619 Self::ControlInHeader => "syntax.control_in_header",
620 Self::MissingContentDisposition => "syntax.missing_content_disposition",
621 Self::DuplicateHeader => "syntax.duplicate_header",
622 Self::NotFormData => "syntax.not_form_data",
623 Self::MissingName => "syntax.missing_name",
624 Self::EmptyName => "syntax.empty_name",
625 Self::DuplicateParameter => "syntax.duplicate_parameter",
626 Self::MalformedParameter => "syntax.malformed_parameter",
627 Self::AmbiguousQuote => "syntax.ambiguous_quote",
628 Self::Truncated => "syntax.truncated",
629 }
630 }
631
632 const fn describe(self) -> &'static str {
633 match self {
634 Self::InvalidContentType => "malformed Content-Type",
635 Self::MissingBoundary => "multipart/form-data without a boundary",
636 Self::DuplicateBoundary => "two boundary parameters",
637 Self::InvalidBoundary => "invalid multipart boundary",
638 Self::NoInitialDelimiter => {
639 "no multipart delimiter found: the body is not multipart or the boundary is wrong"
640 }
641 Self::BoundaryInContent => "the boundary appears inside a part",
642 Self::BareLineFeed => "LF without CR where CRLF is required",
643 Self::MalformedHeaderLine => "malformed part header line",
644 Self::HeaderFolding => "folded part header line",
645 Self::InvalidHeaderName => "invalid part header name",
646 Self::ControlInHeader => "control byte in a part header",
647 Self::MissingContentDisposition => "part without Content-Disposition",
648 Self::DuplicateHeader => "part header given twice",
649 Self::NotFormData => "Content-Disposition type is not form-data",
650 Self::MissingName => "part without a name",
651 Self::EmptyName => "part with an empty name",
652 Self::DuplicateParameter => "parameter given twice",
653 Self::MalformedParameter => "malformed header parameter",
654 Self::AmbiguousQuote => "quoted value contains \\\"",
655 Self::Truncated => "body ended before the closing boundary",
656 }
657 }
658}
659
660#[non_exhaustive]
662#[derive(Clone, Copy, Debug, PartialEq, Eq)]
663pub enum EncodingError {
664 InvalidPercent,
666 InvalidUtf8,
668 UnsupportedCharset,
670 InvalidExtendedValue,
672}
673
674impl EncodingError {
675 #[cfg(target_has_atomic = "32")]
676 const fn index(self) -> usize {
677 match self {
678 Self::InvalidPercent => 0,
679 Self::InvalidUtf8 => 1,
680 Self::UnsupportedCharset => 2,
681 Self::InvalidExtendedValue => 3,
682 }
683 }
684
685 const fn code(self) -> &'static str {
686 match self {
687 Self::InvalidPercent => "encoding.invalid_percent",
688 Self::InvalidUtf8 => "encoding.invalid_utf8",
689 Self::UnsupportedCharset => "encoding.unsupported_charset",
690 Self::InvalidExtendedValue => "encoding.invalid_extended_value",
691 }
692 }
693
694 const fn describe(self) -> &'static str {
695 match self {
696 Self::InvalidPercent => "invalid percent escape",
697 Self::InvalidUtf8 => "text is not valid UTF-8",
698 Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
699 Self::InvalidExtendedValue => "malformed filename*",
700 }
701 }
702}
703
704#[non_exhaustive]
706#[derive(Clone, Copy, Debug, PartialEq, Eq)]
707pub enum PolicyRefusal {
708 TransferEncoding,
710 NestedMultipart,
712 UnknownHeader,
714 BothFilenames,
716 ExtendedFilename,
718 TokenFieldLate,
721 DuplicateToken,
723}
724
725impl PolicyRefusal {
726 #[cfg(target_has_atomic = "32")]
727 const fn index(self) -> usize {
728 match self {
729 Self::TransferEncoding => 0,
730 Self::NestedMultipart => 1,
731 Self::UnknownHeader => 2,
732 Self::BothFilenames => 3,
733 Self::ExtendedFilename => 4,
734 Self::TokenFieldLate => 5,
735 Self::DuplicateToken => 6,
736 }
737 }
738
739 const fn code(self) -> &'static str {
740 match self {
741 Self::TransferEncoding => "policy.transfer_encoding",
742 Self::NestedMultipart => "policy.nested_multipart",
743 Self::UnknownHeader => "policy.unknown_header",
744 Self::BothFilenames => "policy.both_filenames",
745 Self::ExtendedFilename => "policy.extended_filename",
746 Self::TokenFieldLate => "policy.token_field_late",
747 Self::DuplicateToken => "policy.duplicate_token",
748 }
749 }
750
751 const fn describe(self) -> &'static str {
752 match self {
753 Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
754 Self::NestedMultipart => "nested multipart part",
755 Self::UnknownHeader => "unknown part header",
756 Self::BothFilenames => "both filename and filename*",
757 Self::ExtendedFilename => "filename* is not accepted",
758 Self::TokenFieldLate => {
759 "file part before the anti-forgery token; treat as a possible forgery"
760 }
761 Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
762 }
763 }
764}
765
766#[non_exhaustive]
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
769pub enum ExtractError {
770 Unknown,
772 Missing,
774 Duplicate,
776 Invalid(&'static str),
778 WrongKind,
780}
781
782impl ExtractError {
783 #[cfg(target_has_atomic = "32")]
784 const fn index(self) -> usize {
785 match self {
786 Self::Unknown => 0,
787 Self::Missing => 1,
788 Self::Duplicate => 2,
789 Self::Invalid(_) => 3,
790 Self::WrongKind => 4,
791 }
792 }
793
794 const fn code(self) -> &'static str {
795 match self {
796 Self::Unknown => "extract.unknown",
797 Self::Missing => "extract.missing",
798 Self::Duplicate => "extract.duplicate",
799 Self::Invalid(_) => "extract.invalid",
800 Self::WrongKind => "extract.wrong_kind",
801 }
802 }
803
804 const fn describe(self) -> &'static str {
805 match self {
806 Self::Unknown => "unknown field",
807 Self::Missing => "missing field",
808 Self::Duplicate => "field given more than once",
809 Self::Invalid(why) => why,
810 Self::WrongKind => "file where text was expected, or text where a file was expected",
811 }
812 }
813}
814
815#[cfg(target_has_atomic = "32")]
817pub(crate) const SLOTS: usize = 61;
818
819#[cfg(target_has_atomic = "32")]
821pub(crate) const SLOT_CODES: [&str; SLOTS] = [
822 "limit.body",
823 "limit.fields",
824 "limit.name_length",
825 "limit.value_length",
826 "limit.text_total",
827 "limit.parts",
828 "limit.files",
829 "limit.file_size",
830 "limit.part_header_bytes",
831 "limit.part_header_count",
832 "limit.filename_length",
833 "limit.preamble",
834 "limit.epilogue",
835 "limit.padding",
836 "limit.token_length",
837 "limit.disk",
838 "syntax.invalid_content_type",
839 "syntax.missing_boundary",
840 "syntax.duplicate_boundary",
841 "syntax.invalid_boundary",
842 "syntax.no_initial_delimiter",
843 "syntax.boundary_in_content",
844 "syntax.bare_line_feed",
845 "syntax.malformed_header_line",
846 "syntax.header_folding",
847 "syntax.invalid_header_name",
848 "syntax.control_in_header",
849 "syntax.missing_content_disposition",
850 "syntax.duplicate_header",
851 "syntax.not_form_data",
852 "syntax.missing_name",
853 "syntax.empty_name",
854 "syntax.duplicate_parameter",
855 "syntax.malformed_parameter",
856 "syntax.ambiguous_quote",
857 "syntax.truncated",
858 "encoding.invalid_percent",
859 "encoding.invalid_utf8",
860 "encoding.unsupported_charset",
861 "encoding.invalid_extended_value",
862 "policy.transfer_encoding",
863 "policy.nested_multipart",
864 "policy.unknown_header",
865 "policy.both_filenames",
866 "policy.extended_filename",
867 "policy.token_field_late",
868 "policy.duplicate_token",
869 "extract.unknown",
870 "extract.missing",
871 "extract.duplicate",
872 "extract.invalid",
873 "extract.wrong_kind",
874 "store.quota_exceeded",
875 "store.not_a_directory",
876 "store.destination_exists",
877 "unsupported_media_type",
878 "invalid_limits",
879 "buffer_too_small",
880 "io",
881 "random",
882 "poisoned",
883];
884
885#[cfg(test)]
886pub(crate) mod tests {
887 use super::*;
888 use alloc::string::ToString;
889
890 pub(crate) fn every_error() -> alloc::vec::Vec<FormError> {
894 use alloc::vec::Vec;
895 let at = Location::NONE;
896 let mut all = Vec::new();
897 for limit in [
898 Limit::Body,
899 Limit::Fields,
900 Limit::NameLength,
901 Limit::ValueLength,
902 Limit::TextTotal,
903 Limit::Parts,
904 Limit::Files,
905 Limit::FileSize,
906 Limit::PartHeaderBytes,
907 Limit::PartHeaderCount,
908 Limit::FilenameLength,
909 Limit::Preamble,
910 Limit::Epilogue,
911 Limit::Padding,
912 Limit::TokenLength,
913 Limit::Disk,
914 ] {
915 all.push(FormError::limit(limit, at));
916 }
917 for reason in [
918 SyntaxError::InvalidContentType,
919 SyntaxError::MissingBoundary,
920 SyntaxError::DuplicateBoundary,
921 SyntaxError::InvalidBoundary,
922 SyntaxError::NoInitialDelimiter,
923 SyntaxError::BoundaryInContent,
924 SyntaxError::BareLineFeed,
925 SyntaxError::MalformedHeaderLine,
926 SyntaxError::HeaderFolding,
927 SyntaxError::InvalidHeaderName,
928 SyntaxError::ControlInHeader,
929 SyntaxError::MissingContentDisposition,
930 SyntaxError::DuplicateHeader,
931 SyntaxError::NotFormData,
932 SyntaxError::MissingName,
933 SyntaxError::EmptyName,
934 SyntaxError::DuplicateParameter,
935 SyntaxError::MalformedParameter,
936 SyntaxError::AmbiguousQuote,
937 SyntaxError::Truncated,
938 ] {
939 all.push(FormError::syntax(reason, at));
940 }
941 for reason in [
942 EncodingError::InvalidPercent,
943 EncodingError::InvalidUtf8,
944 EncodingError::UnsupportedCharset,
945 EncodingError::InvalidExtendedValue,
946 ] {
947 all.push(FormError::encoding(reason, at));
948 }
949 for reason in [
950 PolicyRefusal::TransferEncoding,
951 PolicyRefusal::NestedMultipart,
952 PolicyRefusal::UnknownHeader,
953 PolicyRefusal::BothFilenames,
954 PolicyRefusal::ExtendedFilename,
955 PolicyRefusal::TokenFieldLate,
956 PolicyRefusal::DuplicateToken,
957 ] {
958 all.push(FormError::policy(reason, at));
959 }
960 for reason in [
961 ExtractError::Unknown,
962 ExtractError::Missing,
963 ExtractError::Duplicate,
964 ExtractError::Invalid("not a number"),
965 ExtractError::WrongKind,
966 ] {
967 all.push(FormError::Extract {
968 reason,
969 field: Some("f"),
970 });
971 }
972 #[cfg(feature = "fs")]
973 for reason in [
974 crate::store::StoreError::QuotaExceeded,
975 crate::store::StoreError::NotADirectory,
976 crate::store::StoreError::DestinationExists,
977 ] {
978 all.push(FormError::Store { reason });
979 }
980 all.push(FormError::UnsupportedMediaType);
981 all.push(FormError::InvalidLimits { limit: Limit::Body });
982 all.push(FormError::BufferTooSmall { needed: 1 });
983 #[cfg(feature = "std")]
984 all.push(FormError::Io {
985 kind: std::io::ErrorKind::Other,
986 });
987 #[cfg(feature = "std")]
988 all.push(FormError::Random);
989 all.push(FormError::Poisoned);
990 all
991 }
992
993 #[cfg(target_has_atomic = "32")]
994 #[test]
995 fn test_every_error_has_its_own_slot_holding_its_code() {
996 let mut seen = [false; SLOTS];
997 for e in every_error() {
998 let slot = e.slot().unwrap();
999 assert_eq!(SLOT_CODES[slot], e.code(), "{e:?}");
1000 assert!(!seen[slot], "slot {slot} twice");
1001 seen[slot] = true;
1002 }
1003 let unused: alloc::vec::Vec<&str> = (0..SLOTS)
1006 .filter(|&i| !seen[i])
1007 .map(|i| SLOT_CODES[i])
1008 .collect();
1009 let mut expected = alloc::vec::Vec::new();
1010 if !cfg!(feature = "fs") {
1011 expected.extend([
1012 "store.quota_exceeded",
1013 "store.not_a_directory",
1014 "store.destination_exists",
1015 ]);
1016 }
1017 if !cfg!(feature = "std") {
1018 expected.extend(["io", "random"]);
1019 }
1020 assert_eq!(unused, expected);
1021 for (i, a) in SLOT_CODES.iter().enumerate() {
1022 assert!(!SLOT_CODES[..i].contains(a), "{a} twice");
1023 }
1024 let refused = FormError::Refused {
1025 refusal: Refusal::new(422, "x"),
1026 at: Location::NONE,
1027 };
1028 assert_eq!(refused.slot(), None);
1029 }
1030
1031 #[test]
1032 fn test_status_hint_every_family_maps() {
1033 let at = Location::NONE;
1034 assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
1035 assert_eq!(
1036 FormError::syntax(SyntaxError::Truncated, at).status_hint(),
1037 400
1038 );
1039 assert_eq!(
1040 FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
1041 415
1042 );
1043 assert_eq!(
1044 FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
1045 400
1046 );
1047 assert_eq!(
1048 FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
1049 415
1050 );
1051 assert_eq!(
1052 FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
1053 400
1054 );
1055 let extract = FormError::Extract {
1056 reason: ExtractError::Missing,
1057 field: Some("email"),
1058 };
1059 assert_eq!(extract.status_hint(), 422);
1060 assert_eq!(
1061 FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
1062 500
1063 );
1064 assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
1065 }
1066
1067 #[test]
1068 fn test_status_hint_refusal_out_of_range_reports_400() {
1069 let at = Location::NONE;
1070 let ok = FormError::Refused {
1071 refusal: Refusal::new(422, "x"),
1072 at,
1073 };
1074 assert_eq!(ok.status_hint(), 422);
1075 for status in [0, 200, 399, 600, 999] {
1076 let e = FormError::Refused {
1077 refusal: Refusal::new(status, "x"),
1078 at,
1079 };
1080 assert_eq!(e.status_hint(), 400);
1081 }
1082 }
1083
1084 #[test]
1085 fn test_code_refusal_returns_inspector_code() {
1086 let e = FormError::Refused {
1087 refusal: Refusal::new(422, "customs.unexpected_field"),
1088 at: Location::NONE,
1089 };
1090 assert_eq!(e.code(), "customs.unexpected_field");
1091 }
1092
1093 #[test]
1094 fn test_display_never_contains_the_field_value() {
1095 let e = FormError::Extract {
1096 reason: ExtractError::Invalid("not a number"),
1097 field: Some("age"),
1098 };
1099 assert_eq!(e.to_string(), "not a number (field `age`)");
1100 let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
1101 assert_eq!(
1102 e.to_string(),
1103 "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
1104 );
1105 }
1106
1107 #[test]
1108 fn test_shifted_moves_input_errors_only() {
1109 let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
1110 assert_eq!(e.location().map(|l| l.offset), Some(12));
1111 assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
1112 }
1113
1114 #[test]
1115 fn test_form_error_is_small_and_copy() {
1116 fn is_copy<T: Copy>() {}
1117 is_copy::<FormError>();
1118 assert!(core::mem::size_of::<FormError>() <= 48);
1119 }
1120}