ddex_parser/
error.rs

1//! Parser-specific error handling
2
3use ddex_core::error::DDEXError;
4use ddex_core::ffi::{FFIError, FFIErrorCategory, FFIErrorSeverity};
5use thiserror::Error;
6
7// Re-export ErrorLocation for use in this crate
8pub use ddex_core::error::ErrorLocation;
9
10// Define Result type alias
11pub type Result<T> = std::result::Result<T, ParseError>;
12
13/// Parser-specific errors
14#[derive(Debug, Error, Clone)]
15pub enum ParseError {
16    #[error("XML parsing error: {message}")]
17    XmlError {
18        message: String,
19        location: ErrorLocation,
20    },
21
22    #[error("Unsupported DDEX version: {version}")]
23    UnsupportedVersion { version: String },
24
25    #[error("Security violation: {message}")]
26    SecurityViolation { message: String },
27
28    #[error("Parse timeout after {seconds} seconds")]
29    Timeout { seconds: u64 },
30
31    #[error("Type conversion error: {message}")]
32    ConversionError {
33        message: String,
34        location: ErrorLocation,
35    },
36
37    #[error("Core error: {0}")]
38    Core(#[from] DDEXError),
39
40    #[error("IO error: {message}")]
41    Io { message: String },
42
43    #[error("XML depth limit exceeded: {depth} > {max}")]
44    DepthLimitExceeded { depth: usize, max: usize },
45
46    #[error("Invalid UTF-8 encoding at position {position}: {error}")]
47    InvalidUtf8 { position: usize, error: String },
48
49    #[error("Mismatched XML tags: expected '{expected}', found '{found}' at position {position}")]
50    MismatchedTags {
51        expected: String,
52        found: String,
53        position: usize,
54    },
55
56    #[error("Unexpected closing tag '{tag}' at position {position}")]
57    UnexpectedClosingTag { tag: String, position: usize },
58
59    #[error("Unclosed XML tags at end of document: {tags:?} at position {position}")]
60    UnclosedTags { tags: Vec<String>, position: usize },
61
62    #[error("Malformed XML: {message} at position {position}")]
63    MalformedXml { message: String, position: usize },
64
65    #[error("Invalid XML attribute: {message} at position {position}")]
66    InvalidAttribute { message: String, position: usize },
67
68    /// Simple XML error variant for compatibility with utf8_utils
69    #[error("XML parsing error: {0}")]
70    SimpleXmlError(String),
71}
72
73impl From<ParseError> for FFIError {
74    fn from(err: ParseError) -> Self {
75        match err {
76            ParseError::Core(core_err) => core_err.into(),
77            ParseError::XmlError { message, location } => FFIError {
78                code: "PARSE_XML_ERROR".to_string(),
79                message,
80                location: Some(ddex_core::ffi::FFIErrorLocation {
81                    line: location.line,
82                    column: location.column,
83                    path: location.path,
84                }),
85                severity: FFIErrorSeverity::Error,
86                hint: Some("Check XML syntax".to_string()),
87                category: FFIErrorCategory::XmlParsing,
88            },
89            ParseError::UnsupportedVersion { version } => FFIError {
90                code: "UNSUPPORTED_VERSION".to_string(),
91                message: format!("Unsupported DDEX version: {}", version),
92                location: None,
93                severity: FFIErrorSeverity::Error,
94                hint: Some("Use ERN 3.8.2, 4.2, or 4.3".to_string()),
95                category: FFIErrorCategory::Version,
96            },
97            ParseError::SecurityViolation { message } => FFIError {
98                code: "SECURITY_VIOLATION".to_string(),
99                message,
100                location: None,
101                severity: FFIErrorSeverity::Error,
102                hint: Some("Check for XXE or entity expansion attacks".to_string()),
103                category: FFIErrorCategory::Validation,
104            },
105            ParseError::Timeout { seconds } => FFIError {
106                code: "PARSE_TIMEOUT".to_string(),
107                message: format!("Parse timeout after {} seconds", seconds),
108                location: None,
109                severity: FFIErrorSeverity::Error,
110                hint: Some("File may be too large or complex".to_string()),
111                category: FFIErrorCategory::Io,
112            },
113            ParseError::ConversionError { message, location } => FFIError {
114                code: "TYPE_CONVERSION_ERROR".to_string(),
115                message,
116                location: Some(ddex_core::ffi::FFIErrorLocation {
117                    line: location.line,
118                    column: location.column,
119                    path: location.path,
120                }),
121                severity: FFIErrorSeverity::Error,
122                hint: Some("Check builder state and validation".to_string()),
123                category: FFIErrorCategory::Validation,
124            },
125            ParseError::Io { message } => FFIError {
126                code: "IO_ERROR".to_string(),
127                message,
128                location: None,
129                severity: FFIErrorSeverity::Error,
130                hint: None,
131                category: FFIErrorCategory::Io,
132            },
133            ParseError::DepthLimitExceeded { depth, max } => FFIError {
134                code: "DEPTH_LIMIT_EXCEEDED".to_string(),
135                message: format!("XML depth limit exceeded: {} > {}", depth, max),
136                location: None,
137                severity: FFIErrorSeverity::Error,
138                hint: Some("Reduce XML nesting depth to prevent stack overflow".to_string()),
139                category: FFIErrorCategory::Validation,
140            },
141            ParseError::InvalidUtf8 { position, error } => FFIError {
142                code: "INVALID_UTF8".to_string(),
143                message: format!("Invalid UTF-8 encoding at position {}: {}", position, error),
144                location: Some(ddex_core::ffi::FFIErrorLocation {
145                    line: 0,
146                    column: 0,
147                    path: "parser".to_string(),
148                }),
149                severity: FFIErrorSeverity::Error,
150                hint: Some("Ensure the XML file is properly encoded as UTF-8".to_string()),
151                category: FFIErrorCategory::XmlParsing,
152            },
153            ParseError::MismatchedTags {
154                expected,
155                found,
156                position,
157            } => FFIError {
158                code: "MISMATCHED_TAGS".to_string(),
159                message: format!(
160                    "Mismatched XML tags: expected '{}', found '{}' at position {}",
161                    expected, found, position
162                ),
163                location: Some(ddex_core::ffi::FFIErrorLocation {
164                    line: 0,
165                    column: 0,
166                    path: "parser".to_string(),
167                }),
168                severity: FFIErrorSeverity::Error,
169                hint: Some(format!(
170                    "Ensure opening tag '{}' has matching closing tag",
171                    expected
172                )),
173                category: FFIErrorCategory::XmlParsing,
174            },
175            ParseError::UnexpectedClosingTag { tag, position } => FFIError {
176                code: "UNEXPECTED_CLOSING_TAG".to_string(),
177                message: format!("Unexpected closing tag '{}' at position {}", tag, position),
178                location: Some(ddex_core::ffi::FFIErrorLocation {
179                    line: 0,
180                    column: 0,
181                    path: "parser".to_string(),
182                }),
183                severity: FFIErrorSeverity::Error,
184                hint: Some(
185                    "Remove the unexpected closing tag or add the missing opening tag".to_string(),
186                ),
187                category: FFIErrorCategory::XmlParsing,
188            },
189            ParseError::UnclosedTags { tags, position } => FFIError {
190                code: "UNCLOSED_TAGS".to_string(),
191                message: format!(
192                    "Unclosed XML tags at end of document: {:?} at position {}",
193                    tags, position
194                ),
195                location: Some(ddex_core::ffi::FFIErrorLocation {
196                    line: 0,
197                    column: 0,
198                    path: "parser".to_string(),
199                }),
200                severity: FFIErrorSeverity::Error,
201                hint: Some(format!("Add closing tags for: {}", tags.join(", "))),
202                category: FFIErrorCategory::XmlParsing,
203            },
204            ParseError::MalformedXml { message, position } => FFIError {
205                code: "MALFORMED_XML".to_string(),
206                message: format!("Malformed XML: {} at position {}", message, position),
207                location: Some(ddex_core::ffi::FFIErrorLocation {
208                    line: 0,
209                    column: 0,
210                    path: "parser".to_string(),
211                }),
212                severity: FFIErrorSeverity::Error,
213                hint: Some("Check XML syntax and structure".to_string()),
214                category: FFIErrorCategory::XmlParsing,
215            },
216            ParseError::InvalidAttribute { message, position } => FFIError {
217                code: "INVALID_ATTRIBUTE".to_string(),
218                message: format!(
219                    "Invalid XML attribute: {} at position {}",
220                    message, position
221                ),
222                location: Some(ddex_core::ffi::FFIErrorLocation {
223                    line: 0,
224                    column: 0,
225                    path: "parser".to_string(),
226                }),
227                severity: FFIErrorSeverity::Error,
228                hint: Some("Check attribute name and value syntax".to_string()),
229                category: FFIErrorCategory::XmlParsing,
230            },
231            ParseError::SimpleXmlError(message) => FFIError {
232                code: "XML_ERROR".to_string(),
233                message,
234                location: None,
235                severity: FFIErrorSeverity::Error,
236                hint: Some("Check XML syntax".to_string()),
237                category: FFIErrorCategory::XmlParsing,
238            },
239        }
240    }
241}
242
243impl From<std::io::Error> for ParseError {
244    fn from(err: std::io::Error) -> Self {
245        ParseError::Io {
246            message: err.to_string(),
247        }
248    }
249}
250
251impl From<std::str::Utf8Error> for ParseError {
252    fn from(err: std::str::Utf8Error) -> Self {
253        ParseError::XmlError {
254            message: format!("UTF-8 encoding error: {}", err),
255            location: ErrorLocation {
256                line: 0,
257                column: 0,
258                byte_offset: None,
259                path: "parser".to_string(),
260            },
261        }
262    }
263}
264
265impl From<quick_xml::events::attributes::AttrError> for ParseError {
266    fn from(err: quick_xml::events::attributes::AttrError) -> Self {
267        ParseError::XmlError {
268            message: format!("XML attribute error: {}", err),
269            location: ErrorLocation {
270                line: 0,
271                column: 0,
272                byte_offset: None,
273                path: "parser".to_string(),
274            },
275        }
276    }
277}
278
279impl From<quick_xml::Error> for ParseError {
280    fn from(err: quick_xml::Error) -> Self {
281        ParseError::XmlError {
282            message: format!("XML parsing error: {}", err),
283            location: ErrorLocation {
284                line: 0,
285                column: 0,
286                byte_offset: None,
287                path: "parser".to_string(),
288            },
289        }
290    }
291}
292
293impl From<String> for ParseError {
294    fn from(err: String) -> Self {
295        ParseError::XmlError {
296            message: err,
297            location: ErrorLocation {
298                line: 0,
299                column: 0,
300                byte_offset: None,
301                path: "parser".to_string(),
302            },
303        }
304    }
305}