1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Input validation errors for the oxipdf IR (§7).
//!
//! These errors are produced during `StyledTree` construction and validation.
//! Invalid IR is always a hard error — no silent fixup or fallback.
use crate::node::NodeId;
use crate::version::IrVersion;
/// Errors produced when validating a `StyledTree`.
///
/// Each variant includes machine-readable context sufficient for the consumer
/// to identify and fix the problem without guesswork.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InputValidationError {
/// The tree contains no nodes.
#[error("StyledTree must contain at least one node")]
EmptyTree,
/// The tree's IR version is incompatible with this engine build.
#[error(
"IR version {tree_version} is incompatible with engine version {engine_version} \
(same major version required, tree minor <= engine minor)"
)]
IncompatibleVersion {
tree_version: IrVersion,
engine_version: IrVersion,
},
/// A node's children list references a `NodeId` that does not exist.
#[error(
"{parent} references child {child} which does not exist \
(tree has {node_count} nodes)"
)]
InvalidChildReference {
parent: NodeId,
child: NodeId,
node_count: u32,
},
/// A node appears in the children list of more than one parent,
/// violating the tree (single-parent) invariant.
#[error("{child} is a child of both {parent_a} and {parent_b}")]
MultipleParents {
child: NodeId,
parent_a: NodeId,
parent_b: NodeId,
},
/// A node lists itself as its own child.
#[error("{node} references itself as a child")]
SelfReference { node: NodeId },
/// A non-root node is not reachable from the root via any children chain.
#[error("{node} is not reachable from the root")]
OrphanNode { node: NodeId },
/// A resource limit defined in [`ResourceLimits`](crate::config::ResourceLimits)
/// has been exceeded.
#[error("resource limit exceeded: {limit_name} (current: {current}, max: {max})")]
ResourceLimitExceeded {
/// Human-readable name of the limit (e.g. "node_count", "tree_depth").
limit_name: &'static str,
/// The actual value encountered.
current: u64,
/// The configured maximum.
max: u64,
/// Path from root to the node where the limit was detected.
/// Empty if the limit is tree-global (e.g. total node count).
node_path: Vec<NodeId>,
},
/// The page template configuration is invalid (e.g., margins exceed
/// page dimensions, negative margin values).
#[error("invalid page template: {detail}")]
InvalidPageTemplate { detail: String },
/// A feature is present in the IR but is not supported by this engine
/// version. This is a hard error — unsupported features never produce
/// silent fallbacks.
#[error("unsupported feature \"{feature_name}\" at {node_id}: {detail}")]
UnsupportedFeature {
/// The node where the unsupported feature was encountered.
node_id: NodeId,
/// Machine-readable feature name (e.g. "math_layout", "cmyk_color").
feature_name: String,
/// Human-readable explanation of what is unsupported and why.
detail: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display_messages() {
let err = InputValidationError::EmptyTree;
assert_eq!(err.to_string(), "StyledTree must contain at least one node");
let err = InputValidationError::IncompatibleVersion {
tree_version: IrVersion::new(2, 0),
engine_version: IrVersion::new(1, 0),
};
assert!(err.to_string().contains("2.0"));
assert!(err.to_string().contains("1.0"));
let err = InputValidationError::ResourceLimitExceeded {
limit_name: "node_count",
current: 1_500_000,
max: 1_000_000,
node_path: vec![],
};
assert!(err.to_string().contains("node_count"));
assert!(err.to_string().contains("1500000"));
assert!(err.to_string().contains("1000000"));
}
#[test]
fn unsupported_feature_display() {
let err = InputValidationError::UnsupportedFeature {
node_id: NodeId::from_raw(7),
feature_name: "cmyk_color".into(),
detail: "CMYK color space is not supported".into(),
};
let msg = err.to_string();
assert!(msg.contains("cmyk_color"));
assert!(msg.contains("node#7"));
assert!(msg.contains("CMYK"));
}
#[test]
fn resource_limit_with_node_path() {
let err = InputValidationError::ResourceLimitExceeded {
limit_name: "tree_depth",
current: 300,
max: 256,
node_path: vec![
NodeId::from_raw(0),
NodeId::from_raw(3),
NodeId::from_raw(12),
],
};
assert!(err.to_string().contains("tree_depth"));
assert_eq!(
match &err {
InputValidationError::ResourceLimitExceeded { node_path, .. } => node_path.len(),
_ => 0,
},
3
);
}
#[test]
fn error_is_std_error() {
let err: Box<dyn std::error::Error> = Box::new(InputValidationError::EmptyTree);
// Verify it implements std::error::Error
assert!(!err.to_string().is_empty());
}
}