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