Skip to main content

zencodec/
limits.rs

1//! Resource limits for codec operations.
2//!
3//! [`ResourceLimits`] defines caps on resource usage. [`LimitExceeded`]
4//! is returned when a check fails. Use the `check_*` methods for
5//! parse-time rejection (fastest — reject before any pixel work).
6
7/// Threading policy for codec operations.
8///
9/// Controls how many threads a codec may use. Codecs report their
10/// supported range via
11/// [`EncodeCapabilities::threads_supported_range()`](crate::EncodeCapabilities::threads_supported_range)
12/// and [`DecodeCapabilities::threads_supported_range()`](crate::DecodeCapabilities::threads_supported_range).
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ThreadingPolicy {
16    /// Force single-threaded operation.
17    ///
18    /// Useful for deterministic output or constrained environments.
19    SingleThread,
20
21    /// Use at most `max_threads` threads. If the codec would need more,
22    /// fall back to single-threaded.
23    LimitOrSingle {
24        /// Maximum thread count before falling back to single-threaded.
25        max_threads: u16,
26    },
27
28    /// Prefer at most `preferred_max_threads` threads, but the codec
29    /// may use more if it needs to.
30    LimitOrAny {
31        /// Preferred maximum thread count (advisory, not enforced).
32        preferred_max_threads: u16,
33    },
34
35    /// Let the codec pick a reasonable thread count based on available
36    /// parallelism (typically half of available cores or similar).
37    Balanced,
38
39    /// No thread limit. Use as many threads as the codec wants.
40    #[default]
41    Unlimited,
42}
43
44/// Resource limits for encode/decode operations.
45///
46/// Used to prevent DoS attacks and resource exhaustion. All fields are optional;
47/// `None` means no limit for that resource.
48///
49/// Codecs enforce what they can — not all codecs support all limit types.
50/// Use the `check_*` methods for caller-side validation before decode/encode.
51///
52/// # Example
53///
54/// ```
55/// use zencodec::ResourceLimits;
56///
57/// let limits = ResourceLimits::none()
58///     .with_max_pixels(100_000_000)
59///     .with_max_memory(512 * 1024 * 1024);
60/// ```
61///
62/// Typical usage with a decoder:
63///
64/// ```ignore
65/// // Parse-time rejection (before any pixel work)
66/// let info = config.probe_header(data)?;
67/// limits.check_image_info(&info)?;
68/// ```
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct ResourceLimits {
72    /// Maximum total pixels (width × height).
73    pub max_pixels: Option<u64>,
74    /// Maximum memory allocation in bytes.
75    pub max_memory_bytes: Option<u64>,
76    /// Maximum encoded output size in bytes (encode only).
77    pub max_output_bytes: Option<u64>,
78    /// Maximum image width in pixels.
79    pub max_width: Option<u32>,
80    /// Maximum image height in pixels.
81    pub max_height: Option<u32>,
82    /// Maximum input data size in bytes (decode only).
83    pub max_input_bytes: Option<u64>,
84    /// Maximum number of animation frames.
85    pub max_frames: Option<u32>,
86    /// Maximum total animation duration in milliseconds.
87    pub max_animation_ms: Option<u64>,
88    /// Threading policy for the codec.
89    ///
90    /// Defaults to [`ThreadingPolicy::Unlimited`].
91    pub threading: ThreadingPolicy,
92}
93
94// All primitives, no pointers — but Option<u64> niche optimization and
95// enum discriminant alignment can differ between 32-bit and 64-bit.
96#[cfg(target_pointer_width = "64")]
97const _: () = assert!(core::mem::size_of::<ResourceLimits>() == 112);
98
99impl Default for ResourceLimits {
100    fn default() -> Self {
101        Self {
102            max_pixels: None,
103            max_memory_bytes: None,
104            max_output_bytes: None,
105            max_width: None,
106            max_height: None,
107            max_input_bytes: None,
108            max_frames: None,
109            max_animation_ms: None,
110            threading: ThreadingPolicy::Unlimited,
111        }
112    }
113}
114
115impl ResourceLimits {
116    /// No limits (all fields `None`), unlimited threading.
117    pub fn none() -> Self {
118        Self::default()
119    }
120
121    /// Set maximum total pixels.
122    pub fn with_max_pixels(mut self, max: u64) -> Self {
123        self.max_pixels = Some(max);
124        self
125    }
126
127    /// Set maximum memory allocation in bytes.
128    pub fn with_max_memory(mut self, bytes: u64) -> Self {
129        self.max_memory_bytes = Some(bytes);
130        self
131    }
132
133    /// Set maximum encoded output size in bytes.
134    pub fn with_max_output(mut self, bytes: u64) -> Self {
135        self.max_output_bytes = Some(bytes);
136        self
137    }
138
139    /// Set maximum image width in pixels.
140    pub fn with_max_width(mut self, width: u32) -> Self {
141        self.max_width = Some(width);
142        self
143    }
144
145    /// Set maximum image height in pixels.
146    pub fn with_max_height(mut self, height: u32) -> Self {
147        self.max_height = Some(height);
148        self
149    }
150
151    /// Set maximum input data size in bytes (decode only).
152    pub fn with_max_input_bytes(mut self, bytes: u64) -> Self {
153        self.max_input_bytes = Some(bytes);
154        self
155    }
156
157    /// Set maximum number of animation frames.
158    pub fn with_max_frames(mut self, frames: u32) -> Self {
159        self.max_frames = Some(frames);
160        self
161    }
162
163    /// Set maximum total animation duration in milliseconds.
164    pub fn with_max_animation_ms(mut self, ms: u64) -> Self {
165        self.max_animation_ms = Some(ms);
166        self
167    }
168
169    /// Set threading policy.
170    pub fn with_threading(mut self, policy: ThreadingPolicy) -> Self {
171        self.threading = policy;
172        self
173    }
174
175    /// Current threading policy.
176    pub fn threading(&self) -> ThreadingPolicy {
177        self.threading
178    }
179
180    /// Whether any limits are set (including non-default threading).
181    pub fn has_any(&self) -> bool {
182        self.max_pixels.is_some()
183            || self.max_memory_bytes.is_some()
184            || self.max_output_bytes.is_some()
185            || self.max_width.is_some()
186            || self.max_height.is_some()
187            || self.max_input_bytes.is_some()
188            || self.max_frames.is_some()
189            || self.max_animation_ms.is_some()
190            || self.threading != ThreadingPolicy::Unlimited
191    }
192
193    // --- Validation methods ---
194
195    /// Check image dimensions against `max_width`, `max_height`, and `max_pixels`.
196    pub fn check_dimensions(&self, width: u32, height: u32) -> Result<(), LimitExceeded> {
197        if let Some(max) = self.max_width
198            && width > max
199        {
200            return Err(LimitExceeded::Width { actual: width, max });
201        }
202        if let Some(max) = self.max_height
203            && height > max
204        {
205            return Err(LimitExceeded::Height {
206                actual: height,
207                max,
208            });
209        }
210        if let Some(max) = self.max_pixels {
211            let pixels = width as u64 * height as u64;
212            if pixels > max {
213                return Err(LimitExceeded::Pixels {
214                    actual: pixels,
215                    max,
216                });
217            }
218        }
219        Ok(())
220    }
221
222    /// Check a memory estimate against `max_memory_bytes`.
223    pub fn check_memory(&self, bytes: u64) -> Result<(), LimitExceeded> {
224        if let Some(max) = self.max_memory_bytes
225            && bytes > max
226        {
227            return Err(LimitExceeded::Memory { actual: bytes, max });
228        }
229        Ok(())
230    }
231
232    /// Check input data size against `max_input_bytes`.
233    pub fn check_input_size(&self, bytes: u64) -> Result<(), LimitExceeded> {
234        if let Some(max) = self.max_input_bytes
235            && bytes > max
236        {
237            return Err(LimitExceeded::InputSize { actual: bytes, max });
238        }
239        Ok(())
240    }
241
242    /// Check encoded output size against `max_output_bytes`.
243    pub fn check_output_size(&self, bytes: u64) -> Result<(), LimitExceeded> {
244        if let Some(max) = self.max_output_bytes
245            && bytes > max
246        {
247            return Err(LimitExceeded::OutputSize { actual: bytes, max });
248        }
249        Ok(())
250    }
251
252    /// Check frame count against `max_frames`.
253    pub fn check_frames(&self, count: u32) -> Result<(), LimitExceeded> {
254        if let Some(max) = self.max_frames
255            && count > max
256        {
257            return Err(LimitExceeded::Frames { actual: count, max });
258        }
259        Ok(())
260    }
261
262    /// Check animation duration against `max_animation_ms`.
263    pub fn check_animation_ms(&self, ms: u64) -> Result<(), LimitExceeded> {
264        if let Some(max) = self.max_animation_ms
265            && ms > max
266        {
267            return Err(LimitExceeded::Duration { actual: ms, max });
268        }
269        Ok(())
270    }
271
272    /// Check [`ImageInfo`](crate::ImageInfo) from `probe_header()` against all
273    /// applicable limits. This is the fastest rejection point — call it
274    /// immediately after probing, before any pixel work.
275    ///
276    /// Checks: `max_width`, `max_height`, `max_pixels`, `max_frames`.
277    pub fn check_image_info(&self, info: &crate::ImageInfo) -> Result<(), LimitExceeded> {
278        self.check_dimensions(info.width, info.height)?;
279        if let Some(max) = self.max_frames
280            && let Some(count) = info.frame_count()
281            && count > max
282        {
283            return Err(LimitExceeded::Frames { actual: count, max });
284        }
285        Ok(())
286    }
287
288    /// Check [`OutputInfo`](crate::decode::OutputInfo) against dimension limits.
289    ///
290    /// Checks: `max_width`, `max_height`, `max_pixels`.
291    pub fn check_output_info(&self, info: &crate::OutputInfo) -> Result<(), LimitExceeded> {
292        self.check_dimensions(info.width, info.height)
293    }
294}
295
296/// A resource limit was exceeded.
297///
298/// Returned by [`ResourceLimits::check_dimensions()`] and related methods.
299/// Each variant carries the actual value and the limit that was exceeded,
300/// enabling useful error messages.
301///
302/// Implements [`core::error::Error`] so codecs can wrap it in their own
303/// error types.
304#[derive(Clone, Debug, PartialEq, Eq)]
305#[non_exhaustive]
306pub enum LimitExceeded {
307    /// Image width exceeded `max_width`.
308    Width {
309        /// Actual width.
310        actual: u32,
311        /// Maximum allowed.
312        max: u32,
313    },
314    /// Image height exceeded `max_height`.
315    Height {
316        /// Actual height.
317        actual: u32,
318        /// Maximum allowed.
319        max: u32,
320    },
321    /// Pixel count exceeded `max_pixels`.
322    Pixels {
323        /// Actual pixel count.
324        actual: u64,
325        /// Maximum allowed.
326        max: u64,
327    },
328    /// Memory exceeded `max_memory_bytes`.
329    Memory {
330        /// Estimated memory in bytes.
331        actual: u64,
332        /// Maximum allowed.
333        max: u64,
334    },
335    /// Input data size exceeded `max_input_bytes`.
336    InputSize {
337        /// Actual input size in bytes.
338        actual: u64,
339        /// Maximum allowed.
340        max: u64,
341    },
342    /// Encoded output exceeded `max_output_bytes`.
343    OutputSize {
344        /// Actual or estimated output size in bytes.
345        actual: u64,
346        /// Maximum allowed.
347        max: u64,
348    },
349    /// Frame count exceeded `max_frames`.
350    Frames {
351        /// Actual frame count.
352        actual: u32,
353        /// Maximum allowed.
354        max: u32,
355    },
356    /// Animation duration exceeded `max_animation_ms`.
357    Duration {
358        /// Actual duration in milliseconds.
359        actual: u64,
360        /// Maximum allowed.
361        max: u64,
362    },
363}
364
365impl core::fmt::Display for LimitExceeded {
366    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367        match self {
368            Self::Width { actual, max } => write!(f, "width {actual} exceeds limit {max}"),
369            Self::Height { actual, max } => write!(f, "height {actual} exceeds limit {max}"),
370            Self::Pixels { actual, max } => {
371                write!(f, "pixel count {actual} exceeds limit {max}")
372            }
373            Self::Memory { actual, max } => {
374                write!(f, "memory {actual} bytes exceeds limit {max}")
375            }
376            Self::InputSize { actual, max } => {
377                write!(f, "input size {actual} bytes exceeds limit {max}")
378            }
379            Self::OutputSize { actual, max } => {
380                write!(f, "output size {actual} bytes exceeds limit {max}")
381            }
382            Self::Frames { actual, max } => {
383                write!(f, "frame count {actual} exceeds limit {max}")
384            }
385            Self::Duration { actual, max } => {
386                write!(f, "duration {actual}ms exceeds limit {max}ms")
387            }
388        }
389    }
390}
391
392impl core::error::Error for LimitExceeded {}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn default_has_no_limits() {
400        let limits = ResourceLimits::none();
401        assert!(!limits.has_any());
402    }
403
404    #[test]
405    fn builder_sets_limits() {
406        let limits = ResourceLimits::none()
407            .with_max_pixels(1_000_000)
408            .with_max_memory(512 * 1024 * 1024);
409        assert!(limits.has_any());
410        assert_eq!(limits.max_pixels, Some(1_000_000));
411        assert_eq!(limits.max_memory_bytes, Some(512 * 1024 * 1024));
412        assert!(limits.max_output_bytes.is_none());
413    }
414
415    #[test]
416    fn animation_limits() {
417        let limits = ResourceLimits::none()
418            .with_max_frames(100)
419            .with_max_animation_ms(30_000);
420        assert!(limits.has_any());
421        assert_eq!(limits.max_frames, Some(100));
422        assert_eq!(limits.max_animation_ms, Some(30_000));
423    }
424
425    #[test]
426    fn has_any_includes_animation_fields() {
427        let limits = ResourceLimits::none().with_max_frames(10);
428        assert!(limits.has_any());
429
430        let limits = ResourceLimits::none().with_max_animation_ms(5000);
431        assert!(limits.has_any());
432    }
433
434    #[test]
435    fn threading_policy_default() {
436        let limits = ResourceLimits::none();
437        assert_eq!(limits.threading(), ThreadingPolicy::Unlimited);
438        assert!(!limits.has_any());
439    }
440
441    #[test]
442    fn threading_policy_single_thread() {
443        let limits = ResourceLimits::none().with_threading(ThreadingPolicy::SingleThread);
444        assert!(limits.has_any());
445        assert_eq!(limits.threading(), ThreadingPolicy::SingleThread);
446    }
447
448    #[test]
449    fn threading_policy_limit_or_single() {
450        let limits = ResourceLimits::none()
451            .with_threading(ThreadingPolicy::LimitOrSingle { max_threads: 4 });
452        assert!(limits.has_any());
453        assert_eq!(
454            limits.threading(),
455            ThreadingPolicy::LimitOrSingle { max_threads: 4 }
456        );
457    }
458
459    #[test]
460    fn threading_policy_balanced() {
461        let limits = ResourceLimits::none().with_threading(ThreadingPolicy::Balanced);
462        assert!(limits.has_any());
463    }
464
465    // --- Validation tests ---
466
467    #[test]
468    fn check_dimensions_pass() {
469        let limits = ResourceLimits::none()
470            .with_max_width(1920)
471            .with_max_height(1080)
472            .with_max_pixels(2_073_600);
473        assert!(limits.check_dimensions(1920, 1080).is_ok());
474        assert!(limits.check_dimensions(100, 100).is_ok());
475    }
476
477    #[test]
478    fn check_dimensions_width_exceeded() {
479        let limits = ResourceLimits::none().with_max_width(1920);
480        let err = limits.check_dimensions(1921, 1080).unwrap_err();
481        assert_eq!(
482            err,
483            LimitExceeded::Width {
484                actual: 1921,
485                max: 1920
486            }
487        );
488    }
489
490    #[test]
491    fn check_dimensions_height_exceeded() {
492        let limits = ResourceLimits::none().with_max_height(1080);
493        let err = limits.check_dimensions(1920, 1081).unwrap_err();
494        assert_eq!(
495            err,
496            LimitExceeded::Height {
497                actual: 1081,
498                max: 1080
499            }
500        );
501    }
502
503    #[test]
504    fn check_dimensions_pixels_exceeded() {
505        let limits = ResourceLimits::none().with_max_pixels(1_000_000);
506        let err = limits.check_dimensions(1001, 1000).unwrap_err();
507        assert_eq!(
508            err,
509            LimitExceeded::Pixels {
510                actual: 1_001_000,
511                max: 1_000_000
512            }
513        );
514    }
515
516    #[test]
517    fn check_dimensions_no_limits_always_passes() {
518        let limits = ResourceLimits::none();
519        assert!(limits.check_dimensions(100_000, 100_000).is_ok());
520    }
521
522    #[test]
523    fn check_memory_pass_and_fail() {
524        let limits = ResourceLimits::none().with_max_memory(512 * 1024 * 1024);
525        assert!(limits.check_memory(256 * 1024 * 1024).is_ok());
526        let err = limits.check_memory(1024 * 1024 * 1024).unwrap_err();
527        assert!(matches!(err, LimitExceeded::Memory { .. }));
528    }
529
530    #[test]
531    fn check_input_size_pass_and_fail() {
532        let limits = ResourceLimits::none().with_max_input_bytes(10 * 1024 * 1024);
533        assert!(limits.check_input_size(5 * 1024 * 1024).is_ok());
534        let err = limits.check_input_size(20 * 1024 * 1024).unwrap_err();
535        assert!(matches!(err, LimitExceeded::InputSize { .. }));
536    }
537
538    #[test]
539    fn check_output_size_pass_and_fail() {
540        let limits = ResourceLimits::none().with_max_output(1024);
541        assert!(limits.check_output_size(512).is_ok());
542        let err = limits.check_output_size(2048).unwrap_err();
543        assert!(matches!(err, LimitExceeded::OutputSize { .. }));
544    }
545
546    #[test]
547    fn check_frames_pass_and_fail() {
548        let limits = ResourceLimits::none().with_max_frames(100);
549        assert!(limits.check_frames(50).is_ok());
550        let err = limits.check_frames(200).unwrap_err();
551        assert_eq!(
552            err,
553            LimitExceeded::Frames {
554                actual: 200,
555                max: 100
556            }
557        );
558    }
559
560    #[test]
561    fn check_animation_ms_pass_and_fail() {
562        let limits = ResourceLimits::none().with_max_animation_ms(30_000);
563        assert!(limits.check_animation_ms(15_000).is_ok());
564        let err = limits.check_animation_ms(60_000).unwrap_err();
565        assert!(matches!(err, LimitExceeded::Duration { .. }));
566    }
567
568    #[test]
569    fn check_image_info_dimensions_and_frames() {
570        use crate::{ImageFormat, ImageInfo};
571        let limits = ResourceLimits::none()
572            .with_max_width(4096)
573            .with_max_pixels(16_000_000)
574            .with_max_frames(100);
575
576        let info = ImageInfo::new(3840, 2160, ImageFormat::Avif).with_sequence(
577            crate::ImageSequence::Animation {
578                frame_count: Some(50),
579                loop_count: None,
580                random_access: false,
581            },
582        );
583        assert!(limits.check_image_info(&info).is_ok());
584
585        let big = ImageInfo::new(5000, 4000, ImageFormat::Jpeg);
586        let err = limits.check_image_info(&big).unwrap_err();
587        assert!(matches!(err, LimitExceeded::Width { .. }));
588
589        let many_frames = ImageInfo::new(100, 100, ImageFormat::Gif).with_sequence(
590            crate::ImageSequence::Animation {
591                frame_count: Some(200),
592                loop_count: None,
593                random_access: false,
594            },
595        );
596        let err = limits.check_image_info(&many_frames).unwrap_err();
597        assert_eq!(
598            err,
599            LimitExceeded::Frames {
600                actual: 200,
601                max: 100
602            }
603        );
604    }
605
606    #[test]
607    fn limit_exceeded_display() {
608        use alloc::format;
609        let err = LimitExceeded::Width {
610            actual: 5000,
611            max: 4096,
612        };
613        assert_eq!(format!("{err}"), "width 5000 exceeds limit 4096");
614
615        let err = LimitExceeded::InputSize {
616            actual: 20_000_000,
617            max: 10_000_000,
618        };
619        assert_eq!(
620            format!("{err}"),
621            "input size 20000000 bytes exceeds limit 10000000"
622        );
623
624        let err = LimitExceeded::Duration {
625            actual: 60_000,
626            max: 30_000,
627        };
628        assert_eq!(format!("{err}"), "duration 60000ms exceeds limit 30000ms");
629    }
630
631    #[test]
632    fn limit_exceeded_is_error() {
633        fn assert_error<E: core::error::Error>(_: &E) {}
634        let err = LimitExceeded::Width {
635            actual: 5000,
636            max: 4096,
637        };
638        assert_error(&err);
639    }
640}