use core::fmt;
use crate::hook::Refusal;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormError {
Limit {
limit: Limit,
at: Location,
},
Syntax {
reason: SyntaxError,
at: Location,
},
Encoding {
reason: EncodingError,
at: Location,
},
Policy {
reason: PolicyRefusal,
at: Location,
},
Refused {
refusal: Refusal,
at: Location,
},
Extract {
reason: ExtractError,
field: Option<&'static str>,
},
UnsupportedMediaType,
InvalidLimits {
limit: Limit,
},
BufferTooSmall {
needed: usize,
},
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
Io {
kind: std::io::ErrorKind,
},
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
Random,
#[cfg(feature = "fs")]
#[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
Store {
reason: crate::store::StoreError,
},
Poisoned,
}
impl FormError {
pub(crate) const fn limit(limit: Limit, at: Location) -> Self {
Self::Limit { limit, at }
}
pub(crate) const fn syntax(reason: SyntaxError, at: Location) -> Self {
Self::Syntax { reason, at }
}
pub(crate) const fn encoding(reason: EncodingError, at: Location) -> Self {
Self::Encoding { reason, at }
}
pub(crate) const fn policy(reason: PolicyRefusal, at: Location) -> Self {
Self::Policy { reason, at }
}
#[must_use]
pub(crate) const fn shifted(self, by: u64) -> Self {
match self {
Self::Limit { limit, at } => Self::Limit {
limit,
at: at.shifted(by),
},
Self::Syntax { reason, at } => Self::Syntax {
reason,
at: at.shifted(by),
},
Self::Encoding { reason, at } => Self::Encoding {
reason,
at: at.shifted(by),
},
Self::Policy { reason, at } => Self::Policy {
reason,
at: at.shifted(by),
},
Self::Refused { refusal, at } => Self::Refused {
refusal,
at: at.shifted(by),
},
other => other,
}
}
#[must_use]
pub const fn status_hint(&self) -> u16 {
match self {
Self::Limit { .. } => 413,
Self::Syntax { .. } => 400,
Self::Encoding { reason, .. } => match reason {
EncodingError::UnsupportedCharset => 415,
_ => 400,
},
Self::Policy { reason, .. } => match reason {
PolicyRefusal::TransferEncoding | PolicyRefusal::NestedMultipart => 415,
_ => 400,
},
Self::Refused { refusal, .. } => {
if refusal.status >= 400 && refusal.status <= 599 {
refusal.status
} else {
400
}
}
Self::Extract { .. } => 422,
Self::UnsupportedMediaType => 415,
Self::InvalidLimits { .. } | Self::BufferTooSmall { .. } | Self::Poisoned => 500,
#[cfg(feature = "std")]
Self::Io { .. } | Self::Random => 500,
#[cfg(feature = "fs")]
Self::Store { reason } => match reason {
crate::store::StoreError::QuotaExceeded => 503,
_ => 500,
},
}
}
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::Limit { limit, .. } => limit.code(),
Self::Syntax { reason, .. } => reason.code(),
Self::Encoding { reason, .. } => reason.code(),
Self::Policy { reason, .. } => reason.code(),
Self::Refused { refusal, .. } => refusal.code,
Self::Extract { reason, .. } => reason.code(),
Self::UnsupportedMediaType => "unsupported_media_type",
Self::InvalidLimits { .. } => "invalid_limits",
Self::BufferTooSmall { .. } => "buffer_too_small",
#[cfg(feature = "std")]
Self::Io { .. } => "io",
#[cfg(feature = "std")]
Self::Random => "random",
#[cfg(feature = "fs")]
Self::Store { reason } => reason.code(),
Self::Poisoned => "poisoned",
}
}
#[cfg(target_has_atomic = "32")]
pub(crate) const fn slot(&self) -> Option<usize> {
let slot = match self {
Self::Limit { limit, .. } => limit.index(),
Self::Syntax { reason, .. } => reason.index().wrapping_add(16),
Self::Encoding { reason, .. } => reason.index().wrapping_add(36),
Self::Policy { reason, .. } => reason.index().wrapping_add(40),
Self::Extract { reason, .. } => reason.index().wrapping_add(47),
#[cfg(feature = "fs")]
Self::Store { reason } => reason.index().wrapping_add(52),
Self::UnsupportedMediaType => 55,
Self::InvalidLimits { .. } => 56,
Self::BufferTooSmall { .. } => 57,
#[cfg(feature = "std")]
Self::Io { .. } => 58,
#[cfg(feature = "std")]
Self::Random => 59,
Self::Poisoned => 60,
Self::Refused { .. } => return None,
};
Some(slot)
}
#[must_use]
pub const fn location(&self) -> Option<Location> {
match self {
Self::Limit { at, .. }
| Self::Syntax { at, .. }
| Self::Encoding { at, .. }
| Self::Policy { at, .. }
| Self::Refused { at, .. } => Some(*at),
_ => None,
}
}
}
impl fmt::Display for FormError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Limit { limit, at } => {
write!(f, "{} ({at}): {}", limit.describe(), limit.action())
}
Self::Syntax { reason, at } => write!(f, "{} ({at})", reason.describe()),
Self::Encoding { reason, at } => write!(f, "{} ({at})", reason.describe()),
Self::Policy { reason, at } => write!(f, "{} ({at})", reason.describe()),
Self::Refused { refusal, at } => {
write!(f, "refused by the inspector: {} ({at})", refusal.code)
}
Self::Extract { reason, field } => match field {
Some(name) => write!(f, "{} (field `{name}`)", reason.describe()),
None => f.write_str(reason.describe()),
},
Self::UnsupportedMediaType => f.write_str(
"unsupported media type: only application/x-www-form-urlencoded and multipart/form-data are parsed",
),
Self::InvalidLimits { limit } => write!(
f,
"invalid limits: {} cannot be satisfied by any body; fix the configuration",
limit.field_name()
),
Self::BufferTooSmall { needed } => write!(
f,
"buffer too small: {needed} bytes needed; size it with the Limits buffer functions"
),
#[cfg(feature = "std")]
Self::Io { kind } => write!(f, "I/O failure: {kind}"),
#[cfg(feature = "std")]
Self::Random => f.write_str(
"the operating system's random source failed; no predictable fallback is used",
),
#[cfg(feature = "fs")]
Self::Store { reason } => write!(f, "storage: {}", reason.describe()),
Self::Poisoned => {
f.write_str("parser used after an error; call reset before parsing again")
}
}
}
}
impl core::error::Error for FormError {}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Location {
pub part: Option<u32>,
pub offset: u64,
}
impl Location {
pub const NONE: Location = Location {
part: None,
offset: 0,
};
pub(crate) const fn at(offset: u64) -> Self {
Self { part: None, offset }
}
pub(crate) const fn in_part(part: Option<u32>, offset: u64) -> Self {
Self { part, offset }
}
const fn shifted(self, by: u64) -> Self {
Self {
part: self.part,
offset: self.offset.saturating_add(by),
}
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.part {
Some(part) => write!(f, "part {part}, byte {}", self.offset),
None => write!(f, "byte {}", self.offset),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Limit {
Body,
Fields,
NameLength,
ValueLength,
TextTotal,
Parts,
Files,
FileSize,
PartHeaderBytes,
PartHeaderCount,
FilenameLength,
Preamble,
Epilogue,
Padding,
TokenLength,
Disk,
}
impl Limit {
#[cfg(target_has_atomic = "32")]
const fn index(self) -> usize {
match self {
Self::Body => 0,
Self::Fields => 1,
Self::NameLength => 2,
Self::ValueLength => 3,
Self::TextTotal => 4,
Self::Parts => 5,
Self::Files => 6,
Self::FileSize => 7,
Self::PartHeaderBytes => 8,
Self::PartHeaderCount => 9,
Self::FilenameLength => 10,
Self::Preamble => 11,
Self::Epilogue => 12,
Self::Padding => 13,
Self::TokenLength => 14,
Self::Disk => 15,
}
}
const fn code(self) -> &'static str {
match self {
Self::Body => "limit.body",
Self::Fields => "limit.fields",
Self::NameLength => "limit.name_length",
Self::ValueLength => "limit.value_length",
Self::TextTotal => "limit.text_total",
Self::Parts => "limit.parts",
Self::Files => "limit.files",
Self::FileSize => "limit.file_size",
Self::PartHeaderBytes => "limit.part_header_bytes",
Self::PartHeaderCount => "limit.part_header_count",
Self::FilenameLength => "limit.filename_length",
Self::Preamble => "limit.preamble",
Self::Epilogue => "limit.epilogue",
Self::Padding => "limit.padding",
Self::TokenLength => "limit.token_length",
Self::Disk => "limit.disk",
}
}
const fn field_name(self) -> &'static str {
match self {
Self::Body => "max_body_bytes",
Self::Fields => "max_fields",
Self::NameLength => "max_name_bytes",
Self::ValueLength => "max_value_bytes",
Self::TextTotal => "max_text_bytes",
Self::Parts => "max_parts",
Self::Files => "max_files",
Self::FileSize => "max_file_bytes",
Self::PartHeaderBytes => "max_part_header_bytes",
Self::PartHeaderCount => "max_part_headers",
Self::FilenameLength => "max_filename_bytes",
Self::Preamble => "max_preamble_bytes",
Self::Epilogue => "max_epilogue_bytes",
Self::Padding => "max_padding_bytes",
Self::TokenLength => "max_token_bytes",
Self::Disk => "max_disk_bytes",
}
}
const fn describe(self) -> &'static str {
match self {
Self::Body => "body too large",
Self::Fields => "too many fields",
Self::NameLength => "field name too long",
Self::ValueLength => "field value too long",
Self::TextTotal => "text fields too large in total",
Self::Parts => "too many parts",
Self::Files => "too many files",
Self::FileSize => "file too large",
Self::PartHeaderBytes => "part headers too large",
Self::PartHeaderCount => "too many part header lines",
Self::FilenameLength => "file name too long",
Self::Preamble => "too many bytes before the first boundary",
Self::Epilogue => "too many bytes after the last boundary",
Self::Padding => "too much padding after a boundary",
Self::TokenLength => "anti-forgery token too long",
Self::Disk => "too many bytes stored on disk for one request",
}
}
const fn action(self) -> &'static str {
match self {
Self::Body => "raise max_body_bytes if this route accepts larger bodies",
Self::Fields => "raise max_fields if this route accepts more fields",
Self::NameLength => "raise max_name_bytes if this route uses longer names",
Self::ValueLength => "raise max_value_bytes, or use a file input for large text",
Self::TextTotal => "raise max_text_bytes if this route accepts more text",
Self::Parts => "raise max_parts if this route accepts more parts",
Self::Files => "raise max_files if this route accepts more files",
Self::FileSize => "raise max_file_bytes if this route accepts larger files",
Self::PartHeaderBytes => "raise max_part_header_bytes if real clients need it",
Self::PartHeaderCount => "raise max_part_headers if real clients need it",
Self::FilenameLength => "raise max_filename_bytes if real clients need it",
Self::Preamble | Self::Epilogue => "the boundary is probably wrong",
Self::Padding => "the body is not what browsers send",
Self::TokenLength => "treat the request as forged",
Self::Disk => "raise the store's max_disk_bytes if this route accepts more",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SyntaxError {
InvalidContentType,
MissingBoundary,
DuplicateBoundary,
InvalidBoundary,
NoInitialDelimiter,
BoundaryInContent,
BareLineFeed,
MalformedHeaderLine,
HeaderFolding,
InvalidHeaderName,
ControlInHeader,
MissingContentDisposition,
DuplicateHeader,
NotFormData,
MissingName,
EmptyName,
DuplicateParameter,
MalformedParameter,
AmbiguousQuote,
Truncated,
}
impl SyntaxError {
#[cfg(target_has_atomic = "32")]
const fn index(self) -> usize {
match self {
Self::InvalidContentType => 0,
Self::MissingBoundary => 1,
Self::DuplicateBoundary => 2,
Self::InvalidBoundary => 3,
Self::NoInitialDelimiter => 4,
Self::BoundaryInContent => 5,
Self::BareLineFeed => 6,
Self::MalformedHeaderLine => 7,
Self::HeaderFolding => 8,
Self::InvalidHeaderName => 9,
Self::ControlInHeader => 10,
Self::MissingContentDisposition => 11,
Self::DuplicateHeader => 12,
Self::NotFormData => 13,
Self::MissingName => 14,
Self::EmptyName => 15,
Self::DuplicateParameter => 16,
Self::MalformedParameter => 17,
Self::AmbiguousQuote => 18,
Self::Truncated => 19,
}
}
const fn code(self) -> &'static str {
match self {
Self::InvalidContentType => "syntax.invalid_content_type",
Self::MissingBoundary => "syntax.missing_boundary",
Self::DuplicateBoundary => "syntax.duplicate_boundary",
Self::InvalidBoundary => "syntax.invalid_boundary",
Self::NoInitialDelimiter => "syntax.no_initial_delimiter",
Self::BoundaryInContent => "syntax.boundary_in_content",
Self::BareLineFeed => "syntax.bare_line_feed",
Self::MalformedHeaderLine => "syntax.malformed_header_line",
Self::HeaderFolding => "syntax.header_folding",
Self::InvalidHeaderName => "syntax.invalid_header_name",
Self::ControlInHeader => "syntax.control_in_header",
Self::MissingContentDisposition => "syntax.missing_content_disposition",
Self::DuplicateHeader => "syntax.duplicate_header",
Self::NotFormData => "syntax.not_form_data",
Self::MissingName => "syntax.missing_name",
Self::EmptyName => "syntax.empty_name",
Self::DuplicateParameter => "syntax.duplicate_parameter",
Self::MalformedParameter => "syntax.malformed_parameter",
Self::AmbiguousQuote => "syntax.ambiguous_quote",
Self::Truncated => "syntax.truncated",
}
}
const fn describe(self) -> &'static str {
match self {
Self::InvalidContentType => "malformed Content-Type",
Self::MissingBoundary => "multipart/form-data without a boundary",
Self::DuplicateBoundary => "two boundary parameters",
Self::InvalidBoundary => "invalid multipart boundary",
Self::NoInitialDelimiter => {
"no multipart delimiter found: the body is not multipart or the boundary is wrong"
}
Self::BoundaryInContent => "the boundary appears inside a part",
Self::BareLineFeed => "LF without CR where CRLF is required",
Self::MalformedHeaderLine => "malformed part header line",
Self::HeaderFolding => "folded part header line",
Self::InvalidHeaderName => "invalid part header name",
Self::ControlInHeader => "control byte in a part header",
Self::MissingContentDisposition => "part without Content-Disposition",
Self::DuplicateHeader => "part header given twice",
Self::NotFormData => "Content-Disposition type is not form-data",
Self::MissingName => "part without a name",
Self::EmptyName => "part with an empty name",
Self::DuplicateParameter => "parameter given twice",
Self::MalformedParameter => "malformed header parameter",
Self::AmbiguousQuote => "quoted value contains \\\"",
Self::Truncated => "body ended before the closing boundary",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EncodingError {
InvalidPercent,
InvalidUtf8,
UnsupportedCharset,
InvalidExtendedValue,
}
impl EncodingError {
#[cfg(target_has_atomic = "32")]
const fn index(self) -> usize {
match self {
Self::InvalidPercent => 0,
Self::InvalidUtf8 => 1,
Self::UnsupportedCharset => 2,
Self::InvalidExtendedValue => 3,
}
}
const fn code(self) -> &'static str {
match self {
Self::InvalidPercent => "encoding.invalid_percent",
Self::InvalidUtf8 => "encoding.invalid_utf8",
Self::UnsupportedCharset => "encoding.unsupported_charset",
Self::InvalidExtendedValue => "encoding.invalid_extended_value",
}
}
const fn describe(self) -> &'static str {
match self {
Self::InvalidPercent => "invalid percent escape",
Self::InvalidUtf8 => "text is not valid UTF-8",
Self::UnsupportedCharset => "unsupported charset: only UTF-8 and US-ASCII are accepted",
Self::InvalidExtendedValue => "malformed filename*",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PolicyRefusal {
TransferEncoding,
NestedMultipart,
UnknownHeader,
BothFilenames,
ExtendedFilename,
TokenFieldLate,
DuplicateToken,
}
impl PolicyRefusal {
#[cfg(target_has_atomic = "32")]
const fn index(self) -> usize {
match self {
Self::TransferEncoding => 0,
Self::NestedMultipart => 1,
Self::UnknownHeader => 2,
Self::BothFilenames => 3,
Self::ExtendedFilename => 4,
Self::TokenFieldLate => 5,
Self::DuplicateToken => 6,
}
}
const fn code(self) -> &'static str {
match self {
Self::TransferEncoding => "policy.transfer_encoding",
Self::NestedMultipart => "policy.nested_multipart",
Self::UnknownHeader => "policy.unknown_header",
Self::BothFilenames => "policy.both_filenames",
Self::ExtendedFilename => "policy.extended_filename",
Self::TokenFieldLate => "policy.token_field_late",
Self::DuplicateToken => "policy.duplicate_token",
}
}
const fn describe(self) -> &'static str {
match self {
Self::TransferEncoding => "Content-Transfer-Encoding other than identity",
Self::NestedMultipart => "nested multipart part",
Self::UnknownHeader => "unknown part header",
Self::BothFilenames => "both filename and filename*",
Self::ExtendedFilename => "filename* is not accepted",
Self::TokenFieldLate => {
"file part before the anti-forgery token; treat as a possible forgery"
}
Self::DuplicateToken => "anti-forgery token given twice; treat as a possible forgery",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtractError {
Unknown,
Missing,
Duplicate,
Invalid(&'static str),
WrongKind,
}
impl ExtractError {
#[cfg(target_has_atomic = "32")]
const fn index(self) -> usize {
match self {
Self::Unknown => 0,
Self::Missing => 1,
Self::Duplicate => 2,
Self::Invalid(_) => 3,
Self::WrongKind => 4,
}
}
const fn code(self) -> &'static str {
match self {
Self::Unknown => "extract.unknown",
Self::Missing => "extract.missing",
Self::Duplicate => "extract.duplicate",
Self::Invalid(_) => "extract.invalid",
Self::WrongKind => "extract.wrong_kind",
}
}
const fn describe(self) -> &'static str {
match self {
Self::Unknown => "unknown field",
Self::Missing => "missing field",
Self::Duplicate => "field given more than once",
Self::Invalid(why) => why,
Self::WrongKind => "file where text was expected, or text where a file was expected",
}
}
}
#[cfg(target_has_atomic = "32")]
pub(crate) const SLOTS: usize = 61;
#[cfg(target_has_atomic = "32")]
pub(crate) const SLOT_CODES: [&str; SLOTS] = [
"limit.body",
"limit.fields",
"limit.name_length",
"limit.value_length",
"limit.text_total",
"limit.parts",
"limit.files",
"limit.file_size",
"limit.part_header_bytes",
"limit.part_header_count",
"limit.filename_length",
"limit.preamble",
"limit.epilogue",
"limit.padding",
"limit.token_length",
"limit.disk",
"syntax.invalid_content_type",
"syntax.missing_boundary",
"syntax.duplicate_boundary",
"syntax.invalid_boundary",
"syntax.no_initial_delimiter",
"syntax.boundary_in_content",
"syntax.bare_line_feed",
"syntax.malformed_header_line",
"syntax.header_folding",
"syntax.invalid_header_name",
"syntax.control_in_header",
"syntax.missing_content_disposition",
"syntax.duplicate_header",
"syntax.not_form_data",
"syntax.missing_name",
"syntax.empty_name",
"syntax.duplicate_parameter",
"syntax.malformed_parameter",
"syntax.ambiguous_quote",
"syntax.truncated",
"encoding.invalid_percent",
"encoding.invalid_utf8",
"encoding.unsupported_charset",
"encoding.invalid_extended_value",
"policy.transfer_encoding",
"policy.nested_multipart",
"policy.unknown_header",
"policy.both_filenames",
"policy.extended_filename",
"policy.token_field_late",
"policy.duplicate_token",
"extract.unknown",
"extract.missing",
"extract.duplicate",
"extract.invalid",
"extract.wrong_kind",
"store.quota_exceeded",
"store.not_a_directory",
"store.destination_exists",
"unsupported_media_type",
"invalid_limits",
"buffer_too_small",
"io",
"random",
"poisoned",
];
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use alloc::string::ToString;
pub(crate) fn every_error() -> alloc::vec::Vec<FormError> {
use alloc::vec::Vec;
let at = Location::NONE;
let mut all = Vec::new();
for limit in [
Limit::Body,
Limit::Fields,
Limit::NameLength,
Limit::ValueLength,
Limit::TextTotal,
Limit::Parts,
Limit::Files,
Limit::FileSize,
Limit::PartHeaderBytes,
Limit::PartHeaderCount,
Limit::FilenameLength,
Limit::Preamble,
Limit::Epilogue,
Limit::Padding,
Limit::TokenLength,
Limit::Disk,
] {
all.push(FormError::limit(limit, at));
}
for reason in [
SyntaxError::InvalidContentType,
SyntaxError::MissingBoundary,
SyntaxError::DuplicateBoundary,
SyntaxError::InvalidBoundary,
SyntaxError::NoInitialDelimiter,
SyntaxError::BoundaryInContent,
SyntaxError::BareLineFeed,
SyntaxError::MalformedHeaderLine,
SyntaxError::HeaderFolding,
SyntaxError::InvalidHeaderName,
SyntaxError::ControlInHeader,
SyntaxError::MissingContentDisposition,
SyntaxError::DuplicateHeader,
SyntaxError::NotFormData,
SyntaxError::MissingName,
SyntaxError::EmptyName,
SyntaxError::DuplicateParameter,
SyntaxError::MalformedParameter,
SyntaxError::AmbiguousQuote,
SyntaxError::Truncated,
] {
all.push(FormError::syntax(reason, at));
}
for reason in [
EncodingError::InvalidPercent,
EncodingError::InvalidUtf8,
EncodingError::UnsupportedCharset,
EncodingError::InvalidExtendedValue,
] {
all.push(FormError::encoding(reason, at));
}
for reason in [
PolicyRefusal::TransferEncoding,
PolicyRefusal::NestedMultipart,
PolicyRefusal::UnknownHeader,
PolicyRefusal::BothFilenames,
PolicyRefusal::ExtendedFilename,
PolicyRefusal::TokenFieldLate,
PolicyRefusal::DuplicateToken,
] {
all.push(FormError::policy(reason, at));
}
for reason in [
ExtractError::Unknown,
ExtractError::Missing,
ExtractError::Duplicate,
ExtractError::Invalid("not a number"),
ExtractError::WrongKind,
] {
all.push(FormError::Extract {
reason,
field: Some("f"),
});
}
#[cfg(feature = "fs")]
for reason in [
crate::store::StoreError::QuotaExceeded,
crate::store::StoreError::NotADirectory,
crate::store::StoreError::DestinationExists,
] {
all.push(FormError::Store { reason });
}
all.push(FormError::UnsupportedMediaType);
all.push(FormError::InvalidLimits { limit: Limit::Body });
all.push(FormError::BufferTooSmall { needed: 1 });
#[cfg(feature = "std")]
all.push(FormError::Io {
kind: std::io::ErrorKind::Other,
});
#[cfg(feature = "std")]
all.push(FormError::Random);
all.push(FormError::Poisoned);
all
}
#[cfg(target_has_atomic = "32")]
#[test]
fn test_every_error_has_its_own_slot_holding_its_code() {
let mut seen = [false; SLOTS];
for e in every_error() {
let slot = e.slot().unwrap();
assert_eq!(SLOT_CODES[slot], e.code(), "{e:?}");
assert!(!seen[slot], "slot {slot} twice");
seen[slot] = true;
}
let unused: alloc::vec::Vec<&str> = (0..SLOTS)
.filter(|&i| !seen[i])
.map(|i| SLOT_CODES[i])
.collect();
let mut expected = alloc::vec::Vec::new();
if !cfg!(feature = "fs") {
expected.extend([
"store.quota_exceeded",
"store.not_a_directory",
"store.destination_exists",
]);
}
if !cfg!(feature = "std") {
expected.extend(["io", "random"]);
}
assert_eq!(unused, expected);
for (i, a) in SLOT_CODES.iter().enumerate() {
assert!(!SLOT_CODES[..i].contains(a), "{a} twice");
}
let refused = FormError::Refused {
refusal: Refusal::new(422, "x"),
at: Location::NONE,
};
assert_eq!(refused.slot(), None);
}
#[test]
fn test_status_hint_every_family_maps() {
let at = Location::NONE;
assert_eq!(FormError::limit(Limit::Parts, at).status_hint(), 413);
assert_eq!(
FormError::syntax(SyntaxError::Truncated, at).status_hint(),
400
);
assert_eq!(
FormError::encoding(EncodingError::UnsupportedCharset, at).status_hint(),
415
);
assert_eq!(
FormError::encoding(EncodingError::InvalidUtf8, at).status_hint(),
400
);
assert_eq!(
FormError::policy(PolicyRefusal::NestedMultipart, at).status_hint(),
415
);
assert_eq!(
FormError::policy(PolicyRefusal::TokenFieldLate, at).status_hint(),
400
);
let extract = FormError::Extract {
reason: ExtractError::Missing,
field: Some("email"),
};
assert_eq!(extract.status_hint(), 422);
assert_eq!(
FormError::InvalidLimits { limit: Limit::Body }.status_hint(),
500
);
assert_eq!(FormError::BufferTooSmall { needed: 3 }.status_hint(), 500);
}
#[test]
fn test_status_hint_refusal_out_of_range_reports_400() {
let at = Location::NONE;
let ok = FormError::Refused {
refusal: Refusal::new(422, "x"),
at,
};
assert_eq!(ok.status_hint(), 422);
for status in [0, 200, 399, 600, 999] {
let e = FormError::Refused {
refusal: Refusal::new(status, "x"),
at,
};
assert_eq!(e.status_hint(), 400);
}
}
#[test]
fn test_code_refusal_returns_inspector_code() {
let e = FormError::Refused {
refusal: Refusal::new(422, "customs.unexpected_field"),
at: Location::NONE,
};
assert_eq!(e.code(), "customs.unexpected_field");
}
#[test]
fn test_display_never_contains_the_field_value() {
let e = FormError::Extract {
reason: ExtractError::Invalid("not a number"),
field: Some("age"),
};
assert_eq!(e.to_string(), "not a number (field `age`)");
let e = FormError::limit(Limit::Parts, Location::in_part(Some(3), 99));
assert_eq!(
e.to_string(),
"too many parts (part 3, byte 99): raise max_parts if this route accepts more parts"
);
}
#[test]
fn test_shifted_moves_input_errors_only() {
let e = FormError::encoding(EncodingError::InvalidPercent, Location::at(2)).shifted(10);
assert_eq!(e.location().map(|l| l.offset), Some(12));
assert_eq!(FormError::Poisoned.shifted(10), FormError::Poisoned);
}
#[test]
fn test_form_error_is_small_and_copy() {
fn is_copy<T: Copy>() {}
is_copy::<FormError>();
assert!(core::mem::size_of::<FormError>() <= 48);
}
}