Skip to main content

djvu_rs/
resource_limits.rs

1//! Configurable resource limits shared by validation, parse, and render entry points.
2
3/// Documented default ceiling for render output pixel area (`width * height`).
4pub const DEFAULT_MAX_RENDER_PIXELS: u64 = 512 * 1024 * 1024;
5
6/// Which configured resource axis was exceeded.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ResourceLimitAxis {
9    /// Input file size in bytes.
10    FileBytes,
11    /// Page count from INFO-bearing components.
12    PageCount,
13    /// Embedded component count in a bundled DJVM.
14    ComponentCount,
15    /// Single-page pixel area from an INFO chunk.
16    PagePixels,
17    /// Sum of every page's pixel area.
18    TotalPixels,
19    /// Estimated peak decoded-page memory in bytes.
20    DecodedBytes,
21    /// Render output pixel area (`width * height`).
22    RenderOutputPixels,
23}
24
25/// A configured resource limit was exceeded by a public decode/render operation.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ResourceLimitExceeded {
28    /// Public entry point that rejected the input (for example `"document.parse"`
29    /// or `"render_pixmap"`).
30    pub operation: &'static str,
31    /// Which limit axis was exceeded.
32    pub axis: ResourceLimitAxis,
33    /// Observed value that exceeded the limit.
34    pub found: u64,
35    /// Configured limit for the axis.
36    pub limit: u64,
37    /// 1-based page number when [`Self::axis`] is [`ResourceLimitAxis::PagePixels`].
38    pub page_number: Option<usize>,
39    /// Page or render width when relevant.
40    pub width: Option<u32>,
41    /// Page or render height when relevant.
42    pub height: Option<u32>,
43}
44
45impl core::fmt::Display for ResourceLimitExceeded {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        match self.axis {
48            ResourceLimitAxis::PagePixels => write!(
49                f,
50                "{}: page {} is {}x{} = {} pixels, exceeding limit {}",
51                self.operation,
52                self.page_number.unwrap_or(0),
53                self.width.unwrap_or(0),
54                self.height.unwrap_or(0),
55                self.found,
56                self.limit
57            ),
58            ResourceLimitAxis::RenderOutputPixels => write!(
59                f,
60                "{}: render output {}x{} = {} pixels exceeds limit {}",
61                self.operation,
62                self.width.unwrap_or(0),
63                self.height.unwrap_or(0),
64                self.found,
65                self.limit
66            ),
67            ResourceLimitAxis::FileBytes => write!(
68                f,
69                "{}: file is {} bytes, exceeding limit {}",
70                self.operation, self.found, self.limit
71            ),
72            ResourceLimitAxis::PageCount => write!(
73                f,
74                "{}: document has {} pages, exceeding limit {}",
75                self.operation, self.found, self.limit
76            ),
77            ResourceLimitAxis::ComponentCount => write!(
78                f,
79                "{}: document has {} components, exceeding limit {}",
80                self.operation, self.found, self.limit
81            ),
82            ResourceLimitAxis::TotalPixels => write!(
83                f,
84                "{}: document totals {} pixels, exceeding limit {}",
85                self.operation, self.found, self.limit
86            ),
87            ResourceLimitAxis::DecodedBytes => write!(
88                f,
89                "{}: peak decoded page memory is an estimated {} bytes, exceeding limit {}",
90                self.operation, self.found, self.limit
91            ),
92        }
93    }
94}
95
96impl core::error::Error for ResourceLimitExceeded {}
97
98/// Configured processing limits checked by the resource layer.
99#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
100pub struct ResourceLimits {
101    /// Maximum accepted input size in bytes.
102    pub max_file_bytes: Option<u64>,
103    /// Maximum accepted page count.
104    pub max_pages: Option<u64>,
105    /// Maximum accepted embedded component count (bundled documents).
106    pub max_components: Option<u64>,
107    /// Maximum accepted pixel area (`width * height`) of any single page.
108    pub max_page_pixels: Option<u64>,
109    /// Maximum accepted sum of every page's pixel area.
110    pub max_total_pixels: Option<u64>,
111    /// Maximum accepted peak decoded-page memory, in bytes.
112    pub max_decoded_bytes: Option<u64>,
113    /// Maximum accepted render output pixel area (`width * height`).
114    pub max_render_pixels: Option<u64>,
115}
116
117impl ResourceLimits {
118    /// Whether every limit field is unset.
119    pub const fn is_empty(&self) -> bool {
120        self.max_file_bytes.is_none()
121            && self.max_pages.is_none()
122            && self.max_components.is_none()
123            && self.max_page_pixels.is_none()
124            && self.max_total_pixels.is_none()
125            && self.max_decoded_bytes.is_none()
126            && self.max_render_pixels.is_none()
127    }
128
129    /// Document-level inherited render ceiling from the public API contract.
130    pub const fn inherited() -> Self {
131        Self {
132            max_render_pixels: Some(DEFAULT_MAX_RENDER_PIXELS),
133            max_file_bytes: None,
134            max_pages: None,
135            max_components: None,
136            max_page_pixels: None,
137            max_total_pixels: None,
138            max_decoded_bytes: None,
139        }
140    }
141}
142
143/// Options controlling document parse behaviour and resource limits.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub struct ParseOptions {
146    /// Configurable resource limits checked before the document is fully parsed.
147    pub limits: Option<ResourceLimits>,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn exceeded(axis: ResourceLimitAxis) -> ResourceLimitExceeded {
155        ResourceLimitExceeded {
156            operation: "document.parse",
157            axis,
158            found: 100,
159            limit: 10,
160            page_number: Some(3),
161            width: Some(20),
162            height: Some(5),
163        }
164    }
165
166    /// Every axis must render a distinct, human-readable message that names
167    /// the operation and both the observed and configured values — this is
168    /// what a caller (CLI error output, API consumer) actually sees.
169    #[test]
170    fn display_file_bytes() {
171        let msg = exceeded(ResourceLimitAxis::FileBytes).to_string();
172        assert_eq!(msg, "document.parse: file is 100 bytes, exceeding limit 10");
173    }
174
175    #[test]
176    fn display_page_count() {
177        let msg = exceeded(ResourceLimitAxis::PageCount).to_string();
178        assert_eq!(
179            msg,
180            "document.parse: document has 100 pages, exceeding limit 10"
181        );
182    }
183
184    #[test]
185    fn display_component_count() {
186        let msg = exceeded(ResourceLimitAxis::ComponentCount).to_string();
187        assert_eq!(
188            msg,
189            "document.parse: document has 100 components, exceeding limit 10"
190        );
191    }
192
193    #[test]
194    fn display_page_pixels_includes_page_number_and_dimensions() {
195        let msg = exceeded(ResourceLimitAxis::PagePixels).to_string();
196        assert_eq!(
197            msg,
198            "document.parse: page 3 is 20x5 = 100 pixels, exceeding limit 10"
199        );
200    }
201
202    #[test]
203    fn display_total_pixels() {
204        let msg = exceeded(ResourceLimitAxis::TotalPixels).to_string();
205        assert_eq!(
206            msg,
207            "document.parse: document totals 100 pixels, exceeding limit 10"
208        );
209    }
210
211    #[test]
212    fn display_decoded_bytes() {
213        let msg = exceeded(ResourceLimitAxis::DecodedBytes).to_string();
214        assert_eq!(
215            msg,
216            "document.parse: peak decoded page memory is an estimated 100 bytes, exceeding limit 10"
217        );
218    }
219
220    #[test]
221    fn display_render_output_pixels_includes_dimensions() {
222        let msg = exceeded(ResourceLimitAxis::RenderOutputPixels).to_string();
223        assert_eq!(
224            msg,
225            "document.parse: render output 20x5 = 100 pixels exceeds limit 10"
226        );
227    }
228
229    /// `Display` must fall back to `0` rather than panic when the optional
230    /// page-number/width/height fields are unset (every non-`PagePixels`,
231    /// non-`RenderOutputPixels` axis constructs the error this way).
232    #[test]
233    fn display_page_pixels_defaults_missing_fields_to_zero() {
234        let err = ResourceLimitExceeded {
235            operation: "op",
236            axis: ResourceLimitAxis::PagePixels,
237            found: 5,
238            limit: 1,
239            page_number: None,
240            width: None,
241            height: None,
242        };
243        assert_eq!(
244            err.to_string(),
245            "op: page 0 is 0x0 = 5 pixels, exceeding limit 1"
246        );
247    }
248
249    #[test]
250    fn is_empty_true_for_default() {
251        assert!(ResourceLimits::default().is_empty());
252    }
253
254    #[test]
255    fn is_empty_false_when_any_field_set() {
256        let limits = ResourceLimits {
257            max_pages: Some(5),
258            ..ResourceLimits::default()
259        };
260        assert!(!limits.is_empty());
261    }
262
263    #[test]
264    fn inherited_sets_only_render_pixel_ceiling() {
265        let inherited = ResourceLimits::inherited();
266        assert_eq!(inherited.max_render_pixels, Some(DEFAULT_MAX_RENDER_PIXELS));
267        assert!(inherited.max_file_bytes.is_none());
268        assert!(inherited.max_pages.is_none());
269        assert!(inherited.max_components.is_none());
270        assert!(inherited.max_page_pixels.is_none());
271        assert!(inherited.max_total_pixels.is_none());
272        assert!(inherited.max_decoded_bytes.is_none());
273    }
274}