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,
499 BareLineFeed,
501 MalformedHeaderLine,
503 HeaderFolding,
505 InvalidHeaderName,
508 ControlInHeader,
510 MissingContentDisposition,
512 DuplicateHeader,
514 NotFormData,
516 MissingName,
518 EmptyName,
520 DuplicateParameter,
522 MalformedParameter,
524 AmbiguousQuote,
526 Truncated,
528}
529
530impl SyntaxError {
531 const fn code(self) -> &'static str {
532 match self {
533 Self::InvalidContentType => "syntax.invalid_content_type",
534 Self::MissingBoundary => "syntax.missing_boundary",
535 Self::DuplicateBoundary => "syntax.duplicate_boundary",
536 Self::InvalidBoundary => "syntax.invalid_boundary",
537 Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
538 Self::BoundaryInContent => "syntax.boundary_in_content",
539 Self::BareLineFeed => "syntax.bare_line_feed",
540 Self::MalformedHeaderLine => "syntax.malformed_header_line",
541 Self::HeaderFolding => "syntax.header_folding",
542 Self::InvalidHeaderName => "syntax.invalid_header_name",
543 Self::ControlInHeader => "syntax.control_in_header",
544 Self::MissingContentDisposition => "syntax.missing_content_disposition",
545 Self::DuplicateHeader => "syntax.duplicate_header",
546 Self::NotFormData => "syntax.not_form_data",
547 Self::MissingName => "syntax.missing_name",
548 Self::EmptyName => "syntax.empty_name",
549 Self::DuplicateParameter => "syntax.duplicate_parameter",
550 Self::MalformedParameter => "syntax.malformed_parameter",
551 Self::AmbiguousQuote => "syntax.ambiguous_quote",
552 Self::Truncated => "syntax.truncated",
553 }
554 }
555
556 const fn describe(self) -> &'static str {
557 match self {
558 Self::InvalidContentType => "malformed Content-Type",
559 Self::MissingBoundary => "multipart/form-data without a boundary",
560 Self::DuplicateBoundary => "two boundary parameters",
561 Self::InvalidBoundary => "invalid multipart boundary",
562 Self::NoInitialDelimiter => {
563 "no multipart delimiter found: the body is not multipart or the boundary is wrong"
564 }
565 Self::BoundaryInContent => "the boundary appears inside a part",
566 Self::BareLineFeed => "LF without CR where CRLF is required",
567 Self::MalformedHeaderLine => "malformed part header line",
568 Self::HeaderFolding => "folded part header line",
569 Self::InvalidHeaderName => "invalid part header name",
570 Self::ControlInHeader => "control byte in a part header",
571 Self::MissingContentDisposition => "part without Content-Disposition",
572 Self::DuplicateHeader => "part header given twice",
573 Self::NotFormData => "Content-Disposition type is not form-data",
574 Self::MissingName => "part without a name",
575 Self::EmptyName => "part with an empty name",
576 Self::DuplicateParameter => "parameter given twice",
577 Self::MalformedParameter => "malformed header parameter",
578 Self::AmbiguousQuote => "quoted value contains \\\"",
579 Self::Truncated => "body ended before the closing boundary",
580 }
581 }
582}
583
584#[non_exhaustive]
586#[derive(Clone, Copy, Debug, PartialEq, Eq)]
587pub enum EncodingError {
588 InvalidPercent,
590 InvalidUtf8,
592 UnsupportedCharset,
594 InvalidExtendedValue,
596}
597
598impl EncodingError {
599 const fn code(self) -> &'static str {
600 match self {
601 Self::InvalidPercent => "encoding.invalid_percent",
602 Self::InvalidUtf8 => "encoding.invalid_utf8",
603 Self::UnsupportedCharset => "encoding.unsupported_charset",
604 Self::InvalidExtendedValue => "encoding.invalid_extended_value",
605 }
606 }
607
608 const fn describe(self) -> &'static str {
609 match self {
610 Self::InvalidPercent => "invalid percent escape",
611 Self::InvalidUtf8 => "text is not valid UTF-8",
612 Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
613 Self::InvalidExtendedValue => "malformed filename*",
614 }
615 }
616}
617
618#[non_exhaustive]
620#[derive(Clone, Copy, Debug, PartialEq, Eq)]
621pub enum PolicyRefusal {
622 TransferEncoding,
624 NestedMultipart,
626 UnknownHeader,
628 BothFilenames,
630 ExtendedFilename,
632 TokenFieldLate,
635 DuplicateToken,
637}
638
639impl PolicyRefusal {
640 const fn code(self) -> &'static str {
641 match self {
642 Self::TransferEncoding => "policy.transfer_encoding",
643 Self::NestedMultipart => "policy.nested_multipart",
644 Self::UnknownHeader => "policy.unknown_header",
645 Self::BothFilenames => "policy.both_filenames",
646 Self::ExtendedFilename => "policy.extended_filename",
647 Self::TokenFieldLate => "policy.token_field_late",
648 Self::DuplicateToken => "policy.duplicate_token",
649 }
650 }
651
652 const fn describe(self) -> &'static str {
653 match self {
654 Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
655 Self::NestedMultipart => "nested multipart part",
656 Self::UnknownHeader => "unknown part header",
657 Self::BothFilenames => "both filename and filename*",
658 Self::ExtendedFilename => "filename* is not accepted",
659 Self::TokenFieldLate => {
660 "file part before the anti-forgery token; treat as a possible forgery"
661 }
662 Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
663 }
664 }
665}
666
667#[non_exhaustive]
669#[derive(Clone, Copy, Debug, PartialEq, Eq)]
670pub enum ExtractError {
671 Unknown,
673 Missing,
675 Duplicate,
677 Invalid(&'static str),
679 WrongKind,
681}
682
683impl ExtractError {
684 const fn code(self) -> &'static str {
685 match self {
686 Self::Unknown => "extract.unknown",
687 Self::Missing => "extract.missing",
688 Self::Duplicate => "extract.duplicate",
689 Self::Invalid(_) => "extract.invalid",
690 Self::WrongKind => "extract.wrong_kind",
691 }
692 }
693
694 const fn describe(self) -> &'static str {
695 match self {
696 Self::Unknown => "unknown field",
697 Self::Missing => "missing field",
698 Self::Duplicate => "field given more than once",
699 Self::Invalid(why) => why,
700 Self::WrongKind => "file where text was expected, or text where a file was expected",
701 }
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use alloc::string::ToString;
709
710 #[test]
711 fn test_status_hint_every_family_maps() {
712 let at = Location::NONE;
713 assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
714 assert_eq!(
715 FormError::syntax(SyntaxError::Truncated, at).status_hint(),
716 400
717 );
718 assert_eq!(
719 FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
720 415
721 );
722 assert_eq!(
723 FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
724 400
725 );
726 assert_eq!(
727 FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
728 415
729 );
730 assert_eq!(
731 FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
732 400
733 );
734 let extract = FormError::Extract {
735 reason: ExtractError::Missing,
736 field: Some("email"),
737 };
738 assert_eq!(extract.status_hint(), 422);
739 assert_eq!(
740 FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
741 500
742 );
743 assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
744 }
745
746 #[test]
747 fn test_status_hint_refusal_out_of_range_reports_400() {
748 let at = Location::NONE;
749 let ok = FormError::Refused {
750 refusal: Refusal::new(422, "x"),
751 at,
752 };
753 assert_eq!(ok.status_hint(), 422);
754 for status in [0, 200, 399, 600, 999] {
755 let e = FormError::Refused {
756 refusal: Refusal::new(status, "x"),
757 at,
758 };
759 assert_eq!(e.status_hint(), 400);
760 }
761 }
762
763 #[test]
764 fn test_code_refusal_returns_inspector_code() {
765 let e = FormError::Refused {
766 refusal: Refusal::new(422, "customs.unexpected_field"),
767 at: Location::NONE,
768 };
769 assert_eq!(e.code(), "customs.unexpected_field");
770 }
771
772 #[test]
773 fn test_display_never_contains_the_field_value() {
774 let e = FormError::Extract {
775 reason: ExtractError::Invalid("not a number"),
776 field: Some("age"),
777 };
778 assert_eq!(e.to_string(), "not a number (field `age`)");
779 let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
780 assert_eq!(
781 e.to_string(),
782 "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
783 );
784 }
785
786 #[test]
787 fn test_shifted_moves_input_errors_only() {
788 let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
789 assert_eq!(e.location().map(|l| l.offset), Some(12));
790 assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
791 }
792
793 #[test]
794 fn test_form_error_is_small_and_copy() {
795 fn is_copy<T: Copy>() {}
796 is_copy::<FormError>();
797 assert!(core::mem::size_of::<FormError>() <= 48);
798 }
799}