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,
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,
}
}
#[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",
Self::Poisoned => "poisoned",
}
}
#[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",
),
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 {
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 {
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 {
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 {
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 {
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(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[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);
}
}