#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ThreadingPolicy {
SingleThread,
LimitOrSingle {
max_threads: u16,
},
LimitOrAny {
preferred_max_threads: u16,
},
Balanced,
#[default]
Unlimited,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResourceLimits {
pub max_pixels: Option<u64>,
pub max_memory_bytes: Option<u64>,
pub max_output_bytes: Option<u64>,
pub max_width: Option<u32>,
pub max_height: Option<u32>,
pub max_input_bytes: Option<u64>,
pub max_frames: Option<u32>,
pub max_animation_ms: Option<u64>,
pub max_total_pixels: Option<u64>,
pub threading: ThreadingPolicy,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(core::mem::size_of::<ResourceLimits>() == 128);
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_pixels: None,
max_memory_bytes: None,
max_output_bytes: None,
max_width: None,
max_height: None,
max_input_bytes: None,
max_frames: None,
max_animation_ms: None,
max_total_pixels: None,
threading: ThreadingPolicy::Unlimited,
}
}
}
impl ResourceLimits {
pub fn none() -> Self {
Self::default()
}
pub fn with_max_pixels(mut self, max: u64) -> Self {
self.max_pixels = Some(max);
self
}
pub fn with_max_memory(mut self, bytes: u64) -> Self {
self.max_memory_bytes = Some(bytes);
self
}
pub fn with_max_output(mut self, bytes: u64) -> Self {
self.max_output_bytes = Some(bytes);
self
}
pub fn with_max_width(mut self, width: u32) -> Self {
self.max_width = Some(width);
self
}
pub fn with_max_height(mut self, height: u32) -> Self {
self.max_height = Some(height);
self
}
pub fn with_max_input_bytes(mut self, bytes: u64) -> Self {
self.max_input_bytes = Some(bytes);
self
}
pub fn with_max_frames(mut self, frames: u32) -> Self {
self.max_frames = Some(frames);
self
}
pub fn with_max_animation_ms(mut self, ms: u64) -> Self {
self.max_animation_ms = Some(ms);
self
}
pub fn with_max_total_pixels(mut self, max: u64) -> Self {
self.max_total_pixels = Some(max);
self
}
pub fn with_threading(mut self, policy: ThreadingPolicy) -> Self {
self.threading = policy;
self
}
pub fn threading(&self) -> ThreadingPolicy {
self.threading
}
pub fn has_any(&self) -> bool {
self.max_pixels.is_some()
|| self.max_memory_bytes.is_some()
|| self.max_output_bytes.is_some()
|| self.max_width.is_some()
|| self.max_height.is_some()
|| self.max_input_bytes.is_some()
|| self.max_frames.is_some()
|| self.max_animation_ms.is_some()
|| self.max_total_pixels.is_some()
|| self.threading != ThreadingPolicy::Unlimited
}
pub fn check_dimensions(&self, width: u32, height: u32) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_width
&& width > max
{
return Err(LimitExceeded::Width { actual: width, max });
}
if let Some(max) = self.max_height
&& height > max
{
return Err(LimitExceeded::Height {
actual: height,
max,
});
}
if let Some(max) = self.max_pixels {
let pixels = width as u64 * height as u64;
if pixels > max {
return Err(LimitExceeded::Pixels {
actual: pixels,
max,
});
}
}
Ok(())
}
pub fn check_memory(&self, bytes: u64) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_memory_bytes
&& bytes > max
{
return Err(LimitExceeded::Memory { actual: bytes, max });
}
Ok(())
}
pub fn check_input_size(&self, bytes: u64) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_input_bytes
&& bytes > max
{
return Err(LimitExceeded::InputSize { actual: bytes, max });
}
Ok(())
}
pub fn check_output_size(&self, bytes: u64) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_output_bytes
&& bytes > max
{
return Err(LimitExceeded::OutputSize { actual: bytes, max });
}
Ok(())
}
pub fn check_frames(&self, count: u32) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_frames
&& count > max
{
return Err(LimitExceeded::Frames { actual: count, max });
}
Ok(())
}
pub fn check_animation_ms(&self, ms: u64) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_animation_ms
&& ms > max
{
return Err(LimitExceeded::Duration { actual: ms, max });
}
Ok(())
}
pub fn check_total_pixels(&self, total: u64) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_total_pixels
&& total > max
{
return Err(LimitExceeded::TotalPixels { actual: total, max });
}
Ok(())
}
pub fn check_image_info(&self, info: &crate::ImageInfo) -> Result<(), LimitExceeded> {
self.check_dimensions(info.width, info.height)?;
if let Some(max) = self.max_frames
&& let Some(count) = info.frame_count()
&& count > max
{
return Err(LimitExceeded::Frames { actual: count, max });
}
if let Some(max) = self.max_total_pixels
&& let Some(count) = info.frame_count()
{
let total = info.width as u64 * info.height as u64 * count as u64;
if total > max {
return Err(LimitExceeded::TotalPixels { actual: total, max });
}
}
Ok(())
}
pub fn check_output_info(&self, info: &crate::OutputInfo) -> Result<(), LimitExceeded> {
self.check_dimensions(info.width, info.height)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitExceeded {
Width {
actual: u32,
max: u32,
},
Height {
actual: u32,
max: u32,
},
Pixels {
actual: u64,
max: u64,
},
Memory {
actual: u64,
max: u64,
},
InputSize {
actual: u64,
max: u64,
},
OutputSize {
actual: u64,
max: u64,
},
Frames {
actual: u32,
max: u32,
},
Duration {
actual: u64,
max: u64,
},
TotalPixels {
actual: u64,
max: u64,
},
}
impl core::fmt::Display for LimitExceeded {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Width { actual, max } => write!(f, "width {actual} exceeds limit {max}"),
Self::Height { actual, max } => write!(f, "height {actual} exceeds limit {max}"),
Self::Pixels { actual, max } => {
write!(f, "pixel count {actual} exceeds limit {max}")
}
Self::Memory { actual, max } => {
write!(f, "memory {actual} bytes exceeds limit {max}")
}
Self::InputSize { actual, max } => {
write!(f, "input size {actual} bytes exceeds limit {max}")
}
Self::OutputSize { actual, max } => {
write!(f, "output size {actual} bytes exceeds limit {max}")
}
Self::Frames { actual, max } => {
write!(f, "frame count {actual} exceeds limit {max}")
}
Self::Duration { actual, max } => {
write!(f, "duration {actual}ms exceeds limit {max}ms")
}
Self::TotalPixels { actual, max } => {
write!(f, "total pixels {actual} exceeds limit {max}")
}
}
}
}
impl core::error::Error for LimitExceeded {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_has_no_limits() {
let limits = ResourceLimits::none();
assert!(!limits.has_any());
}
#[test]
fn builder_sets_limits() {
let limits = ResourceLimits::none()
.with_max_pixels(1_000_000)
.with_max_memory(512 * 1024 * 1024);
assert!(limits.has_any());
assert_eq!(limits.max_pixels, Some(1_000_000));
assert_eq!(limits.max_memory_bytes, Some(512 * 1024 * 1024));
assert!(limits.max_output_bytes.is_none());
}
#[test]
fn animation_limits() {
let limits = ResourceLimits::none()
.with_max_frames(100)
.with_max_animation_ms(30_000);
assert!(limits.has_any());
assert_eq!(limits.max_frames, Some(100));
assert_eq!(limits.max_animation_ms, Some(30_000));
}
#[test]
fn has_any_includes_animation_fields() {
let limits = ResourceLimits::none().with_max_frames(10);
assert!(limits.has_any());
let limits = ResourceLimits::none().with_max_animation_ms(5000);
assert!(limits.has_any());
}
#[test]
fn threading_policy_default() {
let limits = ResourceLimits::none();
assert_eq!(limits.threading(), ThreadingPolicy::Unlimited);
assert!(!limits.has_any());
}
#[test]
fn threading_policy_single_thread() {
let limits = ResourceLimits::none().with_threading(ThreadingPolicy::SingleThread);
assert!(limits.has_any());
assert_eq!(limits.threading(), ThreadingPolicy::SingleThread);
}
#[test]
fn threading_policy_limit_or_single() {
let limits = ResourceLimits::none()
.with_threading(ThreadingPolicy::LimitOrSingle { max_threads: 4 });
assert!(limits.has_any());
assert_eq!(
limits.threading(),
ThreadingPolicy::LimitOrSingle { max_threads: 4 }
);
}
#[test]
fn threading_policy_balanced() {
let limits = ResourceLimits::none().with_threading(ThreadingPolicy::Balanced);
assert!(limits.has_any());
}
#[test]
fn check_dimensions_pass() {
let limits = ResourceLimits::none()
.with_max_width(1920)
.with_max_height(1080)
.with_max_pixels(2_073_600);
assert!(limits.check_dimensions(1920, 1080).is_ok());
assert!(limits.check_dimensions(100, 100).is_ok());
}
#[test]
fn check_dimensions_width_exceeded() {
let limits = ResourceLimits::none().with_max_width(1920);
let err = limits.check_dimensions(1921, 1080).unwrap_err();
assert_eq!(
err,
LimitExceeded::Width {
actual: 1921,
max: 1920
}
);
}
#[test]
fn check_dimensions_height_exceeded() {
let limits = ResourceLimits::none().with_max_height(1080);
let err = limits.check_dimensions(1920, 1081).unwrap_err();
assert_eq!(
err,
LimitExceeded::Height {
actual: 1081,
max: 1080
}
);
}
#[test]
fn check_dimensions_pixels_exceeded() {
let limits = ResourceLimits::none().with_max_pixels(1_000_000);
let err = limits.check_dimensions(1001, 1000).unwrap_err();
assert_eq!(
err,
LimitExceeded::Pixels {
actual: 1_001_000,
max: 1_000_000
}
);
}
#[test]
fn check_dimensions_no_limits_always_passes() {
let limits = ResourceLimits::none();
assert!(limits.check_dimensions(100_000, 100_000).is_ok());
}
#[test]
fn check_memory_pass_and_fail() {
let limits = ResourceLimits::none().with_max_memory(512 * 1024 * 1024);
assert!(limits.check_memory(256 * 1024 * 1024).is_ok());
let err = limits.check_memory(1024 * 1024 * 1024).unwrap_err();
assert!(matches!(err, LimitExceeded::Memory { .. }));
}
#[test]
fn check_input_size_pass_and_fail() {
let limits = ResourceLimits::none().with_max_input_bytes(10 * 1024 * 1024);
assert!(limits.check_input_size(5 * 1024 * 1024).is_ok());
let err = limits.check_input_size(20 * 1024 * 1024).unwrap_err();
assert!(matches!(err, LimitExceeded::InputSize { .. }));
}
#[test]
fn check_output_size_pass_and_fail() {
let limits = ResourceLimits::none().with_max_output(1024);
assert!(limits.check_output_size(512).is_ok());
let err = limits.check_output_size(2048).unwrap_err();
assert!(matches!(err, LimitExceeded::OutputSize { .. }));
}
#[test]
fn check_frames_pass_and_fail() {
let limits = ResourceLimits::none().with_max_frames(100);
assert!(limits.check_frames(50).is_ok());
let err = limits.check_frames(200).unwrap_err();
assert_eq!(
err,
LimitExceeded::Frames {
actual: 200,
max: 100
}
);
}
#[test]
fn check_animation_ms_pass_and_fail() {
let limits = ResourceLimits::none().with_max_animation_ms(30_000);
assert!(limits.check_animation_ms(15_000).is_ok());
let err = limits.check_animation_ms(60_000).unwrap_err();
assert!(matches!(err, LimitExceeded::Duration { .. }));
}
#[test]
fn check_image_info_dimensions_and_frames() {
use crate::{ImageFormat, ImageInfo};
let limits = ResourceLimits::none()
.with_max_width(4096)
.with_max_pixels(16_000_000)
.with_max_frames(100);
let info = ImageInfo::new(3840, 2160, ImageFormat::Avif).with_sequence(
crate::ImageSequence::Animation {
frame_count: Some(50),
loop_count: None,
random_access: false,
},
);
assert!(limits.check_image_info(&info).is_ok());
let big = ImageInfo::new(5000, 4000, ImageFormat::Jpeg);
let err = limits.check_image_info(&big).unwrap_err();
assert!(matches!(err, LimitExceeded::Width { .. }));
let many_frames = ImageInfo::new(100, 100, ImageFormat::Gif).with_sequence(
crate::ImageSequence::Animation {
frame_count: Some(200),
loop_count: None,
random_access: false,
},
);
let err = limits.check_image_info(&many_frames).unwrap_err();
assert_eq!(
err,
LimitExceeded::Frames {
actual: 200,
max: 100
}
);
}
#[test]
fn limit_exceeded_display() {
use alloc::format;
let err = LimitExceeded::Width {
actual: 5000,
max: 4096,
};
assert_eq!(format!("{err}"), "width 5000 exceeds limit 4096");
let err = LimitExceeded::InputSize {
actual: 20_000_000,
max: 10_000_000,
};
assert_eq!(
format!("{err}"),
"input size 20000000 bytes exceeds limit 10000000"
);
let err = LimitExceeded::Duration {
actual: 60_000,
max: 30_000,
};
assert_eq!(format!("{err}"), "duration 60000ms exceeds limit 30000ms");
}
#[test]
fn limit_exceeded_is_error() {
fn assert_error<E: core::error::Error>(_: &E) {}
let err = LimitExceeded::Width {
actual: 5000,
max: 4096,
};
assert_error(&err);
}
#[test]
fn total_pixels_builder_and_has_any() {
let limits = ResourceLimits::none().with_max_total_pixels(100_000_000);
assert!(limits.has_any());
assert_eq!(limits.max_total_pixels, Some(100_000_000));
}
#[test]
fn check_total_pixels_pass_and_fail() {
let limits = ResourceLimits::none().with_max_total_pixels(50_000_000);
assert!(limits.check_total_pixels(50_000_000).is_ok());
let err = limits.check_total_pixels(50_000_001).unwrap_err();
assert_eq!(
err,
LimitExceeded::TotalPixels {
actual: 50_000_001,
max: 50_000_000
}
);
}
#[test]
fn check_total_pixels_no_limit() {
let limits = ResourceLimits::none();
assert!(limits.check_total_pixels(u64::MAX).is_ok());
}
#[test]
fn check_image_info_total_pixels() {
use crate::{ImageFormat, ImageInfo};
let limits = ResourceLimits::none().with_max_total_pixels(100_000_000);
let info = ImageInfo::new(1000, 1000, ImageFormat::Gif).with_sequence(
crate::ImageSequence::Animation {
frame_count: Some(200),
loop_count: None,
random_access: false,
},
);
let err = limits.check_image_info(&info).unwrap_err();
assert_eq!(
err,
LimitExceeded::TotalPixels {
actual: 200_000_000,
max: 100_000_000
}
);
}
#[test]
fn check_image_info_total_pixels_pass() {
use crate::{ImageFormat, ImageInfo};
let limits = ResourceLimits::none().with_max_total_pixels(100_000_000);
let info = ImageInfo::new(1000, 1000, ImageFormat::Gif).with_sequence(
crate::ImageSequence::Animation {
frame_count: Some(100),
loop_count: None,
random_access: false,
},
);
assert!(limits.check_image_info(&info).is_ok());
}
#[test]
fn check_image_info_total_pixels_still_image() {
use crate::{ImageFormat, ImageInfo};
let limits = ResourceLimits::none().with_max_total_pixels(1_000_000);
let info = ImageInfo::new(1000, 1000, ImageFormat::Jpeg);
assert!(limits.check_image_info(&info).is_ok());
let info = ImageInfo::new(1001, 1000, ImageFormat::Jpeg);
let err = limits.check_image_info(&info).unwrap_err();
assert_eq!(
err,
LimitExceeded::TotalPixels {
actual: 1_001_000,
max: 1_000_000
}
);
}
#[test]
fn total_pixels_display() {
use alloc::format;
let err = LimitExceeded::TotalPixels {
actual: 200_000_000,
max: 100_000_000,
};
assert_eq!(
format!("{err}"),
"total pixels 200000000 exceeds limit 100000000"
);
}
}