Skip to main content

ooxml_wml/
error.rs

1//! Error types for the ooxml-wml crate.
2
3use thiserror::Error;
4
5/// Result type for ooxml-wml operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur when working with Word documents.
9#[derive(Debug, Error)]
10pub enum Error {
11    /// Error from the core ooxml crate (packaging, relationships).
12    #[error("package error: {0}")]
13    Package(#[from] ooxml_opc::Error),
14
15    /// XML parsing error.
16    #[error("XML error: {0}")]
17    Xml(#[from] quick_xml::Error),
18
19    /// XML parsing error with location context.
20    #[error("{context}: {message}")]
21    Parse {
22        /// Human-readable context (e.g., file path, element being parsed).
23        context: String,
24        /// The error message.
25        message: String,
26        /// Byte position in the source where the error occurred.
27        position: Option<u64>,
28    },
29
30    /// Invalid or malformed document structure.
31    #[error("invalid document: {0}")]
32    Invalid(String),
33
34    /// Unsupported feature or element.
35    #[error("unsupported: {0}")]
36    Unsupported(String),
37
38    /// Required part is missing from the package.
39    #[error("missing part: {0}")]
40    MissingPart(String),
41
42    /// UTF-8 decoding error.
43    #[error("UTF-8 error: {0}")]
44    Utf8(#[from] std::string::FromUtf8Error),
45
46    /// I/O error.
47    #[error("I/O error: {0}")]
48    Io(#[from] std::io::Error),
49
50    /// Raw XML parsing error.
51    #[error("raw XML error: {0}")]
52    RawXml(#[from] ooxml_xml::Error),
53}
54
55impl From<crate::generated_serializers::SerializeError> for Error {
56    fn from(e: crate::generated_serializers::SerializeError) -> Self {
57        match e {
58            crate::generated_serializers::SerializeError::Xml(x) => Error::Xml(x),
59            crate::generated_serializers::SerializeError::Io(io) => Error::Io(io),
60            crate::generated_serializers::SerializeError::RawXml(r) => Error::RawXml(r),
61        }
62    }
63}
64
65impl From<crate::generated_parsers::ParseError> for Error {
66    fn from(e: crate::generated_parsers::ParseError) -> Self {
67        match e {
68            crate::generated_parsers::ParseError::Xml(x) => Error::Xml(x),
69            crate::generated_parsers::ParseError::RawXml(r) => Error::RawXml(r),
70            crate::generated_parsers::ParseError::UnexpectedElement(msg) => Error::Invalid(msg),
71            crate::generated_parsers::ParseError::MissingAttribute(msg) => Error::Invalid(msg),
72            crate::generated_parsers::ParseError::InvalidValue(msg) => Error::Invalid(msg),
73        }
74    }
75}
76
77impl Error {
78    /// Create a parse error with context.
79    pub fn parse(context: impl Into<String>, message: impl Into<String>) -> Self {
80        Self::Parse {
81            context: context.into(),
82            message: message.into(),
83            position: None,
84        }
85    }
86
87    /// Create a parse error with context and position.
88    pub fn parse_at(context: impl Into<String>, message: impl Into<String>, position: u64) -> Self {
89        Self::Parse {
90            context: context.into(),
91            message: message.into(),
92            position: Some(position),
93        }
94    }
95
96    /// Add context to an existing error.
97    pub fn with_context(self, context: impl Into<String>) -> Self {
98        match self {
99            Self::Xml(e) => Self::Parse {
100                context: context.into(),
101                message: e.to_string(),
102                position: None,
103            },
104            Self::Parse {
105                message, position, ..
106            } => Self::Parse {
107                context: context.into(),
108                message,
109                position,
110            },
111            other => other,
112        }
113    }
114
115    /// Add position information to an existing error.
116    pub fn at_position(self, position: u64) -> Self {
117        match self {
118            Self::Parse {
119                context, message, ..
120            } => Self::Parse {
121                context,
122                message,
123                position: Some(position),
124            },
125            Self::Xml(e) => Self::Parse {
126                context: String::new(),
127                message: e.to_string(),
128                position: Some(position),
129            },
130            other => other,
131        }
132    }
133}
134
135/// Context for tracking parsing location.
136///
137/// Used to provide better error messages with file paths and element context.
138#[derive(Debug, Clone, Default)]
139pub struct ParseContext {
140    /// The file path being parsed (e.g., "word/document.xml").
141    pub file_path: Option<String>,
142    /// Stack of element names being parsed (for nested context).
143    pub element_stack: Vec<String>,
144}
145
146impl ParseContext {
147    /// Create a new parse context for a file.
148    pub fn new(file_path: impl Into<String>) -> Self {
149        Self {
150            file_path: Some(file_path.into()),
151            element_stack: Vec::new(),
152        }
153    }
154
155    /// Push an element onto the context stack.
156    pub fn push(&mut self, element: impl Into<String>) {
157        self.element_stack.push(element.into());
158    }
159
160    /// Pop an element from the context stack.
161    pub fn pop(&mut self) {
162        self.element_stack.pop();
163    }
164
165    /// Get a human-readable description of the current context.
166    pub fn describe(&self) -> String {
167        let mut parts = Vec::new();
168        if let Some(ref path) = self.file_path {
169            parts.push(path.clone());
170        }
171        if !self.element_stack.is_empty() {
172            parts.push(format!("in <{}>", self.element_stack.join("/")));
173        }
174        if parts.is_empty() {
175            "unknown location".to_string()
176        } else {
177            parts.join(" ")
178        }
179    }
180
181    /// Create an error with this context.
182    pub fn error(&self, message: impl Into<String>) -> Error {
183        Error::parse(self.describe(), message)
184    }
185
186    /// Create an error with this context and position.
187    pub fn error_at(&self, message: impl Into<String>, position: u64) -> Error {
188        Error::parse_at(self.describe(), message, position)
189    }
190}
191
192/// Convert a byte position to line and column numbers.
193///
194/// Returns (line, column) where both are 1-indexed.
195pub fn position_to_line_col(content: &[u8], position: u64) -> (usize, usize) {
196    let position = position as usize;
197    let content = if position <= content.len() {
198        &content[..position]
199    } else {
200        content
201    };
202
203    let mut line = 1;
204    let mut col = 1;
205
206    for &byte in content {
207        if byte == b'\n' {
208            line += 1;
209            col = 1;
210        } else {
211            col += 1;
212        }
213    }
214
215    (line, col)
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_position_to_line_col() {
224        let content = b"line1\nline2\nline3";
225        assert_eq!(position_to_line_col(content, 0), (1, 1));
226        assert_eq!(position_to_line_col(content, 5), (1, 6)); // at '\n'
227        assert_eq!(position_to_line_col(content, 6), (2, 1)); // start of line2
228        assert_eq!(position_to_line_col(content, 12), (3, 1)); // start of line3
229    }
230
231    #[test]
232    fn test_parse_context() {
233        let mut ctx = ParseContext::new("word/document.xml");
234        assert_eq!(ctx.describe(), "word/document.xml");
235
236        ctx.push("w:body");
237        assert_eq!(ctx.describe(), "word/document.xml in <w:body>");
238
239        ctx.push("w:p");
240        assert_eq!(ctx.describe(), "word/document.xml in <w:body/w:p>");
241
242        ctx.pop();
243        assert_eq!(ctx.describe(), "word/document.xml in <w:body>");
244    }
245
246    #[test]
247    fn test_error_with_context() {
248        let err = Error::parse("word/document.xml", "unexpected element");
249        assert!(err.to_string().contains("word/document.xml"));
250        assert!(err.to_string().contains("unexpected element"));
251    }
252}