1#[cfg(any(
2 feature = "diagram-flowchart",
3 feature = "diagram-swimlane",
4 feature = "diagram-agentflow"
5))]
6use merman_core::diagrams::flowchart::FlowchartModel;
7#[cfg(feature = "diagram-mindmap")]
8use merman_core::diagrams::mindmap::MindmapDiagramRenderModel;
9#[cfg(feature = "diagram-zenuml")]
10use merman_core::diagrams::zenuml::ZenumlDiagramRenderModel;
11#[cfg(feature = "diagram-class")]
12use merman_core::models::class_diagram::ClassDiagram;
13use merman_core::resources::{
14 InputResourceLimitExceeded, InputResourceLimitId, InputResourceLimitPhase, InputResourcePolicy,
15};
16pub use merman_core::resources::{
17 ModelComplexity, RESOURCE_PROFILE_DESCRIPTORS, ResourceProfile as RenderResourceProfile,
18 ResourceProfileDescriptor as RenderResourceProfileDescriptor,
19};
20use merman_core::{
21 OperationCancelled, OperationControl, OperationLedgerError, OperationPhase,
22 OperationResourceDomain, OperationResourceLimitExceeded, OperationResourceOverride,
23 OperationResourceProvenance, ParsedDiagramRender, RenderSemanticModel,
24};
25
26const KIB: usize = 1024;
27const MIB: usize = 1024 * KIB;
28
29pub const WASM_RESVG_TREE_DEPTH_HARD_CAP: usize = 64;
34pub const MAX_PORTABLE_ICON_BODY_XML_DEPTH: usize = 32;
39const _: () = assert!(MAX_PORTABLE_ICON_BODY_XML_DEPTH < WASM_RESVG_TREE_DEPTH_HARD_CAP);
40
41#[cfg(not(target_arch = "wasm32"))]
42pub const MAX_RESVG_TREE_DEPTH: usize = 256;
43pub const SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID: &str = "svg_backend_tree_depth";
44pub const MAX_RESVG_TREE_NODES: usize = 1_000_000;
49pub const SVG_BACKEND_TREE_NODES_HARD_CAP_ID: &str = "svg_backend_tree_nodes";
50
51#[cfg(target_arch = "wasm32")]
52pub const MAX_RESVG_TREE_DEPTH: usize = WASM_RESVG_TREE_DEPTH_HARD_CAP;
53
54#[cfg(not(target_arch = "wasm32"))]
57const MAX_RECURSIVE_MODEL_TREE_DEPTH: usize = merman_core::MAX_DIAGRAM_NESTING_DEPTH;
58
59#[cfg(target_arch = "wasm32")]
60const MAX_RECURSIVE_MODEL_TREE_DEPTH: usize = 64;
61
62pub const RESOURCE_PROFILE_COUNT: usize = merman_core::resources::RESOURCE_PROFILE_COUNT;
63const RENDER_RESOURCE_LIMIT_COUNT: usize = 5;
64pub const RESOURCE_LIMIT_COUNT: usize =
65 merman_core::resources::INPUT_RESOURCE_LIMIT_COUNT + RENDER_RESOURCE_LIMIT_COUNT;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum ResourceLimitPhase {
69 Source,
70 LayoutModel,
71 SvgOutput,
72 SvgPostprocess,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum ResourceLimitCause {
79 Ceiling,
81 ArithmeticOverflow,
83}
84
85impl ResourceLimitCause {
86 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::Ceiling => "ceiling",
89 Self::ArithmeticOverflow => "arithmetic_overflow",
90 }
91 }
92}
93
94impl std::fmt::Display for ResourceLimitCause {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl ResourceLimitPhase {
101 const ALL: [Self; 4] = [
102 Self::Source,
103 Self::LayoutModel,
104 Self::SvgOutput,
105 Self::SvgPostprocess,
106 ];
107
108 pub const fn as_str(self) -> &'static str {
109 match self {
110 Self::Source => "source",
111 Self::LayoutModel => "layout_model",
112 Self::SvgOutput => "svg_output",
113 Self::SvgPostprocess => "svg_postprocess",
114 }
115 }
116
117 fn from_stable_id(value: &str) -> Option<Self> {
118 Self::ALL.into_iter().find(|phase| phase.as_str() == value)
119 }
120}
121
122impl std::fmt::Display for ResourceLimitPhase {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.write_str(self.as_str())
125 }
126}
127
128#[repr(usize)]
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum RenderResourceLimitId {
131 MaxSvgBytes,
132 MaxSvgElements,
133 MaxLayoutWorkUnits,
134 SvgBackendTreeNodes,
135 SvgBackendTreeDepth,
136}
137
138impl RenderResourceLimitId {
139 pub const ALL: [Self; RENDER_RESOURCE_LIMIT_COUNT] = [
140 Self::MaxSvgBytes,
141 Self::MaxSvgElements,
142 Self::MaxLayoutWorkUnits,
143 Self::SvgBackendTreeNodes,
144 Self::SvgBackendTreeDepth,
145 ];
146
147 const fn index(self) -> usize {
148 self as usize
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum ResourceLimitId {
154 Input(InputResourceLimitId),
155 Render(RenderResourceLimitId),
156}
157
158#[allow(non_upper_case_globals)]
159impl ResourceLimitId {
160 pub const MaxSourceBytes: Self = Self::Input(InputResourceLimitId::MaxSourceBytes);
161 pub const MaxModelItems: Self = Self::Input(InputResourceLimitId::MaxModelItems);
162 pub const MaxModelTextBytes: Self = Self::Input(InputResourceLimitId::MaxModelTextBytes);
163 pub const MaxModelNestingDepth: Self = Self::Input(InputResourceLimitId::MaxModelNestingDepth);
164 pub const MaxSvgBytes: Self = Self::Render(RenderResourceLimitId::MaxSvgBytes);
165 pub const MaxSvgElements: Self = Self::Render(RenderResourceLimitId::MaxSvgElements);
166 pub const MaxLayoutWorkUnits: Self = Self::Render(RenderResourceLimitId::MaxLayoutWorkUnits);
167 pub const SvgBackendTreeNodes: Self = Self::Render(RenderResourceLimitId::SvgBackendTreeNodes);
168 pub const SvgBackendTreeDepth: Self = Self::Render(RenderResourceLimitId::SvgBackendTreeDepth);
169
170 pub const ALL: [Self; RESOURCE_LIMIT_COUNT] = [
171 Self::MaxSourceBytes,
172 Self::MaxModelItems,
173 Self::MaxModelTextBytes,
174 Self::MaxModelNestingDepth,
175 Self::MaxLayoutWorkUnits,
176 Self::MaxSvgBytes,
177 Self::MaxSvgElements,
178 Self::SvgBackendTreeNodes,
179 Self::SvgBackendTreeDepth,
180 ];
181
182 pub fn from_stable_id(id: &str) -> Option<Self> {
183 InputResourceLimitId::from_stable_id(id)
184 .map(Self::Input)
185 .or_else(|| {
186 RENDER_RESOURCE_LIMIT_DESCRIPTORS
187 .iter()
188 .find(|descriptor| descriptor.stable_id == id)
189 .map(|descriptor| descriptor.id)
190 })
191 }
192
193 pub const fn descriptor(self) -> ResourceLimitDescriptor {
194 match self {
195 Self::Input(id) => input_descriptor(id),
196 Self::Render(id) => RENDER_RESOURCE_LIMIT_DESCRIPTORS[id.index()],
197 }
198 }
199
200 pub const fn as_str(self) -> &'static str {
201 self.descriptor().stable_id
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206#[non_exhaustive]
207pub struct ResourceLimitDescriptor {
208 pub id: ResourceLimitId,
209 pub stable_id: &'static str,
210 pub phase: ResourceLimitPhase,
211 pub description: &'static str,
212 pub overridable: bool,
213 pub hard_cap: bool,
214 pub minimum_value: usize,
215}
216
217const fn input_descriptor(id: InputResourceLimitId) -> ResourceLimitDescriptor {
218 let descriptor = id.descriptor();
219 ResourceLimitDescriptor {
220 id: ResourceLimitId::Input(id),
221 stable_id: descriptor.stable_id,
222 phase: match descriptor.phase {
223 InputResourceLimitPhase::Source => ResourceLimitPhase::Source,
224 InputResourceLimitPhase::Model => ResourceLimitPhase::LayoutModel,
225 },
226 description: descriptor.description,
227 overridable: descriptor.overridable,
228 hard_cap: false,
229 minimum_value: descriptor.minimum_value,
230 }
231}
232
233const RENDER_RESOURCE_LIMIT_DESCRIPTORS: [ResourceLimitDescriptor; RENDER_RESOURCE_LIMIT_COUNT] = [
234 ResourceLimitDescriptor {
235 id: ResourceLimitId::MaxSvgBytes,
236 stable_id: "max_svg_bytes",
237 phase: ResourceLimitPhase::SvgOutput,
238 description: "Maximum serialized SVG bytes",
239 overridable: true,
240 hard_cap: false,
241 minimum_value: 1,
242 },
243 ResourceLimitDescriptor {
244 id: ResourceLimitId::MaxSvgElements,
245 stable_id: "max_svg_elements",
246 phase: ResourceLimitPhase::SvgPostprocess,
247 description: "Maximum SVG element count",
248 overridable: true,
249 hard_cap: false,
250 minimum_value: 1,
251 },
252 ResourceLimitDescriptor {
253 id: ResourceLimitId::MaxLayoutWorkUnits,
254 stable_id: "max_layout_work_units",
255 phase: ResourceLimitPhase::LayoutModel,
256 description: "Maximum family-accounted derived layout and render geometry work units",
257 overridable: true,
258 hard_cap: false,
259 minimum_value: 1,
260 },
261 ResourceLimitDescriptor {
262 id: ResourceLimitId::SvgBackendTreeNodes,
263 stable_id: SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
264 phase: ResourceLimitPhase::SvgPostprocess,
265 description: "Maximum SVG backend tree nodes accepted by rendering backends",
266 overridable: false,
267 hard_cap: true,
268 minimum_value: 1,
269 },
270 ResourceLimitDescriptor {
271 id: ResourceLimitId::SvgBackendTreeDepth,
272 stable_id: SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
273 phase: ResourceLimitPhase::SvgPostprocess,
274 description: "Maximum SVG backend tree depth accepted by rendering backends",
275 overridable: false,
276 hard_cap: true,
277 minimum_value: 1,
278 },
279];
280
281pub static RESOURCE_LIMIT_DESCRIPTORS: [ResourceLimitDescriptor; RESOURCE_LIMIT_COUNT] = [
282 input_descriptor(InputResourceLimitId::MaxSourceBytes),
283 input_descriptor(InputResourceLimitId::MaxModelItems),
284 input_descriptor(InputResourceLimitId::MaxModelTextBytes),
285 input_descriptor(InputResourceLimitId::MaxModelNestingDepth),
286 RENDER_RESOURCE_LIMIT_DESCRIPTORS[2],
287 RENDER_RESOURCE_LIMIT_DESCRIPTORS[0],
288 RENDER_RESOURCE_LIMIT_DESCRIPTORS[1],
289 RENDER_RESOURCE_LIMIT_DESCRIPTORS[3],
290 RENDER_RESOURCE_LIMIT_DESCRIPTORS[4],
291];
292
293const RENDER_PROFILE_VALUES: [[Option<usize>; RESOURCE_PROFILE_COUNT];
294 RENDER_RESOURCE_LIMIT_COUNT] = [
295 [Some(24 * MIB), Some(12 * MIB), Some(128 * MIB), None],
296 [Some(250_000), Some(125_000), Some(1_000_000), None],
297 [Some(14_100_000), Some(125_000), Some(15_000_000), None],
303 [Some(MAX_RESVG_TREE_NODES); RESOURCE_PROFILE_COUNT],
304 [Some(MAX_RESVG_TREE_DEPTH); RESOURCE_PROFILE_COUNT],
305];
306
307pub const GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE: RenderResourceProfile =
308 merman_core::resources::GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE;
309pub const CLI_DEFAULT_RESOURCE_PROFILE: RenderResourceProfile =
310 merman_core::resources::CLI_DEFAULT_RESOURCE_PROFILE;
311
312pub const fn resource_profile_descriptors() -> &'static [RenderResourceProfileDescriptor] {
313 &merman_core::resources::RESOURCE_PROFILE_DESCRIPTORS
314}
315
316pub const fn resource_limit_descriptors() -> &'static [ResourceLimitDescriptor] {
317 &RESOURCE_LIMIT_DESCRIPTORS
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
321pub enum ResourceLimitOverrideError {
322 #[error("resource limit id `{0}` is not part of resource contract schema 1")]
323 UnknownLimit(String),
324 #[error("resource limit `{0}` is a hard implementation capability and cannot be overridden")]
325 HardCap(&'static str),
326 #[error("resource limit `{0}` must be a positive integer")]
327 NonPositive(&'static str),
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct RenderResourcePolicy {
332 input: InputResourcePolicy,
333 render_base_values: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
334 render_effective_values: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
335 render_explicit_overrides: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
336}
337
338impl Default for RenderResourcePolicy {
339 fn default() -> Self {
340 Self::interactive()
341 }
342}
343
344impl RenderResourcePolicy {
345 pub const fn profile(self) -> RenderResourceProfile {
346 self.input.profile()
347 }
348
349 pub const fn interactive() -> Self {
350 Self::for_profile(RenderResourceProfile::Interactive)
351 }
352
353 pub const fn constrained() -> Self {
354 Self::for_profile(RenderResourceProfile::Constrained)
355 }
356
357 pub const fn trusted_native() -> Self {
358 Self::for_profile(RenderResourceProfile::TrustedNative)
359 }
360
361 pub const fn unbounded_for_trusted_input() -> Self {
362 Self::for_profile(RenderResourceProfile::UnboundedForTrustedInput)
363 }
364
365 pub const fn for_profile(profile: RenderResourceProfile) -> Self {
366 let mut render_values = [None; RENDER_RESOURCE_LIMIT_COUNT];
367 let mut index = 0;
368 while index < RENDER_RESOURCE_LIMIT_COUNT {
369 render_values[index] = RENDER_PROFILE_VALUES[index][profile as usize];
370 index += 1;
371 }
372 Self {
373 input: InputResourcePolicy::for_profile(profile),
374 render_base_values: render_values,
375 render_effective_values: render_values,
376 render_explicit_overrides: [None; RENDER_RESOURCE_LIMIT_COUNT],
377 }
378 }
379
380 pub const fn input_policy(&self) -> &InputResourcePolicy {
381 &self.input
382 }
383
384 pub const fn value(self, id: ResourceLimitId) -> Option<usize> {
385 match id {
386 ResourceLimitId::Input(id) => self.input.value(id),
387 ResourceLimitId::Render(id) => self.render_effective_values[id.index()],
388 }
389 }
390
391 pub const fn base_value(self, id: ResourceLimitId) -> Option<usize> {
392 match id {
393 ResourceLimitId::Input(id) => self.input.base_value(id),
394 ResourceLimitId::Render(id) => self.render_base_values[id.index()],
395 }
396 }
397
398 pub const fn explicit_override(self, id: ResourceLimitId) -> Option<usize> {
399 match id {
400 ResourceLimitId::Input(id) => self.input.explicit_override(id),
401 ResourceLimitId::Render(id) => self.render_explicit_overrides[id.index()],
402 }
403 }
404
405 pub fn explicit_overrides(&self) -> impl Iterator<Item = (ResourceLimitId, usize)> + '_ {
406 ResourceLimitId::ALL
407 .into_iter()
408 .filter_map(|id| self.explicit_override(id).map(|value| (id, value)))
409 }
410
411 pub fn apply_override(
412 &mut self,
413 stable_id: &str,
414 value: usize,
415 ) -> Result<(), ResourceLimitOverrideError> {
416 let id = ResourceLimitId::from_stable_id(stable_id)
417 .ok_or_else(|| ResourceLimitOverrideError::UnknownLimit(stable_id.to_string()))?;
418 self.apply_limit(id, value)
419 }
420
421 pub fn apply_limit(
422 &mut self,
423 id: ResourceLimitId,
424 value: usize,
425 ) -> Result<(), ResourceLimitOverrideError> {
426 match id {
427 ResourceLimitId::Input(id) => {
428 self.input
429 .apply_limit(id, value)
430 .map_err(|error| match error {
431 merman_core::resources::InputResourceLimitOverrideError::UnknownLimit(
432 id,
433 ) => ResourceLimitOverrideError::UnknownLimit(id),
434 merman_core::resources::InputResourceLimitOverrideError::NonPositive(
435 id,
436 ) => ResourceLimitOverrideError::NonPositive(id),
437 })
438 }
439 ResourceLimitId::Render(id) => {
440 let descriptor = RENDER_RESOURCE_LIMIT_DESCRIPTORS[id.index()];
441 if descriptor.hard_cap || !descriptor.overridable {
442 return Err(ResourceLimitOverrideError::HardCap(descriptor.stable_id));
443 }
444 if value == 0 {
445 return Err(ResourceLimitOverrideError::NonPositive(
446 descriptor.stable_id,
447 ));
448 }
449 self.render_effective_values[id.index()] = Some(value);
450 self.render_explicit_overrides[id.index()] = Some(value);
451 Ok(())
452 }
453 }
454 }
455
456 pub fn with_override(
457 mut self,
458 stable_id: &str,
459 value: usize,
460 ) -> Result<Self, ResourceLimitOverrideError> {
461 self.apply_override(stable_id, value)?;
462 Ok(self)
463 }
464
465 pub fn with_limit(
466 mut self,
467 id: ResourceLimitId,
468 value: usize,
469 ) -> Result<Self, ResourceLimitOverrideError> {
470 self.apply_limit(id, value)?;
471 Ok(self)
472 }
473
474 fn check_render_limit(
475 &self,
476 phase: ResourceLimitPhase,
477 id: RenderResourceLimitId,
478 actual: usize,
479 ) -> Result<(), ResourceLimitExceeded> {
480 let Some(max) = self.render_effective_values[id.index()] else {
481 return Ok(());
482 };
483 if actual <= max {
484 return Ok(());
485 }
486 let limit = ResourceLimitId::Render(id);
487 Err(ResourceLimitExceeded {
488 cause: ResourceLimitCause::Ceiling,
489 phase,
490 limit: limit.as_str(),
491 actual,
492 max,
493 profile: self.profile(),
494 explicit_overrides: self
495 .explicit_overrides()
496 .map(|(id, value)| ResourceLimitOverride { id, value })
497 .collect(),
498 })
499 }
500
501 pub fn check_source_bytes(&self, source: &str) -> Result<(), ResourceLimitExceeded> {
502 self.input
503 .check_source_bytes(source)
504 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
505 }
506
507 pub fn check_render_model(
508 &self,
509 model: &RenderSemanticModel,
510 ) -> Result<(), ResourceLimitExceeded> {
511 let complexity = ModelComplexity::from_render_model(model);
512 self.check_render_model_complexity(model, complexity)
513 }
514
515 pub fn check_parsed_render(
516 &self,
517 parsed: &ParsedDiagramRender,
518 ) -> Result<(), ResourceLimitExceeded> {
519 let mut complexity = ModelComplexity::from_render_model(parsed.model());
520 complexity.text_bytes = complexity
521 .text_bytes
522 .saturating_add(parsed.retained_render_context_bytes());
523 self.check_render_model_complexity(parsed.model(), complexity)
524 }
525
526 fn check_render_model_complexity(
527 &self,
528 model: &RenderSemanticModel,
529 complexity: ModelComplexity,
530 ) -> Result<(), ResourceLimitExceeded> {
531 self.check_model_complexity(complexity)?;
532
533 let recursive_tree = match model {
534 #[cfg(feature = "diagram-treemap")]
535 RenderSemanticModel::Treemap(_) => true,
536 #[cfg(feature = "diagram-ishikawa")]
537 RenderSemanticModel::Ishikawa(_) => true,
538 _ => false,
539 };
540 if recursive_tree && complexity.nesting_depth > MAX_RECURSIVE_MODEL_TREE_DEPTH {
541 return Err(ResourceLimitExceeded {
542 cause: ResourceLimitCause::Ceiling,
543 phase: ResourceLimitPhase::LayoutModel,
544 limit: "typed_model_tree_depth",
545 actual: complexity.nesting_depth,
546 max: MAX_RECURSIVE_MODEL_TREE_DEPTH,
547 profile: self.profile(),
548 explicit_overrides: self
549 .explicit_overrides()
550 .map(|(id, value)| ResourceLimitOverride { id, value })
551 .collect(),
552 });
553 }
554
555 Ok(())
556 }
557
558 pub fn check_model_complexity(
559 &self,
560 complexity: ModelComplexity,
561 ) -> Result<(), ResourceLimitExceeded> {
562 self.input
563 .check_model_complexity(complexity)
564 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
565 }
566
567 pub fn check_svg_bytes(
568 &self,
569 svg: &str,
570 phase: ResourceLimitPhase,
571 ) -> Result<(), ResourceLimitExceeded> {
572 self.check_svg_byte_count(svg.len(), phase)
573 }
574
575 pub(crate) fn check_svg_byte_count(
576 &self,
577 bytes: usize,
578 phase: ResourceLimitPhase,
579 ) -> Result<(), ResourceLimitExceeded> {
580 self.check_render_limit(phase, RenderResourceLimitId::MaxSvgBytes, bytes)
581 }
582
583 pub fn check_svg_structure(
584 &self,
585 elements: usize,
586 tree_depth: usize,
587 ) -> Result<(), ResourceLimitExceeded> {
588 self.check_render_limit(
589 ResourceLimitPhase::SvgPostprocess,
590 RenderResourceLimitId::MaxSvgElements,
591 elements,
592 )?;
593 self.check_render_limit(
594 ResourceLimitPhase::SvgPostprocess,
595 RenderResourceLimitId::SvgBackendTreeNodes,
596 elements,
597 )?;
598 self.check_render_limit(
599 ResourceLimitPhase::SvgPostprocess,
600 RenderResourceLimitId::SvgBackendTreeDepth,
601 tree_depth,
602 )
603 }
604
605 #[cfg(any(
606 feature = "diagram-flowchart",
607 feature = "diagram-swimlane",
608 feature = "diagram-agentflow"
609 ))]
610 pub fn check_flowchart_complexity(
611 &self,
612 model: &FlowchartModel,
613 ) -> Result<FlowchartComplexity, ResourceLimitExceeded> {
614 self.input
615 .check_flowchart_complexity(model)
616 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
617 }
618
619 #[cfg(feature = "diagram-class")]
620 pub fn check_class_complexity(
621 &self,
622 model: &ClassDiagram,
623 ) -> Result<ClassComplexity, ResourceLimitExceeded> {
624 self.input
625 .check_class_complexity(model)
626 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
627 }
628
629 #[cfg(feature = "diagram-mindmap")]
630 pub fn check_mindmap_complexity(
631 &self,
632 model: &MindmapDiagramRenderModel,
633 ) -> Result<MindmapComplexity, ResourceLimitExceeded> {
634 self.input
635 .check_mindmap_complexity(model)
636 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
637 }
638
639 #[cfg(feature = "diagram-zenuml")]
640 pub fn check_zenuml_complexity(
641 &self,
642 model: &ZenumlDiagramRenderModel,
643 ) -> Result<ZenumlComplexity, ResourceLimitExceeded> {
644 self.input
645 .check_zenuml_complexity(model)
646 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
647 }
648
649 #[cfg(feature = "diagram-sequence")]
650 pub fn check_sequence_complexity(
651 &self,
652 model: &merman_core::diagrams::sequence::SequenceDiagramRenderModel,
653 ) -> Result<SequenceComplexity, ResourceLimitExceeded> {
654 self.input
655 .check_sequence_complexity(model)
656 .map_err(|error| ResourceLimitExceeded::from_input(self, error))
657 }
658
659 pub fn check_layout_work_units(&self, work_units: usize) -> Result<(), ResourceLimitExceeded> {
660 self.check_render_limit(
661 ResourceLimitPhase::LayoutModel,
662 RenderResourceLimitId::MaxLayoutWorkUnits,
663 work_units,
664 )
665 }
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
675pub(crate) enum OperationWorkError {
676 #[error(transparent)]
677 Cancelled(#[from] OperationCancelled),
678 #[error(transparent)]
679 ResourceLimitExceeded(#[from] ResourceLimitExceeded),
680 #[error(transparent)]
681 ForeignResourceTerminal(OperationLedgerError),
682}
683
684#[cfg_attr(
685 not(feature = "all-diagrams"),
686 allow(
687 dead_code,
688 reason = "Shared operation support has different consumers in each diagram selection."
689 )
690)]
691pub(crate) struct OperationWorkMeter {
692 policy: RenderResourcePolicy,
693 control: OperationControl,
694 used: std::sync::atomic::AtomicUsize,
695 projected_svg_bytes: std::sync::atomic::AtomicUsize,
696}
697
698#[cfg_attr(
699 not(feature = "all-diagrams"),
700 allow(
701 dead_code,
702 reason = "Shared operation support has different consumers in each diagram selection."
703 )
704)]
705pub(crate) struct SvgByteReservation {
706 pub(crate) additional_bytes: usize,
707 pub(crate) limit_error: Option<ResourceLimitExceeded>,
708}
709
710#[cfg_attr(
711 not(feature = "all-diagrams"),
712 allow(
713 dead_code,
714 reason = "Shared operation support has different consumers in each diagram selection."
715 )
716)]
717impl OperationWorkMeter {
718 #[cfg_attr(not(feature = "layout-elk"), allow(dead_code))]
719 pub(crate) fn new(policy: RenderResourcePolicy) -> Self {
720 Self::new_with_control(policy, OperationControl::new())
721 }
722
723 pub(crate) fn new_with_control(
724 policy: RenderResourcePolicy,
725 control: OperationControl,
726 ) -> Self {
727 Self {
728 policy,
729 control,
730 used: std::sync::atomic::AtomicUsize::new(0),
731 projected_svg_bytes: std::sync::atomic::AtomicUsize::new(0),
732 }
733 }
734
735 pub(crate) const fn policy(&self) -> RenderResourcePolicy {
736 self.policy
737 }
738
739 pub(crate) fn checkpoint(&self, phase: OperationPhase) -> Result<(), OperationWorkError> {
741 self.control
742 .terminal_checkpoint_at(phase)
743 .map_err(|error| self.map_terminal_error(error))
744 }
745
746 fn resource_checkpoint(&self, phase: OperationPhase) -> Result<(), OperationWorkError> {
747 self.checkpoint(phase)
748 }
749
750 pub(crate) fn preflight(&self, additional: usize) -> Result<(), OperationWorkError> {
751 let phase = OperationPhase::Layout;
752 self.resource_checkpoint(phase)?;
753 if additional == 0 {
754 return Ok(());
755 }
756 let used = self.used.load(std::sync::atomic::Ordering::Relaxed);
757 let Some(next) = used.checked_add(additional) else {
758 return Err(self.terminate_resource_error(
759 self.layout_work_overflow_details(),
760 phase,
761 used,
762 additional,
763 ));
764 };
765 match self.policy.check_layout_work_units(next) {
766 Ok(()) => Ok(()),
767 Err(error) => Err(self.terminate_resource_error(error, phase, used, additional)),
768 }
769 }
770
771 pub(crate) fn charge(&self, additional: usize) -> Result<(), OperationWorkError> {
773 let phase = OperationPhase::Layout;
774 self.resource_checkpoint(phase)?;
775 if additional == 0 {
776 return Ok(());
777 }
778 let mut used = self.used.load(std::sync::atomic::Ordering::Relaxed);
779 loop {
780 let Some(next) = used.checked_add(additional) else {
781 return Err(self.terminate_resource_error(
782 self.layout_work_overflow_details(),
783 phase,
784 used,
785 additional,
786 ));
787 };
788 if let Err(error) = self.policy.check_layout_work_units(next) {
789 return Err(self.terminate_resource_error(error, phase, used, additional));
790 }
791 match self.used.compare_exchange_weak(
792 used,
793 next,
794 std::sync::atomic::Ordering::Relaxed,
795 std::sync::atomic::Ordering::Relaxed,
796 ) {
797 Ok(_) => return Ok(()),
798 Err(actual) => {
799 self.resource_checkpoint(phase)?;
800 used = actual;
801 }
802 }
803 }
804 }
805
806 fn layout_work_overflow_details(&self) -> ResourceLimitExceeded {
807 accumulation_overflow(
808 self.policy,
809 ResourceLimitPhase::LayoutModel,
810 RenderResourceLimitId::MaxLayoutWorkUnits,
811 )
812 }
813
814 pub(crate) fn arithmetic_overflow(&self) -> OperationWorkError {
815 self.terminate_resource_error(
816 self.layout_work_overflow_details(),
817 OperationPhase::Layout,
818 self.used(),
819 1,
820 )
821 }
822
823 pub(crate) fn charge_svg_bytes(&self, additional: usize) -> Result<(), OperationWorkError> {
829 let phase = OperationPhase::Emit;
830 self.resource_checkpoint(phase)?;
831 let mut used = self
832 .projected_svg_bytes
833 .load(std::sync::atomic::Ordering::Relaxed);
834 loop {
835 let Some(next) = used.checked_add(additional) else {
836 return Err(self.terminate_resource_error(
837 accumulation_overflow(
838 self.policy,
839 ResourceLimitPhase::SvgOutput,
840 RenderResourceLimitId::MaxSvgBytes,
841 ),
842 phase,
843 used,
844 additional,
845 ));
846 };
847 if let Err(error) = self
848 .policy
849 .check_svg_byte_count(next, ResourceLimitPhase::SvgOutput)
850 {
851 return Err(self.terminate_resource_error(error, phase, used, additional));
852 }
853 match self.projected_svg_bytes.compare_exchange_weak(
854 used,
855 next,
856 std::sync::atomic::Ordering::Relaxed,
857 std::sync::atomic::Ordering::Relaxed,
858 ) {
859 Ok(_) => return Ok(()),
860 Err(actual) => {
861 self.resource_checkpoint(phase)?;
862 used = actual;
863 }
864 }
865 }
866 }
867
868 pub(crate) fn preflight_svg_byte_count(
874 &self,
875 actual: usize,
876 resource_phase: ResourceLimitPhase,
877 operation_phase: OperationPhase,
878 ) -> Result<(), OperationWorkError> {
879 self.resource_checkpoint(operation_phase)?;
880 self.policy
881 .check_svg_byte_count(actual, resource_phase)
882 .map_err(|error| self.terminate_resource_error(error, operation_phase, 0, actual))
883 }
884
885 pub(crate) fn preflight_svg_structure(
886 &self,
887 elements: usize,
888 tree_depth: usize,
889 operation_phase: OperationPhase,
890 ) -> Result<(), OperationWorkError> {
891 self.resource_checkpoint(operation_phase)?;
892 self.policy
893 .check_svg_structure(elements, tree_depth)
894 .map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
895 }
896
897 pub(crate) fn preflight_parsed_render(
898 &self,
899 parsed: &ParsedDiagramRender,
900 operation_phase: OperationPhase,
901 ) -> Result<(), OperationWorkError> {
902 self.resource_checkpoint(operation_phase)?;
903 let result = self.policy.check_parsed_render(parsed);
904 self.resource_checkpoint(operation_phase)?;
905 result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
906 }
907
908 #[cfg(feature = "diagram-class")]
909 pub(crate) fn preflight_class_complexity(
910 &self,
911 model: &ClassDiagram,
912 operation_phase: OperationPhase,
913 ) -> Result<ClassComplexity, OperationWorkError> {
914 self.resource_checkpoint(operation_phase)?;
915 let result = self.policy.check_class_complexity(model);
916 self.resource_checkpoint(operation_phase)?;
917 result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
918 }
919
920 #[cfg(feature = "diagram-sequence")]
921 pub(crate) fn preflight_sequence_complexity(
922 &self,
923 model: &merman_core::diagrams::sequence::SequenceDiagramRenderModel,
924 operation_phase: OperationPhase,
925 ) -> Result<SequenceComplexity, OperationWorkError> {
926 self.resource_checkpoint(operation_phase)?;
927 let result = self.policy.check_sequence_complexity(model);
928 self.resource_checkpoint(operation_phase)?;
929 result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
930 }
931
932 pub(crate) fn terminate_absolute_resource_error(
933 &self,
934 error: ResourceLimitExceeded,
935 operation_phase: OperationPhase,
936 ) -> OperationWorkError {
937 let actual = error.actual;
938 self.terminate_resource_error(error, operation_phase, 0, actual)
939 }
940
941 pub(crate) fn terminate_svg_byte_count_overflow(
942 &self,
943 resource_phase: ResourceLimitPhase,
944 operation_phase: OperationPhase,
945 ) -> OperationWorkError {
946 self.terminate_resource_error(
947 accumulation_overflow(
948 self.policy,
949 resource_phase,
950 RenderResourceLimitId::MaxSvgBytes,
951 ),
952 operation_phase,
953 0,
954 usize::MAX,
955 )
956 }
957
958 fn terminate_resource_error(
959 &self,
960 error: ResourceLimitExceeded,
961 phase: OperationPhase,
962 consumed: usize,
963 requested: usize,
964 ) -> OperationWorkError {
965 let provenance = self.operation_resource_provenance();
966 let terminal = match error.cause {
967 ResourceLimitCause::Ceiling => {
968 self.control
969 .terminate_resource_limit(OperationResourceLimitExceeded {
970 id: error.limit,
971 phase,
972 resource_phase: error.phase.as_str(),
973 limit: saturating_u64(error.max),
974 consumed: saturating_u64(consumed),
975 requested: saturating_u64(requested),
976 provenance,
977 })
978 }
979 ResourceLimitCause::ArithmeticOverflow => self.control.terminate_resource_overflow(
980 error.limit,
981 phase,
982 error.phase.as_str(),
983 saturating_u64(error.actual),
984 saturating_u64(error.max),
985 provenance,
986 ),
987 };
988 self.map_terminal_error(terminal)
989 }
990
991 fn operation_resource_provenance(&self) -> OperationResourceProvenance {
992 OperationResourceProvenance::new(
993 OperationResourceDomain::Render,
994 Some(self.policy.profile()),
995 self.policy
996 .explicit_overrides()
997 .map(|(id, value)| OperationResourceOverride {
998 id: id.as_str(),
999 value: saturating_u64(value),
1000 }),
1001 )
1002 }
1003
1004 fn map_terminal_error(&self, error: OperationLedgerError) -> OperationWorkError {
1005 match error {
1006 OperationLedgerError::Cancelled(error) => OperationWorkError::Cancelled(error),
1007 OperationLedgerError::ResourceLimitExceeded(error) => self
1008 .project_resource_limit(&error)
1009 .map(OperationWorkError::ResourceLimitExceeded)
1010 .unwrap_or_else(|| {
1011 OperationWorkError::ForeignResourceTerminal(
1012 OperationLedgerError::ResourceLimitExceeded(error),
1013 )
1014 }),
1015 OperationLedgerError::ArithmeticOverflow { .. } => self
1016 .project_resource_overflow(&error)
1017 .map(OperationWorkError::ResourceLimitExceeded)
1018 .unwrap_or(OperationWorkError::ForeignResourceTerminal(error)),
1019 }
1020 }
1021
1022 fn project_resource_limit(
1023 &self,
1024 error: &OperationResourceLimitExceeded,
1025 ) -> Option<ResourceLimitExceeded> {
1026 let (profile, explicit_overrides) = project_render_provenance(&error.provenance)?;
1027 Some(ResourceLimitExceeded {
1028 cause: ResourceLimitCause::Ceiling,
1029 phase: ResourceLimitPhase::from_stable_id(error.resource_phase)?,
1030 limit: error.id,
1031 actual: saturating_usize(error.consumed.saturating_add(error.requested)),
1032 max: saturating_usize(error.limit),
1033 profile,
1034 explicit_overrides,
1035 })
1036 }
1037
1038 fn project_resource_overflow(
1039 &self,
1040 error: &OperationLedgerError,
1041 ) -> Option<ResourceLimitExceeded> {
1042 let OperationLedgerError::ArithmeticOverflow {
1043 id,
1044 resource_phase,
1045 actual,
1046 maximum,
1047 provenance,
1048 ..
1049 } = error
1050 else {
1051 return None;
1052 };
1053 let (profile, explicit_overrides) = project_render_provenance(provenance)?;
1054 Some(ResourceLimitExceeded {
1055 cause: ResourceLimitCause::ArithmeticOverflow,
1056 phase: ResourceLimitPhase::from_stable_id(resource_phase)?,
1057 limit: id,
1058 actual: saturating_usize(*actual),
1059 max: saturating_usize(*maximum),
1060 profile,
1061 explicit_overrides,
1062 })
1063 }
1064
1065 #[cfg(test)]
1067 pub(crate) fn remaining_svg_bytes(&self) -> Option<usize> {
1068 let maximum = self.policy.value(ResourceLimitId::MaxSvgBytes)?;
1069 let used = self
1070 .projected_svg_bytes
1071 .load(std::sync::atomic::Ordering::Relaxed);
1072 Some(maximum.saturating_sub(used))
1073 }
1074
1075 pub(crate) fn reserve_svg_bytes_up_to(
1081 &self,
1082 requested: usize,
1083 ) -> Result<SvgByteReservation, OperationWorkError> {
1084 let phase = OperationPhase::Emit;
1085 self.resource_checkpoint(phase)?;
1086 let Some(maximum) = self.policy.value(ResourceLimitId::MaxSvgBytes) else {
1087 self.charge_svg_bytes(requested)?;
1088 return Ok(SvgByteReservation {
1089 additional_bytes: requested,
1090 limit_error: None,
1091 });
1092 };
1093
1094 let mut used = self
1095 .projected_svg_bytes
1096 .load(std::sync::atomic::Ordering::Relaxed);
1097 loop {
1098 let available = maximum.saturating_sub(used);
1099 let additional_bytes = requested.min(available);
1100 let Some(next) = used.checked_add(additional_bytes) else {
1101 return Err(self.terminate_resource_error(
1102 accumulation_overflow(
1103 self.policy,
1104 ResourceLimitPhase::SvgOutput,
1105 RenderResourceLimitId::MaxSvgBytes,
1106 ),
1107 phase,
1108 used,
1109 additional_bytes,
1110 ));
1111 };
1112 match self.projected_svg_bytes.compare_exchange_weak(
1113 used,
1114 next,
1115 std::sync::atomic::Ordering::Relaxed,
1116 std::sync::atomic::Ordering::Relaxed,
1117 ) {
1118 Ok(_) => {
1119 let limit_error = (additional_bytes < requested).then(|| {
1120 svg_reservation_limit_error(self.policy, maximum, used, requested)
1121 });
1122 return Ok(SvgByteReservation {
1123 additional_bytes,
1124 limit_error,
1125 });
1126 }
1127 Err(actual) => {
1128 self.resource_checkpoint(phase)?;
1129 used = actual;
1130 }
1131 }
1132 }
1133 }
1134
1135 pub(crate) fn reconcile_svg_bytes(
1137 &self,
1138 reserved: usize,
1139 actual: usize,
1140 ) -> Result<(), OperationWorkError> {
1141 self.resource_checkpoint(OperationPhase::Emit)?;
1142 if actual > reserved {
1143 return self.charge_svg_bytes(actual - reserved);
1144 }
1145
1146 let released = reserved - actual;
1147 if released != 0 {
1148 let previous = self
1149 .projected_svg_bytes
1150 .fetch_sub(released, std::sync::atomic::Ordering::Relaxed);
1151 debug_assert!(previous >= released);
1152 }
1153 Ok(())
1154 }
1155
1156 pub(crate) fn used(&self) -> usize {
1157 self.used.load(std::sync::atomic::Ordering::Relaxed)
1158 }
1159
1160 #[cfg(test)]
1161 pub(crate) fn projected_svg_bytes(&self) -> usize {
1162 self.projected_svg_bytes
1163 .load(std::sync::atomic::Ordering::Relaxed)
1164 }
1165}
1166
1167fn project_render_provenance(
1168 provenance: &OperationResourceProvenance,
1169) -> Option<(RenderResourceProfile, Vec<ResourceLimitOverride>)> {
1170 if provenance.domain != OperationResourceDomain::Render {
1171 return None;
1172 }
1173 let explicit_overrides = provenance
1174 .explicit_overrides
1175 .iter()
1176 .map(|override_| {
1177 Some(ResourceLimitOverride {
1178 id: ResourceLimitId::from_stable_id(override_.id)?,
1179 value: saturating_usize(override_.value),
1180 })
1181 })
1182 .collect::<Option<Vec<_>>>()?;
1183 Some((provenance.profile?, explicit_overrides))
1184}
1185
1186fn saturating_usize(value: u64) -> usize {
1187 usize::try_from(value).unwrap_or(usize::MAX)
1188}
1189
1190fn saturating_u64(value: usize) -> u64 {
1191 u64::try_from(value).unwrap_or(u64::MAX)
1192}
1193
1194fn accumulation_overflow(
1195 policy: RenderResourcePolicy,
1196 phase: ResourceLimitPhase,
1197 id: RenderResourceLimitId,
1198) -> ResourceLimitExceeded {
1199 let limit = ResourceLimitId::Render(id);
1200 ResourceLimitExceeded {
1201 cause: ResourceLimitCause::ArithmeticOverflow,
1202 phase,
1203 limit: limit.as_str(),
1204 actual: usize::MAX,
1205 max: policy.value(limit).unwrap_or(usize::MAX),
1206 profile: policy.profile(),
1207 explicit_overrides: policy
1208 .explicit_overrides()
1209 .map(|(id, value)| ResourceLimitOverride { id, value })
1210 .collect(),
1211 }
1212}
1213
1214#[cfg_attr(
1215 not(feature = "all-diagrams"),
1216 allow(
1217 dead_code,
1218 reason = "Shared operation support has different consumers in each diagram selection."
1219 )
1220)]
1221fn svg_byte_limit_error(policy: RenderResourcePolicy, maximum: usize) -> ResourceLimitExceeded {
1222 ResourceLimitExceeded {
1223 cause: ResourceLimitCause::Ceiling,
1224 phase: ResourceLimitPhase::SvgOutput,
1225 limit: ResourceLimitId::MaxSvgBytes.as_str(),
1226 actual: maximum.saturating_add(1),
1227 max: maximum,
1228 profile: policy.profile(),
1229 explicit_overrides: policy
1230 .explicit_overrides()
1231 .map(|(id, value)| ResourceLimitOverride { id, value })
1232 .collect(),
1233 }
1234}
1235
1236#[cfg_attr(
1237 not(feature = "all-diagrams"),
1238 allow(
1239 dead_code,
1240 reason = "Shared operation support has different consumers in each diagram selection."
1241 )
1242)]
1243fn svg_reservation_limit_error(
1244 policy: RenderResourcePolicy,
1245 maximum: usize,
1246 used: usize,
1247 requested: usize,
1248) -> ResourceLimitExceeded {
1249 if used.checked_add(requested).is_none() {
1250 return accumulation_overflow(
1251 policy,
1252 ResourceLimitPhase::SvgOutput,
1253 RenderResourceLimitId::MaxSvgBytes,
1254 );
1255 }
1256 svg_byte_limit_error(policy, maximum)
1257}
1258
1259#[derive(Debug, Clone, PartialEq, Eq)]
1260#[non_exhaustive]
1261pub struct ResourceLimitExceeded {
1262 pub cause: ResourceLimitCause,
1263 pub phase: ResourceLimitPhase,
1264 pub limit: &'static str,
1265 pub actual: usize,
1266 pub max: usize,
1267 pub profile: RenderResourceProfile,
1268 pub explicit_overrides: Vec<ResourceLimitOverride>,
1269}
1270
1271impl std::fmt::Display for ResourceLimitExceeded {
1272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1273 write!(
1274 f,
1275 "resource limit exceeded during {}: {}",
1276 self.phase, self.limit
1277 )?;
1278 if self.cause == ResourceLimitCause::ArithmeticOverflow {
1279 write!(f, " cause={}", self.cause)?;
1280 }
1281 write!(f, " actual={} max={}", self.actual, self.max)
1282 }
1283}
1284
1285impl std::error::Error for ResourceLimitExceeded {}
1286
1287impl ResourceLimitExceeded {
1288 fn from_input(policy: &RenderResourcePolicy, error: InputResourceLimitExceeded) -> Self {
1289 Self {
1290 cause: ResourceLimitCause::Ceiling,
1291 phase: match error.phase {
1292 InputResourceLimitPhase::Source => ResourceLimitPhase::Source,
1293 InputResourceLimitPhase::Model => ResourceLimitPhase::LayoutModel,
1294 },
1295 limit: error.limit,
1296 actual: error.actual,
1297 max: error.max,
1298 profile: error.profile,
1299 explicit_overrides: policy
1300 .explicit_overrides()
1301 .map(|(id, value)| ResourceLimitOverride { id, value })
1302 .collect(),
1303 }
1304 }
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308pub struct ResourceLimitOverride {
1309 pub id: ResourceLimitId,
1310 pub value: usize,
1311}
1312
1313#[cfg(all(test, feature = "all-diagrams"))]
1314mod tests {
1315 use super::*;
1316 use merman_core::diagrams::flowchart::{
1317 FlowEdge, FlowEdgeMarker, FlowEdgeStroke, FlowEdgeVisibility, FlowNode, FlowSubgraph,
1318 };
1319 use merman_core::{Engine, ParseOptions, RenderSemanticModel};
1320 use std::collections::HashSet;
1321
1322 #[test]
1323 fn layout_work_defaults_preserve_bounded_general_and_native_profiles() {
1324 for (profile, expected) in [
1325 (RenderResourceProfile::Constrained, Some(125_000)),
1326 (RenderResourceProfile::Interactive, Some(14_100_000)),
1327 (RenderResourceProfile::TrustedNative, Some(15_000_000)),
1328 (RenderResourceProfile::UnboundedForTrustedInput, None),
1329 ] {
1330 let policy = RenderResourcePolicy::for_profile(profile);
1331 assert_eq!(policy.value(ResourceLimitId::MaxLayoutWorkUnits), expected);
1332 assert!(policy.explicit_overrides().next().is_none());
1333 }
1334 assert_eq!(
1335 RenderResourcePolicy::default(),
1336 RenderResourcePolicy::interactive()
1337 );
1338 }
1339
1340 #[test]
1341 fn resource_contract_is_complete_unique_and_drives_every_profile() {
1342 assert_eq!(
1343 RESOURCE_PROFILE_DESCRIPTORS.len(),
1344 RenderResourceProfile::ALL.len()
1345 );
1346 assert_eq!(RESOURCE_LIMIT_DESCRIPTORS.len(), RESOURCE_LIMIT_COUNT);
1347
1348 let profile_ids = RESOURCE_PROFILE_DESCRIPTORS
1349 .iter()
1350 .map(|descriptor| descriptor.id)
1351 .collect::<HashSet<_>>();
1352 assert_eq!(profile_ids.len(), RESOURCE_PROFILE_DESCRIPTORS.len());
1353 assert_eq!(
1354 RESOURCE_PROFILE_DESCRIPTORS
1355 .iter()
1356 .filter(|descriptor| descriptor.recommended_binding_default)
1357 .map(|descriptor| descriptor.profile)
1358 .collect::<Vec<_>>(),
1359 vec![GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE]
1360 );
1361 for profile in RenderResourceProfile::ALL {
1362 let descriptor = profile.descriptor();
1363 assert_eq!(RenderResourceProfile::from_id(descriptor.id), Some(profile));
1364 let policy = RenderResourcePolicy::for_profile(profile);
1365 for limit in RESOURCE_LIMIT_DESCRIPTORS {
1366 assert_eq!(policy.profile(), profile);
1367 if limit.hard_cap {
1368 assert!(!limit.overridable);
1369 assert!(policy.value(limit.id).is_some());
1370 }
1371 }
1372 }
1373
1374 let limit_ids = RESOURCE_LIMIT_DESCRIPTORS
1375 .iter()
1376 .map(|descriptor| descriptor.stable_id)
1377 .collect::<HashSet<_>>();
1378 assert_eq!(limit_ids.len(), RESOURCE_LIMIT_DESCRIPTORS.len());
1379 for descriptor in RESOURCE_LIMIT_DESCRIPTORS {
1380 assert_eq!(
1381 ResourceLimitId::from_stable_id(descriptor.stable_id),
1382 Some(descriptor.id)
1383 );
1384 assert_eq!(descriptor.id.descriptor(), descriptor);
1385 }
1386 }
1387
1388 #[test]
1389 fn resource_overrides_fail_closed_for_unknown_and_internal_ids() {
1390 let mut limits = RenderResourcePolicy::interactive();
1391 assert!(matches!(
1392 limits.apply_override("future_limit", 1),
1393 Err(ResourceLimitOverrideError::UnknownLimit(_))
1394 ));
1395 for hard_cap in [
1396 SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
1397 SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
1398 ] {
1399 assert_eq!(
1400 limits.apply_override(hard_cap, 1),
1401 Err(ResourceLimitOverrideError::HardCap(hard_cap))
1402 );
1403 }
1404 assert_eq!(
1405 limits.apply_override("max_svg_elements", 0),
1406 Err(ResourceLimitOverrideError::NonPositive("max_svg_elements"))
1407 );
1408 limits.apply_override("max_svg_elements", 7).unwrap();
1409 assert_eq!(limits.value(ResourceLimitId::MaxSvgElements), Some(7));
1410 }
1411
1412 #[test]
1413 fn resolved_svg_backend_hard_caps_remain_active_for_unbounded_policy() {
1414 let policy = RenderResourcePolicy::unbounded_for_trusted_input();
1415 for (elements, tree_depth, limit, actual, max) in [
1416 (
1417 MAX_RESVG_TREE_NODES + 1,
1418 0,
1419 SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
1420 MAX_RESVG_TREE_NODES + 1,
1421 MAX_RESVG_TREE_NODES,
1422 ),
1423 (
1424 0,
1425 MAX_RESVG_TREE_DEPTH + 1,
1426 SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
1427 MAX_RESVG_TREE_DEPTH + 1,
1428 MAX_RESVG_TREE_DEPTH,
1429 ),
1430 ] {
1431 let error = policy
1432 .check_svg_structure(elements, tree_depth)
1433 .unwrap_err();
1434
1435 assert_eq!(error.phase, ResourceLimitPhase::SvgPostprocess);
1436 assert_eq!(error.limit, limit);
1437 assert_eq!(error.actual, actual);
1438 assert_eq!(error.max, max);
1439 }
1440 }
1441
1442 #[test]
1443 fn source_limit_reports_structured_error() {
1444 let err = RenderResourcePolicy::unbounded_for_trusted_input()
1445 .with_limit(ResourceLimitId::MaxSourceBytes, 4)
1446 .unwrap()
1447 .with_limit(ResourceLimitId::MaxSvgBytes, 123)
1448 .unwrap()
1449 .check_source_bytes("12345")
1450 .unwrap_err();
1451
1452 assert_eq!(err.phase, ResourceLimitPhase::Source);
1453 assert_eq!(err.limit, "max_source_bytes");
1454 assert_eq!(err.actual, 5);
1455 assert_eq!(err.max, 4);
1456 assert_eq!(
1457 err.explicit_overrides,
1458 vec![
1459 ResourceLimitOverride {
1460 id: ResourceLimitId::MaxSourceBytes,
1461 value: 4,
1462 },
1463 ResourceLimitOverride {
1464 id: ResourceLimitId::MaxSvgBytes,
1465 value: 123,
1466 },
1467 ]
1468 );
1469 }
1470
1471 #[test]
1472 fn derived_layout_work_limits_report_the_owned_phase_and_metric() {
1473 let limits = RenderResourcePolicy::unbounded_for_trusted_input()
1474 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 7)
1475 .unwrap();
1476
1477 let error = limits.check_layout_work_units(8).unwrap_err();
1478 assert_eq!(error.phase, ResourceLimitPhase::LayoutModel);
1479 assert_eq!(error.limit, "max_layout_work_units");
1480 assert_eq!(error.cause, ResourceLimitCause::Ceiling);
1481 assert_eq!(error.actual, 8);
1482 assert_eq!(error.max, 7);
1483 }
1484
1485 #[test]
1486 fn operation_work_meter_preflight_does_not_consume_budget() {
1487 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1488 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 7)
1489 .unwrap();
1490 let meter = OperationWorkMeter::new(policy);
1491
1492 meter.preflight(7).unwrap();
1493 meter.preflight(7).unwrap();
1494 assert_eq!(meter.used(), 0);
1495 meter.charge(7).unwrap();
1496 assert_eq!(meter.used(), 7);
1497 }
1498
1499 #[test]
1500 fn operation_work_meter_checks_shared_control_before_charging() {
1501 let control = OperationControl::new();
1502 control.cancel();
1503 let meter = OperationWorkMeter::new_with_control(
1504 RenderResourcePolicy::unbounded_for_trusted_input()
1505 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 1)
1506 .unwrap(),
1507 control,
1508 );
1509
1510 let first = meter.charge(2).unwrap_err();
1511 let OperationWorkError::Cancelled(error) = &first else {
1512 panic!("expected cancellation to win before resource accounting");
1513 };
1514 assert_eq!(error.phase, OperationPhase::Layout);
1515 assert_eq!(meter.used(), 0);
1516 assert_eq!(meter.charge(2).unwrap_err(), first);
1517 }
1518
1519 #[test]
1520 fn operation_work_meter_checks_deadline_before_charging() {
1521 let control = OperationControl::new().with_deadline(std::time::Duration::ZERO);
1522 let meter = OperationWorkMeter::new_with_control(
1523 RenderResourcePolicy::unbounded_for_trusted_input(),
1524 control,
1525 );
1526
1527 let OperationWorkError::Cancelled(error) = meter.charge(1).unwrap_err() else {
1528 panic!("expected deadline cancellation before resource accounting");
1529 };
1530 assert_eq!(error.phase, OperationPhase::Layout);
1531 assert_eq!(error.reason, merman_core::CancelReason::DeadlineExceeded);
1532 assert_eq!(meter.used(), 0);
1533 }
1534
1535 #[test]
1536 fn model_preflight_observes_cancellation_before_latching_its_limit() {
1537 let parsed = Engine::new()
1538 .parse_diagram_for_render_model_sync("flowchart TD\nA --> B\n", ParseOptions::strict())
1539 .expect("the controlled fixture should parse")
1540 .expect("the controlled fixture should produce a render model");
1541 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1542 .with_limit(ResourceLimitId::MaxModelItems, 1)
1543 .expect("the model limit should be valid");
1544 let control = OperationControl::new();
1545 control.cancel_after_checkpoints(1);
1546 let meter = OperationWorkMeter::new_with_control(policy, control);
1547
1548 let first = meter
1549 .preflight_parsed_render(&parsed, OperationPhase::Layout)
1550 .expect_err("cancellation after the scan must beat its pending resource rejection");
1551 assert!(matches!(
1552 first,
1553 OperationWorkError::Cancelled(error)
1554 if error.phase == OperationPhase::Layout
1555 && error.reason == merman_core::CancelReason::Requested
1556 ));
1557 assert_eq!(
1558 meter
1559 .preflight_parsed_render(&parsed, OperationPhase::Emit)
1560 .expect_err("the cancellation must remain the sticky terminal"),
1561 first
1562 );
1563 }
1564
1565 #[test]
1566 fn operation_svg_meter_checks_shared_control_before_reserving_bytes() {
1567 let control = OperationControl::new();
1568 control.cancel();
1569 let meter = OperationWorkMeter::new_with_control(
1570 RenderResourcePolicy::unbounded_for_trusted_input(),
1571 control,
1572 );
1573
1574 let OperationWorkError::Cancelled(error) = meter.charge_svg_bytes(1).unwrap_err() else {
1575 panic!("expected cancellation to win before SVG accounting");
1576 };
1577 assert_eq!(error.phase, OperationPhase::Emit);
1578 assert_eq!(meter.projected_svg_bytes(), 0);
1579 }
1580
1581 #[test]
1582 fn operation_work_meter_replays_first_resource_terminal_after_cancellation() {
1583 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1584 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 10)
1585 .unwrap();
1586 let control = OperationControl::new();
1587 let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1588 meter.charge(8).unwrap();
1589
1590 let first = meter.charge(3).unwrap_err();
1591 let OperationWorkError::ResourceLimitExceeded(error) = &first else {
1592 panic!("expected a resource rejection");
1593 };
1594 assert_eq!(error.cause, ResourceLimitCause::Ceiling);
1595 assert_eq!(error.actual, 11);
1596 assert_eq!(error.max, 10);
1597 assert_eq!(meter.used(), 8);
1598
1599 control.cancel();
1600 assert_eq!(meter.charge(2).unwrap_err(), first);
1601 assert_eq!(meter.used(), 8);
1602 }
1603
1604 #[test]
1605 fn operation_work_meter_replays_the_originating_policy_provenance() {
1606 let originating_policy = RenderResourcePolicy::constrained()
1607 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 1)
1608 .unwrap()
1609 .with_limit(ResourceLimitId::MaxSvgBytes, 17)
1610 .unwrap();
1611 let observing_policy = RenderResourcePolicy::interactive()
1612 .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 99)
1613 .unwrap();
1614 let control = OperationControl::new();
1615 let originating = OperationWorkMeter::new_with_control(originating_policy, control.clone());
1616 let observing = OperationWorkMeter::new_with_control(observing_policy, control);
1617
1618 let OperationWorkError::ResourceLimitExceeded(first) = originating.charge(2).unwrap_err()
1619 else {
1620 panic!("expected the originating ceiling");
1621 };
1622 let OperationWorkError::ResourceLimitExceeded(replayed) =
1623 observing.checkpoint(OperationPhase::Emit).unwrap_err()
1624 else {
1625 panic!("expected the stored render terminal to retain its typed projection");
1626 };
1627
1628 assert_eq!(replayed, first);
1629 assert_eq!(replayed.profile, RenderResourceProfile::Constrained);
1630 assert_eq!(
1631 replayed.explicit_overrides,
1632 vec![
1633 ResourceLimitOverride {
1634 id: ResourceLimitId::MaxLayoutWorkUnits,
1635 value: 1,
1636 },
1637 ResourceLimitOverride {
1638 id: ResourceLimitId::MaxSvgBytes,
1639 value: 17,
1640 },
1641 ]
1642 );
1643 }
1644
1645 #[test]
1646 fn operation_work_meter_preserves_a_foreign_ascii_domain() {
1647 let control = OperationControl::new();
1648 let terminal = control.terminate_resource_limit(OperationResourceLimitExceeded {
1649 id: "max_ascii_output_bytes",
1650 phase: OperationPhase::Emit,
1651 resource_phase: "ascii_output",
1652 limit: 7,
1653 consumed: 7,
1654 requested: 1,
1655 provenance: OperationResourceProvenance::new(
1656 OperationResourceDomain::Ascii,
1657 Some(RenderResourceProfile::Constrained),
1658 [OperationResourceOverride {
1659 id: "max_ascii_output_bytes",
1660 value: 7,
1661 }],
1662 ),
1663 });
1664 let observing =
1665 OperationWorkMeter::new_with_control(RenderResourcePolicy::interactive(), control);
1666
1667 assert_eq!(
1668 observing
1669 .checkpoint(OperationPhase::Layout)
1670 .expect_err("render must not project an ASCII resource terminal"),
1671 OperationWorkError::ForeignResourceTerminal(terminal)
1672 );
1673 }
1674
1675 #[test]
1676 fn operation_work_meter_overflow_fails_under_unlimited_policy() {
1677 let meter = OperationWorkMeter::new(RenderResourcePolicy::unbounded_for_trusted_input());
1678 meter.charge(usize::MAX).unwrap();
1679
1680 let preflight = meter.preflight(1).unwrap_err();
1681 let OperationWorkError::ResourceLimitExceeded(preflight_error) = &preflight else {
1682 panic!("expected a resource rejection");
1683 };
1684 assert_eq!(
1685 preflight_error.cause,
1686 ResourceLimitCause::ArithmeticOverflow
1687 );
1688 assert_eq!(preflight_error.limit, "max_layout_work_units");
1689 assert_eq!(preflight_error.max, usize::MAX);
1690 assert_eq!(meter.used(), usize::MAX);
1691
1692 assert_eq!(meter.charge(1).unwrap_err(), preflight);
1693 assert_eq!(meter.used(), usize::MAX);
1694 }
1695
1696 #[test]
1697 fn operation_svg_meter_accepts_exact_limit_and_rejects_one_more() {
1698 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1699 .with_limit(ResourceLimitId::MaxSvgBytes, 10)
1700 .unwrap();
1701 let control = OperationControl::new();
1702 let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1703
1704 meter.charge_svg_bytes(10).unwrap();
1705 assert_eq!(meter.projected_svg_bytes(), 10);
1706 assert_eq!(meter.remaining_svg_bytes(), Some(0));
1707
1708 let first = meter.charge_svg_bytes(1).unwrap_err();
1709 let OperationWorkError::ResourceLimitExceeded(error) = &first else {
1710 panic!("expected a resource rejection");
1711 };
1712 assert_eq!(error.actual, 11);
1713 assert_eq!(error.max, 10);
1714 assert_eq!(meter.projected_svg_bytes(), 10);
1715
1716 control.cancel();
1717 assert_eq!(meter.charge_svg_bytes(1).unwrap_err(), first);
1718 assert_eq!(meter.projected_svg_bytes(), 10);
1719 }
1720
1721 #[test]
1722 fn operation_svg_meter_reconciles_conservative_reservations() {
1723 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1724 .with_limit(ResourceLimitId::MaxSvgBytes, 12)
1725 .unwrap();
1726 let meter = OperationWorkMeter::new(policy);
1727
1728 meter.charge_svg_bytes(8).unwrap();
1729 meter.reconcile_svg_bytes(8, 5).unwrap();
1730 assert_eq!(meter.projected_svg_bytes(), 5);
1731 assert_eq!(meter.remaining_svg_bytes(), Some(7));
1732
1733 meter.reconcile_svg_bytes(5, 12).unwrap();
1734 assert_eq!(meter.projected_svg_bytes(), 12);
1735 assert_eq!(meter.remaining_svg_bytes(), Some(0));
1736 }
1737
1738 #[test]
1739 fn operation_svg_meter_atomically_reserves_available_growth() {
1740 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1741 .with_limit(ResourceLimitId::MaxSvgBytes, 10)
1742 .unwrap();
1743 let meter = OperationWorkMeter::new(policy);
1744
1745 meter.charge_svg_bytes(7).unwrap();
1746 let reservation = meter.reserve_svg_bytes_up_to(5).unwrap();
1747 assert_eq!(reservation.additional_bytes, 3);
1748 let error = reservation
1749 .limit_error
1750 .expect("a partial reservation carries the deterministic limit error");
1751 assert_eq!(error.actual, 11);
1752 assert_eq!(error.max, 10);
1753 assert_eq!(meter.projected_svg_bytes(), 10);
1754
1755 meter.reconcile_svg_bytes(3, 1).unwrap();
1756 assert_eq!(meter.projected_svg_bytes(), 8);
1757 assert_eq!(meter.remaining_svg_bytes(), Some(2));
1758
1759 meter.charge_svg_bytes(2).unwrap();
1760 assert_eq!(meter.projected_svg_bytes(), 10);
1761 }
1762
1763 #[test]
1764 fn partial_svg_reservation_preserves_arithmetic_overflow_classification() {
1765 let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1766 .with_limit(ResourceLimitId::MaxSvgBytes, usize::MAX)
1767 .unwrap();
1768 let control = OperationControl::new();
1769 let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1770
1771 meter.charge_svg_bytes(usize::MAX).unwrap();
1772 let reservation = meter.reserve_svg_bytes_up_to(1).unwrap();
1773 assert_eq!(reservation.additional_bytes, 0);
1774 let error = reservation
1775 .limit_error
1776 .expect("an overflowing partial reservation must retain its error");
1777 assert_eq!(error.cause, ResourceLimitCause::ArithmeticOverflow);
1778 assert_eq!(error.actual, usize::MAX);
1779 assert_eq!(error.max, usize::MAX);
1780 assert_eq!(meter.projected_svg_bytes(), usize::MAX);
1781
1782 let first = meter.terminate_absolute_resource_error(error, OperationPhase::Emit);
1783 control.cancel();
1784 assert_eq!(
1785 meter
1786 .charge(1)
1787 .expect_err("the finalized overflow must remain the sticky terminal"),
1788 first
1789 );
1790 }
1791
1792 #[test]
1793 fn zenuml_complexity_includes_inline_decorations() {
1794 let parsed = Engine::new()
1795 .parse_diagram_for_render_model_sync(
1796 "zenuml\nA->[rocket]B.call()\n",
1797 ParseOptions::strict(),
1798 )
1799 .unwrap()
1800 .unwrap();
1801 let RenderSemanticModel::Zenuml(model) = parsed.model() else {
1802 panic!("expected ZenUML model");
1803 };
1804 let complexity = ZenumlComplexity::from_model(model);
1805
1806 assert_eq!(complexity.participants, 2);
1807 assert_eq!(complexity.statements, 1);
1808 let required = ["rocket", "call()"]
1809 .into_iter()
1810 .map(str::len)
1811 .sum::<usize>();
1812 assert!(complexity.label_bytes >= required);
1813 }
1814
1815 #[test]
1816 fn zenuml_uses_the_shared_model_budget() {
1817 let parsed = Engine::new()
1818 .parse_diagram_for_render_model_sync(
1819 "zenuml\nA.call() {\n if(ok) {\n if(inner) {\n B.work()\n }\n }\n}\n",
1820 ParseOptions::strict(),
1821 )
1822 .unwrap()
1823 .unwrap();
1824 let RenderSemanticModel::Zenuml(model) = parsed.model() else {
1825 panic!("expected ZenUML model");
1826 };
1827
1828 let limits = RenderResourcePolicy::unbounded_for_trusted_input()
1829 .with_limit(ResourceLimitId::MaxModelItems, 1)
1830 .unwrap();
1831 let error = limits.check_zenuml_complexity(model).unwrap_err();
1832 assert_eq!(error.phase, ResourceLimitPhase::LayoutModel);
1833 assert_eq!(error.limit, "max_model_items");
1834 }
1835
1836 #[test]
1837 fn flowchart_complexity_counts_layout_nodes_and_labels() {
1838 let model = FlowchartModel {
1839 keyword: "graph".to_string(),
1840 acc_descr: None,
1841 acc_title: None,
1842 class_defs: Default::default(),
1843 direction: None,
1844 edge_defaults: None,
1845 vertex_calls: Vec::new(),
1846 nodes: vec![FlowNode {
1847 id: "A".to_string(),
1848 provenance: Default::default(),
1849 label: Some("Alpha".to_string()),
1850 label_type: None,
1851 layout_shape: None,
1852 shape: None,
1853 icon: None,
1854 form: None,
1855 pos: None,
1856 img: None,
1857 constraint: None,
1858 asset_width: None,
1859 asset_height: None,
1860 classes: Vec::new(),
1861 styles: Vec::new(),
1862 link: None,
1863 link_target: None,
1864 have_callback: false,
1865 }],
1866 edges: vec![FlowEdge {
1867 id: "L-A-B".to_string(),
1868 from: "A".to_string(),
1869 to: "B".to_string(),
1870 label: Some("edge".to_string()),
1871 label_type: None,
1872 edge_type: None,
1873 arrow: "-->".to_string(),
1874 start_marker: FlowEdgeMarker::None,
1875 end_marker: FlowEdgeMarker::Point,
1876 is_user_defined_id: false,
1877 stroke: None,
1878 stroke_kind: FlowEdgeStroke::Normal,
1879 visibility: FlowEdgeVisibility::Visible,
1880 interpolate: None,
1881 classes: Vec::new(),
1882 style: Vec::new(),
1883 animate: None,
1884 animation: None,
1885 length: 1,
1886 }],
1887 subgraphs: vec![FlowSubgraph {
1888 metadata: None,
1889 id: "cluster".to_string(),
1890 title: "Cluster".to_string(),
1891 dir: None,
1892 has_explicit_dir: false,
1893 label_type: None,
1894 classes: Vec::new(),
1895 styles: Vec::new(),
1896 nodes: vec!["A".to_string()],
1897 }],
1898 tooltips: Default::default(),
1899 warning_facts: Vec::new(),
1900 };
1901
1902 let complexity = FlowchartComplexity::from_model(&model);
1903 assert_eq!(complexity.nodes, 2);
1904 assert_eq!(complexity.edges, 1);
1905 assert_eq!(complexity.subgraphs, 1);
1906 assert!(complexity.label_bytes >= "AlphaedgeCluster".len());
1907 }
1908}
1909
1910#[cfg(feature = "diagram-class")]
1911pub use merman_core::resources::ClassComplexity;
1912
1913#[cfg(any(
1914 feature = "diagram-flowchart",
1915 feature = "diagram-swimlane",
1916 feature = "diagram-agentflow"
1917))]
1918pub use merman_core::resources::FlowchartComplexity;
1919
1920#[cfg(feature = "diagram-mindmap")]
1921pub use merman_core::resources::MindmapComplexity;
1922
1923#[cfg(feature = "diagram-sequence")]
1924pub use merman_core::resources::SequenceComplexity;
1925
1926#[cfg(feature = "diagram-zenuml")]
1927pub use merman_core::resources::ZenumlComplexity;