#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Limits {
pub max_pixels: Option<u64>,
pub max_total_pixels: 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_metadata_bytes: Option<u32>,
pub max_output_bytes: Option<u64>,
}
impl Limits {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_pixels(mut self, max: u64) -> Self {
self.max_pixels = Some(max);
self
}
#[must_use]
pub fn with_max_total_pixels(mut self, max: u64) -> Self {
self.max_total_pixels = Some(max);
self
}
#[must_use]
pub fn with_max_width(mut self, max: u32) -> Self {
self.max_width = Some(max);
self
}
#[must_use]
pub fn with_max_height(mut self, max: u32) -> Self {
self.max_height = Some(max);
self
}
#[must_use]
pub fn with_max_input_bytes(mut self, max: u64) -> Self {
self.max_input_bytes = Some(max);
self
}
#[must_use]
pub fn with_max_frames(mut self, max: u32) -> Self {
self.max_frames = Some(max);
self
}
#[must_use]
pub fn with_max_animation_ms(mut self, max: u64) -> Self {
self.max_animation_ms = Some(max);
self
}
#[must_use]
pub fn with_max_metadata_bytes(mut self, max: u32) -> Self {
self.max_metadata_bytes = Some(max);
self
}
#[must_use]
pub fn with_max_output_bytes(mut self, max: u64) -> Self {
self.max_output_bytes = Some(max);
self
}
#[must_use]
pub fn has_any(&self) -> bool {
self.max_pixels.is_some()
|| self.max_total_pixels.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_metadata_bytes.is_some()
|| self.max_output_bytes.is_some()
}
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 = u64::from(width).saturating_mul(u64::from(height));
if pixels > max {
return Err(LimitExceeded::Pixels {
actual: pixels,
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_metadata_bytes(&self, bytes: u32) -> Result<(), LimitExceeded> {
if let Some(max) = self.max_metadata_bytes
&& bytes > max
{
return Err(LimitExceeded::MetadataSize { actual: bytes, max });
}
Ok(())
}
pub fn check_still_image(&self, width: u32, height: u32) -> Result<(), LimitExceeded> {
self.check_dimensions(width, height)?;
if let Some(max) = self.max_total_pixels {
let total = u64::from(width).saturating_mul(u64::from(height));
if total > max {
return Err(LimitExceeded::TotalPixels { actual: total, max });
}
}
Ok(())
}
pub fn check_animation(
&self,
width: u32,
height: u32,
frame_count: u32,
) -> Result<(), LimitExceeded> {
self.check_dimensions(width, height)?;
self.check_frames(frame_count)?;
if let Some(max) = self.max_total_pixels {
let total = u64::from(width)
.saturating_mul(u64::from(height))
.saturating_mul(u64::from(frame_count));
if total > max {
return Err(LimitExceeded::TotalPixels { actual: total, max });
}
}
Ok(())
}
}
#[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,
},
TotalPixels {
actual: u64,
max: u64,
},
InputSize {
actual: u64,
max: u64,
},
OutputSize {
actual: u64,
max: u64,
},
Frames {
actual: u32,
max: u32,
},
Duration {
actual: u64,
max: u64,
},
MetadataSize {
actual: u32,
max: u32,
},
}
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::TotalPixels { actual, max } => {
write!(f, "total pixels {actual} 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::MetadataSize { actual, max } => {
write!(f, "metadata chunk size {actual} bytes exceeds limit {max}")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LimitExceeded {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_has_no_limits() {
assert!(!Limits::none().has_any());
}
#[test]
fn builder_sets_fields() {
let l = Limits::none()
.with_max_pixels(1_000_000)
.with_max_total_pixels(10_000_000)
.with_max_metadata_bytes(4 * 1024 * 1024);
assert!(l.has_any());
assert_eq!(l.max_pixels, Some(1_000_000));
assert_eq!(l.max_total_pixels, Some(10_000_000));
assert_eq!(l.max_metadata_bytes, Some(4 * 1024 * 1024));
}
#[test]
fn check_dimensions_pass_and_fail() {
let l = Limits::none()
.with_max_width(1920)
.with_max_height(1080)
.with_max_pixels(2_073_600);
assert!(l.check_dimensions(1920, 1080).is_ok());
assert!(matches!(
l.check_dimensions(1921, 1080),
Err(LimitExceeded::Width { .. })
));
assert!(matches!(
l.check_dimensions(1920, 1081),
Err(LimitExceeded::Height { .. })
));
}
#[test]
fn check_animation_total_pixels_catches_cumulative() {
let l = Limits::none()
.with_max_pixels(2_000_000)
.with_max_total_pixels(100_000_000);
let err = l.check_animation(1000, 1000, 200).unwrap_err();
assert_eq!(
err,
LimitExceeded::TotalPixels {
actual: 200_000_000,
max: 100_000_000
}
);
}
#[test]
fn check_still_image_includes_total_pixels() {
let l = Limits::none().with_max_total_pixels(1_000_000);
let err = l.check_still_image(1001, 1000).unwrap_err();
assert_eq!(
err,
LimitExceeded::TotalPixels {
actual: 1_001_000,
max: 1_000_000
}
);
}
#[test]
fn check_metadata_bytes() {
let l = Limits::none().with_max_metadata_bytes(4096);
assert!(l.check_metadata_bytes(2048).is_ok());
assert!(matches!(
l.check_metadata_bytes(8192),
Err(LimitExceeded::MetadataSize { .. })
));
}
#[test]
fn check_dimensions_no_limits_always_passes() {
let l = Limits::none();
assert!(l.check_dimensions(u32::MAX, u32::MAX).is_ok());
}
#[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::TotalPixels {
actual: 200_000_000,
max: 100_000_000,
};
assert_eq!(
format!("{err}"),
"total pixels 200000000 exceeds limit 100000000"
);
}
}