use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("package error: {0}")]
Package(#[from] ooxml_opc::Error),
#[error("XML error: {0}")]
Xml(#[from] quick_xml::Error),
#[error("{context}: {message}")]
Parse {
context: String,
message: String,
position: Option<u64>,
},
#[error("invalid document: {0}")]
Invalid(String),
#[error("unsupported: {0}")]
Unsupported(String),
#[error("missing part: {0}")]
MissingPart(String),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("raw XML error: {0}")]
RawXml(#[from] ooxml_xml::Error),
}
impl From<crate::generated_serializers::SerializeError> for Error {
fn from(e: crate::generated_serializers::SerializeError) -> Self {
match e {
crate::generated_serializers::SerializeError::Xml(x) => Error::Xml(x),
crate::generated_serializers::SerializeError::Io(io) => Error::Io(io),
crate::generated_serializers::SerializeError::RawXml(r) => Error::RawXml(r),
}
}
}
impl From<crate::generated_parsers::ParseError> for Error {
fn from(e: crate::generated_parsers::ParseError) -> Self {
match e {
crate::generated_parsers::ParseError::Xml(x) => Error::Xml(x),
crate::generated_parsers::ParseError::RawXml(r) => Error::RawXml(r),
crate::generated_parsers::ParseError::UnexpectedElement(msg) => Error::Invalid(msg),
crate::generated_parsers::ParseError::MissingAttribute(msg) => Error::Invalid(msg),
crate::generated_parsers::ParseError::InvalidValue(msg) => Error::Invalid(msg),
}
}
}
impl Error {
pub fn parse(context: impl Into<String>, message: impl Into<String>) -> Self {
Self::Parse {
context: context.into(),
message: message.into(),
position: None,
}
}
pub fn parse_at(context: impl Into<String>, message: impl Into<String>, position: u64) -> Self {
Self::Parse {
context: context.into(),
message: message.into(),
position: Some(position),
}
}
pub fn with_context(self, context: impl Into<String>) -> Self {
match self {
Self::Xml(e) => Self::Parse {
context: context.into(),
message: e.to_string(),
position: None,
},
Self::Parse {
message, position, ..
} => Self::Parse {
context: context.into(),
message,
position,
},
other => other,
}
}
pub fn at_position(self, position: u64) -> Self {
match self {
Self::Parse {
context, message, ..
} => Self::Parse {
context,
message,
position: Some(position),
},
Self::Xml(e) => Self::Parse {
context: String::new(),
message: e.to_string(),
position: Some(position),
},
other => other,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ParseContext {
pub file_path: Option<String>,
pub element_stack: Vec<String>,
}
impl ParseContext {
pub fn new(file_path: impl Into<String>) -> Self {
Self {
file_path: Some(file_path.into()),
element_stack: Vec::new(),
}
}
pub fn push(&mut self, element: impl Into<String>) {
self.element_stack.push(element.into());
}
pub fn pop(&mut self) {
self.element_stack.pop();
}
pub fn describe(&self) -> String {
let mut parts = Vec::new();
if let Some(ref path) = self.file_path {
parts.push(path.clone());
}
if !self.element_stack.is_empty() {
parts.push(format!("in <{}>", self.element_stack.join("/")));
}
if parts.is_empty() {
"unknown location".to_string()
} else {
parts.join(" ")
}
}
pub fn error(&self, message: impl Into<String>) -> Error {
Error::parse(self.describe(), message)
}
pub fn error_at(&self, message: impl Into<String>, position: u64) -> Error {
Error::parse_at(self.describe(), message, position)
}
}
pub fn position_to_line_col(content: &[u8], position: u64) -> (usize, usize) {
let position = position as usize;
let content = if position <= content.len() {
&content[..position]
} else {
content
};
let mut line = 1;
let mut col = 1;
for &byte in content {
if byte == b'\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_to_line_col() {
let content = b"line1\nline2\nline3";
assert_eq!(position_to_line_col(content, 0), (1, 1));
assert_eq!(position_to_line_col(content, 5), (1, 6)); assert_eq!(position_to_line_col(content, 6), (2, 1)); assert_eq!(position_to_line_col(content, 12), (3, 1)); }
#[test]
fn test_parse_context() {
let mut ctx = ParseContext::new("word/document.xml");
assert_eq!(ctx.describe(), "word/document.xml");
ctx.push("w:body");
assert_eq!(ctx.describe(), "word/document.xml in <w:body>");
ctx.push("w:p");
assert_eq!(ctx.describe(), "word/document.xml in <w:body/w:p>");
ctx.pop();
assert_eq!(ctx.describe(), "word/document.xml in <w:body>");
}
#[test]
fn test_error_with_context() {
let err = Error::parse("word/document.xml", "unexpected element");
assert!(err.to_string().contains("word/document.xml"));
assert!(err.to_string().contains("unexpected element"));
}
}