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}