Skip to main content

oxipdf_ir/
error.rs

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