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 Poisoned,
99}
100
101impl FormError {
102 pub(crate) const fn limit(limit: Limit, at: Location) -> Self {
103 Self::Limit { limit, at }
104 }
105
106 pub(crate) const fn syntax(reason: SyntaxError, at: Location) -> Self {
107 Self::Syntax { reason, at }
108 }
109
110 pub(crate) const fn encoding(reason: EncodingError, at: Location) -> Self {
111 Self::Encoding { reason, at }
112 }
113
114 pub(crate) const fn policy(reason: PolicyRefusal, at: Location) -> Self {
115 Self::Policy { reason, at }
116 }
117
118 #[must_use]
121 pub(crate) const fn shifted(self, by: u64) -> Self {
122 match self {
123 Self::Limit { limit, at } => Self::Limit {
124 limit,
125 at: at.shifted(by),
126 },
127 Self::Syntax { reason, at } => Self::Syntax {
128 reason,
129 at: at.shifted(by),
130 },
131 Self::Encoding { reason, at } => Self::Encoding {
132 reason,
133 at: at.shifted(by),
134 },
135 Self::Policy { reason, at } => Self::Policy {
136 reason,
137 at: at.shifted(by),
138 },
139 Self::Refused { refusal, at } => Self::Refused {
140 refusal,
141 at: at.shifted(by),
142 },
143 other => other,
144 }
145 }
146
147 #[must_use]
165 pub const fn status_hint(&self) -> u16 {
166 match self {
167 Self::Limit { .. } => 413,
168 Self::Syntax { .. } => 400,
169 Self::Encoding { reason, .. } => match reason {
170 EncodingError::UnsupportedCharset => 415,
171 _ => 400,
172 },
173 Self::Policy { reason, .. } => match reason {
174 PolicyRefusal::TransferEncoding | PolicyRefusal::NestedMultipart => 415,
175 _ => 400,
176 },
177 Self::Refused { refusal, .. } => {
178 if refusal.status >= 400 && refusal.status <= 599 {
179 refusal.status
180 } else {
181 400
182 }
183 }
184 Self::Extract { .. } => 422,
185 Self::UnsupportedMediaType => 415,
186 Self::InvalidLimits { .. } | Self::BufferTooSmall { .. } | Self::Poisoned => 500,
187 #[cfg(feature = "std")]
188 Self::Io { .. } | Self::Random => 500,
189 }
190 }
191
192 #[must_use]
207 pub const fn code(&self) -> &'static str {
208 match self {
209 Self::Limit { limit, .. } => limit.code(),
210 Self::Syntax { reason, .. } => reason.code(),
211 Self::Encoding { reason, .. } => reason.code(),
212 Self::Policy { reason, .. } => reason.code(),
213 Self::Refused { refusal, .. } => refusal.code,
214 Self::Extract { reason, .. } => reason.code(),
215 Self::UnsupportedMediaType => "unsupported_media_type",
216 Self::InvalidLimits { .. } => "invalid_limits",
217 Self::BufferTooSmall { .. } => "buffer_too_small",
218 #[cfg(feature = "std")]
219 Self::Io { .. } => "io",
220 #[cfg(feature = "std")]
221 Self::Random => "random",
222 Self::Poisoned => "poisoned",
223 }
224 }
225
226 #[must_use]
236 pub const fn location(&self) -> Option<Location> {
237 match self {
238 Self::Limit { at, .. }
239 | Self::Syntax { at, .. }
240 | Self::Encoding { at, .. }
241 | Self::Policy { at, .. }
242 | Self::Refused { at, .. } => Some(*at),
243 _ => None,
244 }
245 }
246}
247
248impl fmt::Display for FormError {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 match self {
251 Self::Limit { limit, at } => {
252 write!(f, "{} ({at}): {}", limit.describe(), limit.action())
253 }
254 Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
255 Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
256 Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
257 Self::Refused { refusal, at } => {
258 write!(f, "refused by the inspector: {} ({at})", refusal.code)
259 }
260 Self::Extract { reason, field } => match field {
261 Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
262 None => f.write_str(reason.describe()),
263 },
264 Self::UnsupportedMediaType => f.write_str(
265 "unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
266 ),
267 Self::InvalidLimits { limit } => write!(
268 f,
269 "invalid limits: {} cannot be satisfied by any body; fix the configuration",
270 limit.field_name()
271 ),
272 Self::BufferTooSmall { needed } => write!(
273 f,
274 "buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
275 ),
276 #[cfg(feature = "std")]
277 Self::Io { kind } => write!(f, "I/O failure: {kind}"),
278 #[cfg(feature = "std")]
279 Self::Random => f.write_str(
280 "the operating system's random source failed; no predictable fallback is used",
281 ),
282 Self::Poisoned => {
283 f.write_str("parser used after an error; call reset before parsing again")
284 }
285 }
286 }
287}
288
289impl core::error::Error for FormError {}
290
291#[non_exhaustive]
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub struct Location {
303 pub part: Option<u32>,
305 pub offset: u64,
307}
308
309impl Location {
310 pub const NONE: Location = Location {
312 part: None,
313 offset: 0,
314 };
315
316 pub(crate) const fn at(offset: u64) -> Self {
317 Self { part: None, offset }
318 }
319
320 pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
321 Self { part, offset }
322 }
323
324 const fn shifted(self, by: u64) -> Self {
325 Self {
326 part: self.part,
327 offset: self.offset.saturating_add(by),
328 }
329 }
330}
331
332impl fmt::Display for Location {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 match self.part {
335 Some(part) => write!(f, "part {part}, byte {}", self.offset),
336 None => write!(f, "byte {}", self.offset),
337 }
338 }
339}
340
341#[non_exhaustive]
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344pub enum Limit {
345 Body,
347 Fields,
349 NameLength,
351 ValueLength,
353 TextTotal,
355 Parts,
357 Files,
359 FileSize,
361 PartHeaderBytes,
363 PartHeaderCount,
365 FilenameLength,
367 Preamble,
369 Epilogue,
371 Padding,
373 TokenLength,
375 Disk,
377}
378
379impl Limit {
380 const fn code(self) -> &'static str {
381 match self {
382 Self::Body => "limit.body",
383 Self::Fields => "limit.fields",
384 Self::NameLength => "limit.name_length",
385 Self::ValueLength => "limit.value_length",
386 Self::TextTotal => "limit.text_total",
387 Self::Parts => "limit.parts",
388 Self::Files => "limit.files",
389 Self::FileSize => "limit.file_size",
390 Self::PartHeaderBytes => "limit.part_header_bytes",
391 Self::PartHeaderCount => "limit.part_header_count",
392 Self::FilenameLength => "limit.filename_length",
393 Self::Preamble => "limit.preamble",
394 Self::Epilogue => "limit.epilogue",
395 Self::Padding => "limit.padding",
396 Self::TokenLength => "limit.token_length",
397 Self::Disk => "limit.disk",
398 }
399 }
400
401 const fn field_name(self) -> &'static str {
402 match self {
403 Self::Body => "max_body_bytes",
404 Self::Fields => "max_fields",
405 Self::NameLength => "max_name_bytes",
406 Self::ValueLength => "max_value_bytes",
407 Self::TextTotal => "max_text_bytes",
408 Self::Parts => "max_parts",
409 Self::Files => "max_files",
410 Self::FileSize => "max_file_bytes",
411 Self::PartHeaderBytes => "max_part_header_bytes",
412 Self::PartHeaderCount => "max_part_headers",
413 Self::FilenameLength => "max_filename_bytes",
414 Self::Preamble => "max_preamble_bytes",
415 Self::Epilogue => "max_epilogue_bytes",
416 Self::Padding => "max_padding_bytes",
417 Self::TokenLength => "max_token_bytes",
418 Self::Disk => "max_disk_bytes",
419 }
420 }
421
422 const fn describe(self) -> &'static str {
423 match self {
424 Self::Body => "body too large",
425 Self::Fields => "too many fields",
426 Self::NameLength => "field name too long",
427 Self::ValueLength => "field value too long",
428 Self::TextTotal => "text fields too large in total",
429 Self::Parts => "too many parts",
430 Self::Files => "too many files",
431 Self::FileSize => "file too large",
432 Self::PartHeaderBytes => "part headers too large",
433 Self::PartHeaderCount => "too many part header lines",
434 Self::FilenameLength => "file name too long",
435 Self::Preamble => "too many bytes before the first boundary",
436 Self::Epilogue => "too many bytes after the last boundary",
437 Self::Padding => "too much padding after a boundary",
438 Self::TokenLength => "anti-forgery token too long",
439 Self::Disk => "too many bytes stored on disk for one request",
440 }
441 }
442
443 const fn action(self) -> &'static str {
444 match self {
445 Self::Body => "raise max_body_bytes if this route accepts larger bodies",
446 Self::Fields => "raise max_fields if this route accepts more fields",
447 Self::NameLength => "raise max_name_bytes if this route uses longer names",
448 Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
449 Self::TextTotal => "raise max_text_bytes if this route accepts more text",
450 Self::Parts => "raise max_parts if this route accepts more parts",
451 Self::Files => "raise max_files if this route accepts more files",
452 Self::FileSize => "raise max_file_bytes if this route accepts larger files",
453 Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
454 Self::PartHeaderCount => "raise max_part_headers if real clients need it",
455 Self::FilenameLength => "raise max_filename_bytes if real clients need it",
456 Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
457 Self::Padding => "the body is not what browsers send",
458 Self::TokenLength => "treat the request as forged",
459 Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
460 }
461 }
462}
463
464#[non_exhaustive]
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467pub enum SyntaxError {
468 InvalidContentType,
470 MissingBoundary,
472 DuplicateBoundary,
474 InvalidBoundary,
477 NoInitialDelimiter,
479 BoundaryInContent,
482 BareLineFeed,
484 MalformedHeaderLine,
486 HeaderFolding,
488 InvalidHeaderName,
491 ControlInHeader,
493 MissingContentDisposition,
495 DuplicateHeader,
497 NotFormData,
499 MissingName,
501 EmptyName,
503 DuplicateParameter,
505 MalformedParameter,
507 AmbiguousQuote,
509 Truncated,
511}
512
513impl SyntaxError {
514 const fn code(self) -> &'static str {
515 match self {
516 Self::InvalidContentType => "syntax.invalid_content_type",
517 Self::MissingBoundary => "syntax.missing_boundary",
518 Self::DuplicateBoundary => "syntax.duplicate_boundary",
519 Self::InvalidBoundary => "syntax.invalid_boundary",
520 Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
521 Self::BoundaryInContent => "syntax.boundary_in_content",
522 Self::BareLineFeed => "syntax.bare_line_feed",
523 Self::MalformedHeaderLine => "syntax.malformed_header_line",
524 Self::HeaderFolding => "syntax.header_folding",
525 Self::InvalidHeaderName => "syntax.invalid_header_name",
526 Self::ControlInHeader => "syntax.control_in_header",
527 Self::MissingContentDisposition => "syntax.missing_content_disposition",
528 Self::DuplicateHeader => "syntax.duplicate_header",
529 Self::NotFormData => "syntax.not_form_data",
530 Self::MissingName => "syntax.missing_name",
531 Self::EmptyName => "syntax.empty_name",
532 Self::DuplicateParameter => "syntax.duplicate_parameter",
533 Self::MalformedParameter => "syntax.malformed_parameter",
534 Self::AmbiguousQuote => "syntax.ambiguous_quote",
535 Self::Truncated => "syntax.truncated",
536 }
537 }
538
539 const fn describe(self) -> &'static str {
540 match self {
541 Self::InvalidContentType => "malformed Content-Type",
542 Self::MissingBoundary => "multipart/form-data without a boundary",
543 Self::DuplicateBoundary => "two boundary parameters",
544 Self::InvalidBoundary => "invalid multipart boundary",
545 Self::NoInitialDelimiter => {
546 "no multipart delimiter found: the body is not multipart or the boundary is wrong"
547 }
548 Self::BoundaryInContent => "the boundary appears inside a part",
549 Self::BareLineFeed => "LF without CR where CRLF is required",
550 Self::MalformedHeaderLine => "malformed part header line",
551 Self::HeaderFolding => "folded part header line",
552 Self::InvalidHeaderName => "invalid part header name",
553 Self::ControlInHeader => "control byte in a part header",
554 Self::MissingContentDisposition => "part without Content-Disposition",
555 Self::DuplicateHeader => "part header given twice",
556 Self::NotFormData => "Content-Disposition type is not form-data",
557 Self::MissingName => "part without a name",
558 Self::EmptyName => "part with an empty name",
559 Self::DuplicateParameter => "parameter given twice",
560 Self::MalformedParameter => "malformed header parameter",
561 Self::AmbiguousQuote => "quoted value contains \\\"",
562 Self::Truncated => "body ended before the closing boundary",
563 }
564 }
565}
566
567#[non_exhaustive]
569#[derive(Clone, Copy, Debug, PartialEq, Eq)]
570pub enum EncodingError {
571 InvalidPercent,
573 InvalidUtf8,
575 UnsupportedCharset,
577 InvalidExtendedValue,
579}
580
581impl EncodingError {
582 const fn code(self) -> &'static str {
583 match self {
584 Self::InvalidPercent => "encoding.invalid_percent",
585 Self::InvalidUtf8 => "encoding.invalid_utf8",
586 Self::UnsupportedCharset => "encoding.unsupported_charset",
587 Self::InvalidExtendedValue => "encoding.invalid_extended_value",
588 }
589 }
590
591 const fn describe(self) -> &'static str {
592 match self {
593 Self::InvalidPercent => "invalid percent escape",
594 Self::InvalidUtf8 => "text is not valid UTF-8",
595 Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
596 Self::InvalidExtendedValue => "malformed filename*",
597 }
598 }
599}
600
601#[non_exhaustive]
603#[derive(Clone, Copy, Debug, PartialEq, Eq)]
604pub enum PolicyRefusal {
605 TransferEncoding,
607 NestedMultipart,
609 UnknownHeader,
611 BothFilenames,
613 ExtendedFilename,
615 TokenFieldLate,
618 DuplicateToken,
620}
621
622impl PolicyRefusal {
623 const fn code(self) -> &'static str {
624 match self {
625 Self::TransferEncoding => "policy.transfer_encoding",
626 Self::NestedMultipart => "policy.nested_multipart",
627 Self::UnknownHeader => "policy.unknown_header",
628 Self::BothFilenames => "policy.both_filenames",
629 Self::ExtendedFilename => "policy.extended_filename",
630 Self::TokenFieldLate => "policy.token_field_late",
631 Self::DuplicateToken => "policy.duplicate_token",
632 }
633 }
634
635 const fn describe(self) -> &'static str {
636 match self {
637 Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
638 Self::NestedMultipart => "nested multipart part",
639 Self::UnknownHeader => "unknown part header",
640 Self::BothFilenames => "both filename and filename*",
641 Self::ExtendedFilename => "filename* is not accepted",
642 Self::TokenFieldLate => {
643 "file part before the anti-forgery token; treat as a possible forgery"
644 }
645 Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
646 }
647 }
648}
649
650#[non_exhaustive]
652#[derive(Clone, Copy, Debug, PartialEq, Eq)]
653pub enum ExtractError {
654 Unknown,
656 Missing,
658 Duplicate,
660 Invalid(&'static str),
662 WrongKind,
664}
665
666impl ExtractError {
667 const fn code(self) -> &'static str {
668 match self {
669 Self::Unknown => "extract.unknown",
670 Self::Missing => "extract.missing",
671 Self::Duplicate => "extract.duplicate",
672 Self::Invalid(_) => "extract.invalid",
673 Self::WrongKind => "extract.wrong_kind",
674 }
675 }
676
677 const fn describe(self) -> &'static str {
678 match self {
679 Self::Unknown => "unknown field",
680 Self::Missing => "missing field",
681 Self::Duplicate => "field given more than once",
682 Self::Invalid(why) => why,
683 Self::WrongKind => "file where text was expected, or text where a file was expected",
684 }
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use alloc::string::ToString;
692
693 #[test]
694 fn test_status_hint_every_family_maps() {
695 let at = Location::NONE;
696 assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
697 assert_eq!(
698 FormError::syntax(SyntaxError::Truncated, at).status_hint(),
699 400
700 );
701 assert_eq!(
702 FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
703 415
704 );
705 assert_eq!(
706 FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
707 400
708 );
709 assert_eq!(
710 FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
711 415
712 );
713 assert_eq!(
714 FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
715 400
716 );
717 let extract = FormError::Extract {
718 reason: ExtractError::Missing,
719 field: Some("email"),
720 };
721 assert_eq!(extract.status_hint(), 422);
722 assert_eq!(
723 FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
724 500
725 );
726 assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
727 }
728
729 #[test]
730 fn test_status_hint_refusal_out_of_range_reports_400() {
731 let at = Location::NONE;
732 let ok = FormError::Refused {
733 refusal: Refusal::new(422, "x"),
734 at,
735 };
736 assert_eq!(ok.status_hint(), 422);
737 for status in [0, 200, 399, 600, 999] {
738 let e = FormError::Refused {
739 refusal: Refusal::new(status, "x"),
740 at,
741 };
742 assert_eq!(e.status_hint(), 400);
743 }
744 }
745
746 #[test]
747 fn test_code_refusal_returns_inspector_code() {
748 let e = FormError::Refused {
749 refusal: Refusal::new(422, "customs.unexpected_field"),
750 at: Location::NONE,
751 };
752 assert_eq!(e.code(), "customs.unexpected_field");
753 }
754
755 #[test]
756 fn test_display_never_contains_the_field_value() {
757 let e = FormError::Extract {
758 reason: ExtractError::Invalid("not a number"),
759 field: Some("age"),
760 };
761 assert_eq!(e.to_string(), "not a number (field `age`)");
762 let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
763 assert_eq!(
764 e.to_string(),
765 "too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
766 );
767 }
768
769 #[test]
770 fn test_shifted_moves_input_errors_only() {
771 let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
772 assert_eq!(e.location().map(|l| l.offset), Some(12));
773 assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
774 }
775
776 #[test]
777 fn test_form_error_is_small_and_copy() {
778 fn is_copy<T: Copy>() {}
779 is_copy::<FormError>();
780 assert!(core::mem::size_of::<FormError>() <= 48);
781 }
782}