Skip to main content

excalidraw_render/
resource_limits.rs

1use crate::{ErrorCode, RequestError};
2
3/// The accepted domain of one limit kind, for its setter's documentation.
4macro_rules! validation_domain {
5    (integer) => {
6        "at least `1`"
7    };
8    (finite) => {
9        "finite, positive"
10    };
11}
12
13macro_rules! define_resource_limits {
14    ($(
15        #[doc = $doc:literal]
16        $field:ident: $type:ty = $default:literal, $maximum:literal, $validation:ident, $unit:literal;
17    )+) => {
18        /// Validated semantic resource ceilings for one render.
19        ///
20        /// Every limit is a deterministic, logical ceiling evaluated by the
21        /// renderer's own accounting, never a measurement of the process: the
22        /// same scene and request cross the same limit on every target.
23        /// Equality is accepted. Start from [`ResourceLimits::default`] (or
24        /// [`ResourceLimits::builder`]) and lower the limits that matter for
25        /// your deployment; no limit can be raised above
26        /// [`ResourceLimits::absolute_maxima`].
27        ///
28        /// `max_working_bytes` budgets the renderer's canonical logical working
29        /// set, not allocator usage: actual peak memory is higher, notably for
30        /// PNG, whose raster surfaces are bounded by `max_raster_dimension` and
31        /// `max_raster_surface_pixels`. See the crate documentation's section
32        /// on serving untrusted input.
33        #[derive(Clone, Debug, PartialEq)]
34        pub struct ResourceLimits {
35            $($field: $type,)+
36        }
37
38        const DEFAULT_LIMITS: ResourceLimits = ResourceLimits {
39            $($field: $default,)+
40        };
41
42        const ABSOLUTE_MAXIMA: ResourceLimits = ResourceLimits {
43            $($field: $maximum,)+
44        };
45
46        impl ResourceLimits {
47            /// Starts a builder from the revision-1 default profile.
48            pub fn builder() -> ResourceLimitsBuilder {
49                ResourceLimitsBuilder::default()
50            }
51
52            /// Returns the immutable absolute-maximum profile.
53            pub const fn absolute_maxima() -> &'static Self {
54                &ABSOLUTE_MAXIMA
55            }
56
57            $(
58                #[doc = concat!(
59                    "Returns the configured `", stringify!($field), "` ceiling: ", $doc,
60                    " Measured in ", $unit, "."
61                )]
62                pub const fn $field(&self) -> $type {
63                    self.$field
64                }
65            )+
66        }
67
68        impl Default for ResourceLimits {
69            fn default() -> Self {
70                DEFAULT_LIMITS.clone()
71            }
72        }
73
74        /// Builder for validated semantic resource ceilings.
75        #[must_use]
76        #[derive(Clone, Debug, Default)]
77        pub struct ResourceLimitsBuilder {
78            limits: ResourceLimits,
79        }
80
81        impl ResourceLimitsBuilder {
82            $(
83                #[doc = concat!(
84                    "Sets the `", stringify!($field), "` ceiling: ", $doc,
85                    "\n\nMeasured in ", $unit, ". Default `", stringify!($default),
86                    "`; accepted values are ",
87                    validation_domain!($validation), " and at most `", stringify!($maximum),
88                    "`, checked by [`build`](Self::build)."
89                )]
90                pub fn $field(mut self, value: $type) -> Self {
91                    self.limits.$field = value;
92                    self
93                }
94            )+
95
96            fn validate_individual_limits(&self) -> Result<(), RequestError> {
97                macro_rules! validate_limit {
98                    (integer, $name:ident) => {
99                        if self.limits.$name == 0
100                            || self.limits.$name > ABSOLUTE_MAXIMA.$name
101                        {
102                            return Err(invalid_limit(
103                                stringify!($name),
104                                format!(
105                                    "must be between 1 and {}, got {}",
106                                    ABSOLUTE_MAXIMA.$name,
107                                    self.limits.$name,
108                                ),
109                            ));
110                        }
111                    };
112                    (finite, $name:ident) => {
113                        if !self.limits.$name.is_finite()
114                            || self.limits.$name <= 0.0
115                            || self.limits.$name > ABSOLUTE_MAXIMA.$name
116                        {
117                            return Err(invalid_limit(
118                                stringify!($name),
119                                format!(
120                                    "must be finite, positive and at most {}, got {}",
121                                    ABSOLUTE_MAXIMA.$name,
122                                    self.limits.$name,
123                                ),
124                            ));
125                        }
126                    };
127                }
128
129                $(validate_limit!($validation, $field);)+
130                Ok(())
131            }
132        }
133    };
134}
135
136define_resource_limits! {
137    /// Bytes of the admitted scene document.
138    max_scene_bytes: u64 = 33_554_432, 67_108_864, integer, "bytes";
139    /// Source element occurrences: every member of the scene's `elements` array, live or deleted.
140    max_elements: u32 = 16_384, 65_536, integer, "element occurrences";
141    /// Asset entries: members of the scene's `files` map.
142    max_asset_entries: u32 = 1_024, 4_096, integer, "asset entries";
143    /// Bytes of any one asset payload.
144    max_asset_bytes: u64 = 33_554_432, 67_108_864, integer, "bytes";
145    /// Bytes of all asset payloads together; at least `max_asset_bytes`.
146    max_total_asset_bytes: u64 = 134_217_728, 268_435_456, integer, "bytes";
147    /// Depth of element relationship traversal, such as nested groups and frames.
148    max_relationship_depth: u32 = 64, 128, integer, "levels";
149    /// Depth of assets nested inside SVG image assets.
150    max_nested_asset_depth: u32 = 4, 8, integer, "levels";
151    /// Nodes in any one nested SVG image.
152    max_nested_svg_nodes: u32 = 65_536, 262_144, integer, "nodes";
153    /// Geometry segments generated for any one element.
154    max_geometry_segments_per_element: u32 = 262_144, 1_048_576, integer, "segments";
155    /// Geometry segments generated for the whole render; at least `max_geometry_segments_per_element`.
156    max_total_geometry_segments: u64 = 4_194_304, 16_777_216, integer, "segments";
157    /// Unicode scalars of text in any one element.
158    max_text_scalars_per_element: u32 = 262_144, 1_048_576, integer, "Unicode scalar values";
159    /// Unicode scalars of text in the whole render; at least `max_text_scalars_per_element`.
160    max_total_text_scalars: u64 = 4_194_304, 16_777_216, integer, "Unicode scalar values";
161    /// Width or height of any decoded image or required raster surface.
162    max_raster_dimension: u32 = 16_384, 32_768, integer, "pixels per side";
163    /// Decoded pixels of any one image asset.
164    max_decoded_pixels_per_asset: u64 = 16_777_216, 67_108_864, integer, "pixels";
165    /// Decoded pixels of all image assets together; at least `max_decoded_pixels_per_asset`.
166    max_total_decoded_pixels: u64 = 33_554_432, 134_217_728, integer, "pixels";
167    /// Area of any one required raster surface: the PNG canvas, or a PDF raster fallback.
168    max_raster_surface_pixels: u64 = 33_554_432, 134_217_728, integer, "pixels";
169    /// Area of all raster surfaces together, placeholders included; at least `max_raster_surface_pixels`.
170    max_total_rasterized_pixels: u64 = 67_108_864, 268_435_456, integer, "pixels";
171    /// Logical `viewBox` extent and final scaled width or height of an SVG.
172    max_svg_dimension: f64 = 1_000_000.0, 10_000_000.0, finite, "SVG user units";
173    /// PDF page width or height after padding and sizing.
174    max_pdf_page_points: f64 = 14_400.0, 14_400.0, finite, "PDF points";
175    /// Magnitude of any normalized coordinate, dimension or transform value.
176    max_coordinate_magnitude: f64 = 1e9, 1e12, finite, "scene units";
177    /// Diagnostics in one compatibility report.
178    max_diagnostics: u32 = 65_536, 262_144, integer, "diagnostics";
179    /// Peak canonical logical working set. A deterministic budget, not an allocator cap: actual memory use is higher.
180    max_working_bytes: u64 = 268_435_456, 536_870_912, integer, "logical bytes";
181    /// Bytes of the complete artifact.
182    max_output_bytes: u64 = 134_217_728, 536_870_912, integer, "bytes";
183    /// Deterministic work spent on any one element.
184    max_element_work_units: u64 = 67_108_864, 268_435_456, integer, "work units";
185    /// Deterministic work spent on any one asset.
186    max_asset_work_units: u64 = 134_217_728, 536_870_912, integer, "work units";
187    /// Deterministic work spent on the whole render; at least `max_element_work_units` and `max_asset_work_units`.
188    max_total_work_units: u64 = 4_294_967_296, 17_179_869_184, integer, "work units";
189}
190
191impl ResourceLimitsBuilder {
192    /// Validates and constructs the immutable resource-limit profile.
193    ///
194    /// # Errors
195    ///
196    /// Returns a [`RequestError`] with code
197    /// `request.invalid-resource-limit`, naming the field, when a limit is
198    /// zero, not finite, or above its [absolute
199    /// maximum](ResourceLimits::absolute_maxima), or when a per-item limit
200    /// exceeds the aggregate it contributes to (for example
201    /// `max_asset_bytes` above `max_total_asset_bytes`).
202    pub fn build(self) -> Result<ResourceLimits, RequestError> {
203        self.validate_individual_limits()?;
204
205        // Each per-item ceiling must fit inside the aggregate it contributes to.
206        let limits = &self.limits;
207        for (field, value, total_field, total) in [
208            (
209                "max_asset_bytes",
210                limits.max_asset_bytes,
211                "max_total_asset_bytes",
212                limits.max_total_asset_bytes,
213            ),
214            (
215                "max_geometry_segments_per_element",
216                u64::from(limits.max_geometry_segments_per_element),
217                "max_total_geometry_segments",
218                limits.max_total_geometry_segments,
219            ),
220            (
221                "max_text_scalars_per_element",
222                u64::from(limits.max_text_scalars_per_element),
223                "max_total_text_scalars",
224                limits.max_total_text_scalars,
225            ),
226            (
227                "max_decoded_pixels_per_asset",
228                limits.max_decoded_pixels_per_asset,
229                "max_total_decoded_pixels",
230                limits.max_total_decoded_pixels,
231            ),
232            (
233                "max_raster_surface_pixels",
234                limits.max_raster_surface_pixels,
235                "max_total_rasterized_pixels",
236                limits.max_total_rasterized_pixels,
237            ),
238            (
239                "max_element_work_units",
240                limits.max_element_work_units,
241                "max_total_work_units",
242                limits.max_total_work_units,
243            ),
244            (
245                "max_asset_work_units",
246                limits.max_asset_work_units,
247                "max_total_work_units",
248                limits.max_total_work_units,
249            ),
250        ] {
251            if value > total {
252                return Err(invalid_limit(
253                    field,
254                    format!("({value}) must not exceed {total_field} ({total})"),
255                ));
256            }
257        }
258
259        Ok(self.limits)
260    }
261}
262
263/// A `request.invalid-resource-limit` failure naming the rejected field and
264/// why: `reason` follows the field name in the message.
265fn invalid_limit(field: &'static str, reason: String) -> RequestError {
266    RequestError::new(
267        ErrorCode::REQUEST_INVALID_RESOURCE_LIMIT,
268        format!("resource limit {field} {reason}"),
269        field,
270    )
271}