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