Skip to main content

oxdoc_core/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, OxdocError>;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum OxdocErrorCode {
7    Io,
8    CorruptedZip,
9    MissingPart,
10    UnsupportedEncryptedPart,
11    PartTooLarge,
12    SuspiciousZipEntry,
13    SuspiciousRelationshipTarget,
14    MissingCoreRelations,
15    MalformedXmlNode,
16    InvalidArgument,
17}
18
19impl OxdocErrorCode {
20    pub fn as_str(self) -> &'static str {
21        match self {
22            OxdocErrorCode::Io => "E001",
23            OxdocErrorCode::CorruptedZip => "E002",
24            OxdocErrorCode::MissingPart => "E003",
25            OxdocErrorCode::UnsupportedEncryptedPart => "E004",
26            OxdocErrorCode::PartTooLarge => "E005",
27            OxdocErrorCode::SuspiciousZipEntry => "E006",
28            OxdocErrorCode::SuspiciousRelationshipTarget => "E007",
29            OxdocErrorCode::MissingCoreRelations => "E008",
30            OxdocErrorCode::MalformedXmlNode => "E009",
31            OxdocErrorCode::InvalidArgument => "E010",
32        }
33    }
34}
35
36#[derive(Debug, Error)]
37pub enum OxdocError {
38    #[error("I/O error: {0}")]
39    Io(#[from] std::io::Error),
40
41    #[error("corrupted or unsupported ZIP container: {0}")]
42    CorruptedZip(#[from] zip::result::ZipError),
43
44    #[error("missing required OOXML part: {0}")]
45    MissingPart(String),
46
47    #[error("unsupported encrypted OOXML part: {0}")]
48    UnsupportedEncryptedPart(String),
49
50    #[error("OOXML part is too large: {path} is {size} bytes; limit is {limit} bytes")]
51    PartTooLarge { path: String, size: u64, limit: u64 },
52
53    #[error("suspicious OOXML ZIP entry: {path}: {reason}")]
54    SuspiciousZipEntry { path: String, reason: String },
55
56    #[error("suspicious OOXML relationship target in {path}: {target}: {reason}")]
57    SuspiciousRelationshipTarget {
58        path: String,
59        target: String,
60        reason: String,
61    },
62
63    #[error("missing office document relationship")]
64    MissingCoreRelations,
65
66    #[error("malformed XML in {path}: {source}")]
67    MalformedXmlNode {
68        path: String,
69        #[source]
70        source: quick_xml::Error,
71    },
72
73    #[error("invalid argument: {0}")]
74    InvalidArgument(String),
75}
76
77impl OxdocError {
78    pub fn code(&self) -> OxdocErrorCode {
79        match self {
80            OxdocError::Io(_) => OxdocErrorCode::Io,
81            OxdocError::CorruptedZip(_) => OxdocErrorCode::CorruptedZip,
82            OxdocError::MissingPart(_) => OxdocErrorCode::MissingPart,
83            OxdocError::UnsupportedEncryptedPart(_) => OxdocErrorCode::UnsupportedEncryptedPart,
84            OxdocError::PartTooLarge { .. } => OxdocErrorCode::PartTooLarge,
85            OxdocError::SuspiciousZipEntry { .. } => OxdocErrorCode::SuspiciousZipEntry,
86            OxdocError::SuspiciousRelationshipTarget { .. } => {
87                OxdocErrorCode::SuspiciousRelationshipTarget
88            }
89            OxdocError::MissingCoreRelations => OxdocErrorCode::MissingCoreRelations,
90            OxdocError::MalformedXmlNode { .. } => OxdocErrorCode::MalformedXmlNode,
91            OxdocError::InvalidArgument(_) => OxdocErrorCode::InvalidArgument,
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::{OxdocError, OxdocErrorCode};
99
100    #[test]
101    fn maps_errors_to_stable_codes() {
102        let cases = [
103            (
104                OxdocError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing")),
105                OxdocErrorCode::Io,
106                "E001",
107            ),
108            (
109                OxdocError::CorruptedZip(zip::result::ZipError::FileNotFound),
110                OxdocErrorCode::CorruptedZip,
111                "E002",
112            ),
113            (
114                OxdocError::MissingPart("word/document.xml".to_owned()),
115                OxdocErrorCode::MissingPart,
116                "E003",
117            ),
118            (
119                OxdocError::UnsupportedEncryptedPart("word/document.xml".to_owned()),
120                OxdocErrorCode::UnsupportedEncryptedPart,
121                "E004",
122            ),
123            (
124                OxdocError::PartTooLarge {
125                    path: "word/document.xml".to_owned(),
126                    size: 2,
127                    limit: 1,
128                },
129                OxdocErrorCode::PartTooLarge,
130                "E005",
131            ),
132            (
133                OxdocError::SuspiciousZipEntry {
134                    path: "word/document.xml".to_owned(),
135                    reason: "directory".to_owned(),
136                },
137                OxdocErrorCode::SuspiciousZipEntry,
138                "E006",
139            ),
140            (
141                OxdocError::SuspiciousRelationshipTarget {
142                    path: "_rels/.rels".to_owned(),
143                    target: "https://example.invalid/document.xml".to_owned(),
144                    reason: "external".to_owned(),
145                },
146                OxdocErrorCode::SuspiciousRelationshipTarget,
147                "E007",
148            ),
149            (
150                OxdocError::MissingCoreRelations,
151                OxdocErrorCode::MissingCoreRelations,
152                "E008",
153            ),
154            (
155                OxdocError::MalformedXmlNode {
156                    path: "_rels/.rels".to_owned(),
157                    source: quick_xml::Error::Syntax(quick_xml::errors::SyntaxError::UnclosedTag),
158                },
159                OxdocErrorCode::MalformedXmlNode,
160                "E009",
161            ),
162            (
163                OxdocError::InvalidArgument("bad delimiter".to_owned()),
164                OxdocErrorCode::InvalidArgument,
165                "E010",
166            ),
167        ];
168
169        for (error, code, label) in cases {
170            assert_eq!(error.code(), code);
171            assert_eq!(error.code().as_str(), label);
172        }
173    }
174}