use crate::CryptoError;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ArchiveLimits {
pub max_entry_count: u32,
pub max_total_plaintext_bytes: u64,
pub max_path_depth: u32,
pub max_path_bytes: u32,
pub max_manifest_bytes: u32,
pub max_archive_ext_bytes: u32,
pub max_entry_ext_bytes: u32,
pub max_total_entry_ext_bytes: u64,
pub max_tlv_value_bytes: u32,
}
impl ArchiveLimits {
pub fn with_max_entry_count(mut self, n: u32) -> Self {
self.max_entry_count = n;
self
}
pub fn with_max_total_plaintext_bytes(mut self, n: u64) -> Self {
self.max_total_plaintext_bytes = n;
self
}
pub fn with_max_path_depth(mut self, n: u32) -> Self {
self.max_path_depth = n;
self
}
pub fn with_max_path_bytes(mut self, n: u32) -> Self {
self.max_path_bytes = n;
self
}
pub fn with_max_manifest_bytes(mut self, n: u32) -> Self {
self.max_manifest_bytes = n;
self
}
pub fn with_max_archive_ext_bytes(mut self, n: u32) -> Self {
self.max_archive_ext_bytes = n;
self
}
pub fn with_max_entry_ext_bytes(mut self, n: u32) -> Self {
self.max_entry_ext_bytes = n;
self
}
pub fn with_max_total_entry_ext_bytes(mut self, n: u64) -> Self {
self.max_total_entry_ext_bytes = n;
self
}
pub fn with_max_tlv_value_bytes(mut self, n: u32) -> Self {
self.max_tlv_value_bytes = n;
self
}
pub(crate) fn validate(self) -> Result<Self, CryptoError> {
if self.max_path_bytes > u16::MAX as u32 {
return Err(CryptoError::InvalidInput(
"Archive path byte cap exceeds FCA u16 path length".to_string(),
));
}
Ok(self)
}
}
impl Default for ArchiveLimits {
fn default() -> Self {
Self {
max_entry_count: 250_000,
max_total_plaintext_bytes: 64 * 1024 * 1024 * 1024,
max_path_depth: 64,
max_path_bytes: 4096,
max_manifest_bytes: 64 * 1024 * 1024,
max_archive_ext_bytes: 64 * 1024,
max_entry_ext_bytes: 64 * 1024,
max_total_entry_ext_bytes: 64 * 1024 * 1024,
max_tlv_value_bytes: 16 * 1024 * 1024,
}
}
}
pub(crate) fn enforce_per_entry_caps(
entry_count: u32,
path_utf8: &str,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
enforce_entry_count_cap(entry_count, limits)?;
enforce_path_depth_cap(path_utf8, limits)?;
Ok(())
}
pub(crate) fn enforce_entry_count_cap(
entry_count: u32,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if entry_count > limits.max_entry_count {
return Err(entry_count_cap_error(entry_count, limits.max_entry_count));
}
Ok(())
}
pub(crate) fn enforce_path_depth_cap(
path_utf8: &str,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
let depth = u32::try_from(path_utf8.split('/').count()).unwrap_or(u32::MAX);
if depth > limits.max_path_depth {
return Err(path_depth_cap_error(
depth,
limits.max_path_depth,
path_utf8,
));
}
Ok(())
}
pub(crate) fn enforce_path_bytes_cap(
path_len: u32,
path: Option<&str>,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if path_len > limits.max_path_bytes {
return Err(path_bytes_cap_error(path_len, limits.max_path_bytes, path));
}
Ok(())
}
pub(crate) fn enforce_manifest_len_cap(
manifest_len: u64,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if manifest_len > u64::from(limits.max_manifest_bytes) {
return Err(manifest_len_cap_error(
manifest_len,
limits.max_manifest_bytes,
));
}
Ok(())
}
pub(crate) fn enforce_archive_ext_cap(
archive_ext_len: u64,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if archive_ext_len > u64::from(limits.max_archive_ext_bytes) {
return Err(archive_ext_cap_error(
archive_ext_len,
limits.max_archive_ext_bytes,
));
}
Ok(())
}
pub(crate) fn enforce_total_plaintext_bytes_cap(
total_file_bytes: u64,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if total_file_bytes > limits.max_total_plaintext_bytes {
return Err(total_bytes_cap_error(
total_file_bytes,
limits.max_total_plaintext_bytes,
));
}
Ok(())
}
pub(crate) fn enforce_total_bytes_cap(
entry_size: u64,
total_bytes: &mut u64,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
let next = total_bytes.checked_add(entry_size).ok_or_else(|| {
CryptoError::InvalidInput("Archive total file bytes overflow".to_string())
})?;
if next > limits.max_total_plaintext_bytes {
return Err(total_bytes_cap_error(
next,
limits.max_total_plaintext_bytes,
));
}
*total_bytes = next;
Ok(())
}
pub(crate) fn enforce_entry_ext_cap(
entry_ext_len: u64,
path: Option<&str>,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
if entry_ext_len > u64::from(limits.max_entry_ext_bytes) {
return Err(entry_ext_cap_error(
entry_ext_len,
limits.max_entry_ext_bytes,
path,
));
}
Ok(())
}
pub(crate) fn enforce_total_entry_ext_cap(
entry_ext_len: u64,
total: &mut u64,
limits: &ArchiveLimits,
) -> Result<(), CryptoError> {
let next = total.checked_add(entry_ext_len).ok_or_else(|| {
CryptoError::InvalidInput("Archive total entry-extension bytes overflow".to_string())
})?;
if next > limits.max_total_entry_ext_bytes {
return Err(total_entry_ext_cap_error(
next,
limits.max_total_entry_ext_bytes,
));
}
*total = next;
Ok(())
}
pub(super) const ARCHIVE_ENTRY_MODE_UNSUPPORTED: &str =
"Archive entry mode contains unsupported bits";
pub(super) fn entry_count_cap_error(entry_count: u32, cap: u32) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive entry-count cap exceeded ({entry_count} entries, cap {cap})"
))
}
pub(super) fn total_bytes_cap_error(total: u64, cap: u64) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive total-bytes cap exceeded ({total} bytes, cap {cap})"
))
}
pub(super) fn manifest_len_cap_error(len: u64, cap: u32) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive manifest length cap exceeded ({len} bytes, cap {cap})"
))
}
pub(super) fn path_bytes_cap_error(declared_len: u32, cap: u32, path: Option<&str>) -> CryptoError {
let head = format!("Archive path byte-length cap exceeded ({declared_len} bytes, cap {cap})");
CryptoError::InvalidInput(match path {
Some(p) => format!("{head}: {p}"),
None => head,
})
}
pub(super) fn path_depth_cap_error(depth: u32, cap: u32, path_utf8: &str) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive path depth cap exceeded ({depth} components, cap {cap}): {path_utf8}"
))
}
pub(super) fn archive_ext_cap_error(declared_len: u64, cap: u32) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive extension length cap exceeded ({declared_len} bytes, cap {cap})"
))
}
pub(super) fn entry_ext_cap_error(declared_len: u64, cap: u32, path: Option<&str>) -> CryptoError {
let head =
format!("Archive entry extension length cap exceeded ({declared_len} bytes, cap {cap})");
CryptoError::InvalidInput(match path {
Some(p) => format!("{head}: {p}"),
None => head,
})
}
pub(super) fn total_entry_ext_cap_error(total: u64, cap: u64) -> CryptoError {
CryptoError::InvalidInput(format!(
"Archive total entry-extension bytes cap exceeded ({total} bytes, cap {cap})"
))
}
#[cfg(test)]
mod tests {
use super::{
ArchiveLimits, enforce_archive_ext_cap, enforce_entry_ext_cap, enforce_manifest_len_cap,
enforce_path_bytes_cap, enforce_per_entry_caps, enforce_total_bytes_cap,
enforce_total_entry_ext_cap,
};
#[test]
fn defaults_match_spec_values() {
let l = ArchiveLimits::default();
assert_eq!(l.max_entry_count, 250_000);
assert_eq!(l.max_total_plaintext_bytes, 64 * 1024 * 1024 * 1024);
assert_eq!(l.max_path_depth, 64);
assert_eq!(l.max_path_bytes, 4096);
assert_eq!(l.max_manifest_bytes, 64 * 1024 * 1024);
assert_eq!(l.max_archive_ext_bytes, 64 * 1024);
assert_eq!(l.max_entry_ext_bytes, 64 * 1024);
assert_eq!(l.max_total_entry_ext_bytes, 64 * 1024 * 1024);
assert_eq!(l.max_tlv_value_bytes, 16 * 1024 * 1024);
}
#[test]
fn validate_accepts_defaults() {
assert!(ArchiveLimits::default().validate().is_ok());
}
#[test]
fn validate_rejects_path_bytes_above_u16_max() {
let l = ArchiveLimits::default().with_max_path_bytes(u16::MAX as u32 + 1);
let err = l.validate().unwrap_err();
assert!(format!("{err}").contains("u16 path length"));
}
#[test]
fn validate_accepts_path_bytes_at_u16_max() {
let l = ArchiveLimits::default().with_max_path_bytes(u16::MAX as u32);
assert!(l.validate().is_ok());
}
#[test]
fn builders_replace_only_targeted_field() {
let base = ArchiveLimits::default();
let l = base.with_max_entry_count(7);
assert_eq!(l.max_entry_count, 7);
assert_eq!(l.max_total_plaintext_bytes, base.max_total_plaintext_bytes);
assert_eq!(l.max_path_depth, base.max_path_depth);
assert_eq!(l.max_path_bytes, base.max_path_bytes);
assert_eq!(l.max_manifest_bytes, base.max_manifest_bytes);
let l = base.with_max_total_plaintext_bytes(123);
assert_eq!(l.max_entry_count, base.max_entry_count);
assert_eq!(l.max_total_plaintext_bytes, 123);
assert_eq!(l.max_path_depth, base.max_path_depth);
assert_eq!(l.max_path_bytes, base.max_path_bytes);
assert_eq!(l.max_manifest_bytes, base.max_manifest_bytes);
let l = base.with_max_path_depth(7);
assert_eq!(l.max_entry_count, base.max_entry_count);
assert_eq!(l.max_total_plaintext_bytes, base.max_total_plaintext_bytes);
assert_eq!(l.max_path_depth, 7);
assert_eq!(l.max_path_bytes, base.max_path_bytes);
assert_eq!(l.max_manifest_bytes, base.max_manifest_bytes);
let l = base.with_max_path_bytes(99);
assert_eq!(l.max_entry_count, base.max_entry_count);
assert_eq!(l.max_total_plaintext_bytes, base.max_total_plaintext_bytes);
assert_eq!(l.max_path_depth, base.max_path_depth);
assert_eq!(l.max_path_bytes, 99);
assert_eq!(l.max_manifest_bytes, base.max_manifest_bytes);
let l = base.with_max_manifest_bytes(42);
assert_eq!(l.max_entry_count, base.max_entry_count);
assert_eq!(l.max_total_plaintext_bytes, base.max_total_plaintext_bytes);
assert_eq!(l.max_path_depth, base.max_path_depth);
assert_eq!(l.max_path_bytes, base.max_path_bytes);
assert_eq!(l.max_manifest_bytes, 42);
let l = base.with_max_archive_ext_bytes(1234);
assert_eq!(l.max_archive_ext_bytes, 1234);
assert_eq!(l.max_entry_ext_bytes, base.max_entry_ext_bytes);
let l = base.with_max_entry_ext_bytes(5678);
assert_eq!(l.max_archive_ext_bytes, base.max_archive_ext_bytes);
assert_eq!(l.max_entry_ext_bytes, 5678);
let l = base.with_max_total_entry_ext_bytes(999);
assert_eq!(l.max_total_entry_ext_bytes, 999);
assert_eq!(l.max_tlv_value_bytes, base.max_tlv_value_bytes);
let l = base.with_max_tlv_value_bytes(321);
assert_eq!(l.max_total_entry_ext_bytes, base.max_total_entry_ext_bytes);
assert_eq!(l.max_tlv_value_bytes, 321);
}
#[test]
fn enforce_per_entry_caps_entry_count_boundary() {
let limits = ArchiveLimits::default().with_max_entry_count(10);
assert!(enforce_per_entry_caps(10, "a", &limits).is_ok());
assert!(enforce_per_entry_caps(11, "a", &limits).is_err());
}
#[test]
fn enforce_per_entry_caps_depth_boundary() {
let limits = ArchiveLimits::default().with_max_path_depth(3);
assert!(enforce_per_entry_caps(1, "a/b/c", &limits).is_ok());
assert!(enforce_per_entry_caps(1, "a/b/c/d", &limits).is_err());
}
#[test]
fn enforce_total_bytes_cap_rejects_overflow() {
let limits = ArchiveLimits::default().with_max_total_plaintext_bytes(u64::MAX);
let mut total = u64::MAX - 100;
let result = enforce_total_bytes_cap(200, &mut total, &limits);
assert!(result.is_err());
assert_eq!(
total,
u64::MAX - 100,
"total_bytes must not wrap or saturate"
);
}
#[test]
fn enforce_total_bytes_cap_boundary() {
let limits = ArchiveLimits::default().with_max_total_plaintext_bytes(100);
let mut total = 0;
assert!(enforce_total_bytes_cap(100, &mut total, &limits).is_ok());
assert_eq!(total, 100);
assert!(enforce_total_bytes_cap(1, &mut total, &limits).is_err());
assert_eq!(total, 100);
}
#[test]
fn enforce_path_bytes_cap_boundary() {
let limits = ArchiveLimits::default().with_max_path_bytes(10);
assert!(enforce_path_bytes_cap(10, None, &limits).is_ok());
assert!(enforce_path_bytes_cap(11, None, &limits).is_err());
}
#[test]
fn enforce_manifest_len_cap_boundary() {
let limits = ArchiveLimits::default().with_max_manifest_bytes(100);
assert!(enforce_manifest_len_cap(100, &limits).is_ok());
assert!(enforce_manifest_len_cap(101, &limits).is_err());
}
#[test]
fn enforce_archive_ext_cap_boundary() {
let limits = ArchiveLimits::default().with_max_archive_ext_bytes(100);
assert!(enforce_archive_ext_cap(100, &limits).is_ok());
assert!(enforce_archive_ext_cap(101, &limits).is_err());
}
#[test]
fn enforce_entry_ext_cap_boundary() {
let limits = ArchiveLimits::default().with_max_entry_ext_bytes(100);
assert!(enforce_entry_ext_cap(100, None, &limits).is_ok());
assert!(enforce_entry_ext_cap(101, None, &limits).is_err());
}
#[test]
fn enforce_total_entry_ext_cap_boundary() {
let limits = ArchiveLimits::default().with_max_total_entry_ext_bytes(100);
let mut total = 0;
assert!(enforce_total_entry_ext_cap(100, &mut total, &limits).is_ok());
assert_eq!(total, 100);
assert!(enforce_total_entry_ext_cap(1, &mut total, &limits).is_err());
assert_eq!(total, 100);
}
}