use crate::error::{FormError, Limit};
pub(crate) const CHARSET_CAPTURE: usize = 32;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
pub max_body_bytes: u64,
pub max_fields: u32,
pub max_name_bytes: u32,
pub max_value_bytes: u32,
pub max_text_bytes: u64,
pub max_parts: u32,
pub max_files: u32,
pub max_file_bytes: u64,
pub max_part_header_bytes: u32,
pub max_part_headers: u16,
pub max_filename_bytes: u32,
pub max_preamble_bytes: u32,
pub max_epilogue_bytes: u32,
pub max_padding_bytes: u8,
pub sniff_bytes: u32,
pub max_token_bytes: u16,
}
macro_rules! setter {
($(#[$doc:meta])* $name:ident, $field:ident, $ty:ty) => {
$(#[$doc])*
#[must_use]
pub const fn $name(mut self, value: $ty) -> Self {
self.$field = value;
self
}
};
}
const fn min_u64(a: u64, b: u64) -> u64 {
if a < b { a } else { b }
}
const fn to_usize(value: u64) -> usize {
if value > usize::MAX as u64 {
usize::MAX
} else {
value as usize
}
}
const fn add_usize(a: usize, b: usize) -> usize {
match a.checked_add(b) {
Some(v) => v,
None => usize::MAX,
}
}
impl Limits {
pub const DEFAULT: Limits = Limits {
max_body_bytes: 10 * 1024 * 1024,
max_fields: 1_000,
max_name_bytes: 256,
max_value_bytes: 1024 * 1024,
max_text_bytes: 2 * 1024 * 1024,
max_parts: 100,
max_files: 20,
max_file_bytes: 10 * 1024 * 1024,
max_part_header_bytes: 8 * 1024,
max_part_headers: 8,
max_filename_bytes: 1_024,
max_preamble_bytes: 1_024,
max_epilogue_bytes: 1_024,
max_padding_bytes: 32,
sniff_bytes: 0,
max_token_bytes: 128,
};
setter!(
with_max_body_bytes, max_body_bytes, u64);
setter!(
with_max_fields, max_fields, u32);
setter!(
with_max_name_bytes, max_name_bytes, u32);
setter!(
with_max_value_bytes, max_value_bytes, u32);
setter!(
with_max_text_bytes, max_text_bytes, u64);
setter!(
with_max_parts, max_parts, u32);
setter!(
with_max_files, max_files, u32);
setter!(
with_max_file_bytes, max_file_bytes, u64);
setter!(
with_max_part_header_bytes, max_part_header_bytes, u32);
setter!(
with_max_part_headers, max_part_headers, u16);
setter!(
with_max_filename_bytes, max_filename_bytes, u32);
setter!(
with_max_preamble_bytes, max_preamble_bytes, u32);
setter!(
with_max_epilogue_bytes, max_epilogue_bytes, u32);
setter!(
with_max_padding_bytes, max_padding_bytes, u8);
setter!(
with_sniff_bytes, sniff_bytes, u32);
setter!(
with_max_token_bytes, max_token_bytes, u16);
pub fn validate(&self) -> Result<(), FormError> {
if self.max_part_header_bytes < 64 {
return Err(FormError::InvalidLimits {
limit: Limit::PartHeaderBytes,
});
}
let parts = [
u64::from(self.max_part_header_bytes),
u64::from(self.name()),
u64::from(self.filename()),
u64::from(self.token()),
CHARSET_CAPTURE as u64,
];
let total = parts.iter().fold(0u64, |a, &b| a.saturating_add(b));
if total > usize::MAX as u64 {
return Err(FormError::InvalidLimits {
limit: Limit::PartHeaderBytes,
});
}
let url = u64::from(self.name())
.saturating_add(u64::from(self.value()))
.saturating_add(1);
if url > usize::MAX as u64 {
return Err(FormError::InvalidLimits {
limit: Limit::ValueLength,
});
}
Ok(())
}
pub(crate) fn validate_with(&self, policy: &crate::Policy) -> Result<(), FormError> {
self.validate()?;
if policy.token_field.is_some() && self.token() == 0 {
return Err(FormError::InvalidLimits {
limit: Limit::TokenLength,
});
}
Ok(())
}
#[must_use]
pub const fn multipart_buffer_len(&self, policy: &crate::Policy) -> usize {
let mut len = add_usize(self.max_part_header_bytes as usize, self.name() as usize);
if matches!(
policy.extended_filename,
crate::ExtendedFilenamePolicy::AcceptAlone
) {
len = add_usize(len, self.filename() as usize);
}
if policy.token_field.is_some() {
len = add_usize(len, self.token() as usize);
}
add_usize(len, CHARSET_CAPTURE)
}
#[must_use]
pub const fn urlencoded_buffer_len(&self) -> usize {
add_usize(add_usize(self.name() as usize, self.value() as usize), 1)
}
pub(crate) const fn body(&self) -> u64 {
self.max_body_bytes
}
pub(crate) const fn text(&self) -> u64 {
min_u64(self.max_text_bytes, self.body())
}
pub(crate) const fn value(&self) -> u32 {
min_u64(self.max_value_bytes as u64, self.text()) as u32
}
pub(crate) const fn name(&self) -> u32 {
min_u64(self.max_name_bytes as u64, self.body()) as u32
}
pub(crate) const fn file(&self) -> u64 {
min_u64(self.max_file_bytes, self.body())
}
pub(crate) const fn filename(&self) -> u32 {
min_u64(
self.max_filename_bytes as u64,
self.max_part_header_bytes as u64,
) as u32
}
pub(crate) const fn token(&self) -> u16 {
min_u64(self.max_token_bytes as u64, self.value() as u64) as u16
}
pub(crate) const fn preamble(&self) -> u64 {
min_u64(self.max_preamble_bytes as u64, self.body())
}
pub(crate) const fn epilogue(&self) -> u64 {
min_u64(self.max_epilogue_bytes as u64, self.body())
}
pub(crate) const fn body_usize(&self) -> usize {
to_usize(self.body())
}
}
impl Default for Limits {
fn default() -> Self {
Self::DEFAULT
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Policy {
pub percent: PercentPolicy,
pub extended_filename: ExtendedFilenamePolicy,
pub unknown_headers: HeaderPolicy,
pub empty_files: EmptyFilePolicy,
pub token_field: Option<TokenField>,
}
impl Policy {
pub const DEFAULT: Policy = Policy {
percent: PercentPolicy::Refuse,
extended_filename: ExtendedFilenamePolicy::Refuse,
unknown_headers: HeaderPolicy::Ignore,
empty_files: EmptyFilePolicy::Report,
token_field: None,
};
#[must_use]
pub const fn with_percent(mut self, policy: PercentPolicy) -> Self {
self.percent = policy;
self
}
#[must_use]
pub const fn with_extended_filename(mut self, policy: ExtendedFilenamePolicy) -> Self {
self.extended_filename = policy;
self
}
#[must_use]
pub const fn with_unknown_headers(mut self, policy: HeaderPolicy) -> Self {
self.unknown_headers = policy;
self
}
#[must_use]
pub const fn with_empty_files(mut self, policy: EmptyFilePolicy) -> Self {
self.empty_files = policy;
self
}
#[must_use]
pub const fn with_token_field(mut self, field: TokenField) -> Self {
self.token_field = Some(field);
self
}
}
impl Default for Policy {
fn default() -> Self {
Self::DEFAULT
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PercentPolicy {
Refuse,
Literal,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtendedFilenamePolicy {
Refuse,
AcceptAlone,
Ignore,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HeaderPolicy {
Ignore,
Refuse,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EmptyFilePolicy {
Report,
Skip,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TokenField {
name: &'static str,
placement: TokenPlacement,
}
impl TokenField {
#[must_use]
pub const fn new(name: &'static str) -> TokenField {
TokenField {
name,
placement: TokenPlacement::BeforeFirstFile,
}
}
#[must_use]
pub const fn anywhere(self) -> TokenField {
TokenField {
name: self.name,
placement: TokenPlacement::Anywhere,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn placement(&self) -> TokenPlacement {
self.placement
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TokenPlacement {
BeforeFirstFile,
Anywhere,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_limits_default_validates() {
assert!(Limits::DEFAULT.validate().is_ok());
assert_eq!(Limits::default(), Limits::DEFAULT);
}
#[test]
fn test_limits_header_budget_below_64_refused() {
for bytes in [0, 1, 63] {
let l = Limits::DEFAULT.with_max_part_header_bytes(bytes);
assert_eq!(
l.validate(),
Err(FormError::InvalidLimits {
limit: Limit::PartHeaderBytes
})
);
}
assert!(
Limits::DEFAULT
.with_max_part_header_bytes(64)
.validate()
.is_ok()
);
}
#[test]
fn test_limits_containing_limit_caps_contained() {
let l = Limits::DEFAULT.with_max_body_bytes(4096);
assert_eq!(l.text(), 4096);
assert_eq!(l.value(), 4096);
assert_eq!(l.file(), 4096);
assert_eq!(l.name(), 256);
assert_eq!(l.token(), 128);
let l = l.with_max_value_bytes(10);
assert_eq!((l.value(), l.token()), (10, 10));
let l = Limits::DEFAULT.with_max_part_header_bytes(100);
assert_eq!(l.filename(), 100);
let l = Limits::DEFAULT.with_sniff_bytes(64).with_max_file_bytes(16);
assert!(l.validate().is_ok());
}
#[test]
fn test_limits_zero_token_with_token_field_refused() {
let policy = Policy::DEFAULT.with_token_field(TokenField::new("t"));
let l = Limits::DEFAULT.with_max_token_bytes(0);
assert_eq!(
l.validate_with(&policy),
Err(FormError::InvalidLimits {
limit: Limit::TokenLength
})
);
assert!(l.validate_with(&Policy::DEFAULT).is_ok());
}
#[test]
fn test_buffer_lengths_follow_policy() {
let l = Limits::DEFAULT;
assert_eq!(l.multipart_buffer_len(&Policy::DEFAULT), 8192 + 256 + 32);
let p = Policy::DEFAULT
.with_extended_filename(ExtendedFilenamePolicy::AcceptAlone)
.with_token_field(TokenField::new("t"));
assert_eq!(l.multipart_buffer_len(&p), 8192 + 256 + 1024 + 128 + 32);
assert_eq!(l.urlencoded_buffer_len(), 256 + 1024 * 1024 + 1);
}
#[test]
fn test_setters_set_one_field_each() {
let l = Limits::DEFAULT
.with_max_body_bytes(1)
.with_max_fields(2)
.with_max_name_bytes(3)
.with_max_value_bytes(4)
.with_max_text_bytes(5)
.with_max_parts(6)
.with_max_files(7)
.with_max_file_bytes(8)
.with_max_part_header_bytes(9)
.with_max_part_headers(10)
.with_max_filename_bytes(11)
.with_max_preamble_bytes(12)
.with_max_epilogue_bytes(13)
.with_max_padding_bytes(14)
.with_sniff_bytes(15)
.with_max_token_bytes(16);
assert_eq!(
(
l.max_body_bytes,
l.max_fields,
l.max_name_bytes,
l.max_value_bytes
),
(1, 2, 3, 4)
);
assert_eq!(
(l.max_text_bytes, l.max_parts, l.max_files, l.max_file_bytes),
(5, 6, 7, 8)
);
assert_eq!(
(
l.max_part_header_bytes,
l.max_part_headers,
l.max_filename_bytes,
l.max_preamble_bytes
),
(9, 10, 11, 12)
);
assert_eq!(
(
l.max_epilogue_bytes,
l.max_padding_bytes,
l.sniff_bytes,
l.max_token_bytes
),
(13, 14, 15, 16)
);
}
#[test]
fn test_policy_setters() {
let p = Policy::DEFAULT
.with_percent(PercentPolicy::Literal)
.with_extended_filename(ExtendedFilenamePolicy::Ignore)
.with_unknown_headers(HeaderPolicy::Refuse)
.with_empty_files(EmptyFilePolicy::Skip)
.with_token_field(TokenField::new("t").anywhere());
assert_eq!(p.percent, PercentPolicy::Literal);
assert_eq!(p.extended_filename, ExtendedFilenamePolicy::Ignore);
assert_eq!(p.unknown_headers, HeaderPolicy::Refuse);
assert_eq!(p.empty_files, EmptyFilePolicy::Skip);
assert_eq!(
p.token_field.map(|t| (t.name(), t.placement())),
Some(("t", TokenPlacement::Anywhere))
);
assert_eq!(Policy::default(), Policy::DEFAULT);
}
}