use crate::{PdfError, Result};
use crate::ResourceLimits;
const ABSOLUTE_SAFE_DECOMPRESSED_SIZE: u64 = 10 * 1024;
pub fn guard_decompression_bomb(
compressed_size: u64,
decompressed_size: u64,
limits: &ResourceLimits,
) -> Result<()> {
if decompressed_size > limits.max_decompressed_size() {
return Err(PdfError::SecurityViolation(format!(
"decompressed size {} bytes exceeds limit {} bytes",
decompressed_size,
limits.max_decompressed_size(),
)));
}
if decompressed_size < ABSOLUTE_SAFE_DECOMPRESSED_SIZE {
return Ok(());
}
if let Some(raw_ratio) = decompressed_size.checked_div(compressed_size) {
let ratio = u32::try_from(raw_ratio).unwrap_or(u32::MAX);
if ratio > limits.max_compression_ratio() {
return Err(PdfError::SecurityViolation(format!(
"compression ratio {ratio}x exceeds limit {}x",
limits.max_compression_ratio(),
)));
}
}
Ok(())
}
pub fn guard_element_explosion(element_count: usize, limits: &ResourceLimits) -> Result<()> {
if element_count > limits.max_element_count() {
return Err(PdfError::SecurityViolation(format!(
"element count {} exceeds limit {}",
element_count,
limits.max_element_count(),
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decompression_within_limits_passes() {
let limits = ResourceLimits::default();
assert!(guard_decompression_bomb(1_000, 10_000, &limits).is_ok());
}
#[test]
fn decompression_exact_limit_passes() {
let limits = ResourceLimits::default();
let max = limits.max_decompressed_size();
assert!(guard_decompression_bomb(max, max, &limits).is_ok());
}
#[test]
fn decompression_size_exceeded_rejected() {
let limits = ResourceLimits::default();
let over = limits.max_decompressed_size() + 1;
let result = guard_decompression_bomb(1_000, over, &limits);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("decompressed size"),
"unexpected message: {msg}"
);
}
#[test]
fn decompression_ratio_exceeded_rejected() {
let limits = ResourceLimits::default();
let result = guard_decompression_bomb(100_000, 20_000_000, &limits);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("compression ratio"),
"unexpected message: {msg}"
);
}
#[test]
fn decompression_ratio_applied_for_small_compressed_large_decompressed() {
let limits = ResourceLimits::default();
assert!(guard_decompression_bomb(100, 1_000_000, &limits).is_err());
}
#[test]
fn decompression_ratio_skipped_for_tiny_decompressed() {
let limits = ResourceLimits::default();
assert!(guard_decompression_bomb(100, 5_000, &limits).is_ok());
}
#[test]
fn decompression_strict_limits_are_tighter() {
let limits = ResourceLimits::strict();
assert!(guard_decompression_bomb(100_000, 5_000_000, &limits).is_ok());
assert!(guard_decompression_bomb(100_000, 6_000_000, &limits).is_err());
}
#[test]
fn decompression_zero_compressed_size_passes() {
let limits = ResourceLimits::default();
assert!(guard_decompression_bomb(0, 1_000, &limits).is_ok());
}
#[test]
fn element_within_limits_passes() {
let limits = ResourceLimits::default();
assert!(guard_element_explosion(1_000, &limits).is_ok());
}
#[test]
fn element_exact_limit_passes() {
let limits = ResourceLimits::default();
assert!(guard_element_explosion(limits.max_element_count(), &limits).is_ok());
}
#[test]
fn element_exceeded_rejected() {
let limits = ResourceLimits::default();
let over = limits.max_element_count() + 1;
let result = guard_element_explosion(over, &limits);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("element count"), "unexpected message: {msg}");
}
#[test]
fn element_strict_limits_are_tighter() {
let limits = ResourceLimits::strict();
assert!(guard_element_explosion(999_999, &limits).is_ok());
assert!(guard_element_explosion(1_000_001, &limits).is_err());
}
#[test]
fn security_violation_error_code() {
let limits = ResourceLimits::default();
let err = guard_element_explosion(usize::MAX, &limits).unwrap_err();
assert_eq!(err.code(), crate::PdfErrorCode::SecurityViolation);
}
}