1use std::fmt;
7
8#[derive(Debug, thiserror::Error)]
25#[non_exhaustive]
26pub enum HwpxError {
27 #[error("ZIP error: {0}")]
29 Zip(String),
30
31 #[error("Invalid HWPX mimetype: expected 'application/hwp+zip', got '{actual}'")]
33 InvalidMimetype {
34 actual: String,
36 },
37
38 #[error("Missing required file in HWPX archive: '{path}'")]
40 MissingFile {
41 path: String,
43 },
44
45 #[error("XML parse error in '{file}': {detail}")]
47 XmlParse {
48 file: String,
50 detail: String,
52 },
53
54 #[error("Invalid attribute '{attribute}' on <{element}>: '{value}'")]
56 InvalidAttribute {
57 element: String,
59 attribute: String,
61 value: String,
63 },
64
65 #[error("{kind} index {index} out of bounds (max: {max})")]
67 IndexOutOfBounds {
68 kind: &'static str,
70 index: u32,
72 max: u32,
74 },
75
76 #[error("Invalid HWPX structure: {detail}")]
78 InvalidStructure {
79 detail: String,
81 },
82
83 #[error("I/O error: {0}")]
85 Io(#[from] std::io::Error),
86
87 #[error("Core error: {0}")]
89 Core(#[from] hwpforge_core::CoreError),
90
91 #[error("Foundation error: {0}")]
93 Foundation(#[from] hwpforge_foundation::FoundationError),
94
95 #[error("XML serialization error: {detail}")]
97 XmlSerialize {
98 detail: String,
100 },
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum HwpxErrorCode {
110 Zip = 4000,
112 InvalidMimetype = 4001,
114 MissingFile = 4002,
116 XmlParse = 4003,
118 InvalidAttribute = 4004,
120 IndexOutOfBounds = 4005,
122 InvalidStructure = 4006,
124 Io = 4007,
126 Core = 4008,
128 Foundation = 4009,
130 XmlSerialize = 4010,
132}
133
134impl fmt::Display for HwpxErrorCode {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "E{}", *self as u16)
137 }
138}
139
140impl HwpxError {
141 pub fn code(&self) -> HwpxErrorCode {
143 match self {
144 Self::Zip(_) => HwpxErrorCode::Zip,
145 Self::InvalidMimetype { .. } => HwpxErrorCode::InvalidMimetype,
146 Self::MissingFile { .. } => HwpxErrorCode::MissingFile,
147 Self::XmlParse { .. } => HwpxErrorCode::XmlParse,
148 Self::InvalidAttribute { .. } => HwpxErrorCode::InvalidAttribute,
149 Self::IndexOutOfBounds { .. } => HwpxErrorCode::IndexOutOfBounds,
150 Self::InvalidStructure { .. } => HwpxErrorCode::InvalidStructure,
151 Self::Io(_) => HwpxErrorCode::Io,
152 Self::Core(_) => HwpxErrorCode::Core,
153 Self::Foundation(_) => HwpxErrorCode::Foundation,
154 Self::XmlSerialize { .. } => HwpxErrorCode::XmlSerialize,
155 }
156 }
157}
158
159pub type HwpxResult<T> = Result<T, HwpxError>;
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn zip_error_display() {
168 let err = HwpxError::Zip("corrupt archive".into());
169 assert_eq!(err.to_string(), "ZIP error: corrupt archive");
170 assert_eq!(err.code(), HwpxErrorCode::Zip);
171 }
172
173 #[test]
174 fn invalid_mimetype_display() {
175 let err = HwpxError::InvalidMimetype { actual: "application/zip".into() };
176 let msg = err.to_string();
177 assert!(msg.contains("application/hwp+zip"));
178 assert!(msg.contains("application/zip"));
179 assert_eq!(err.code(), HwpxErrorCode::InvalidMimetype);
180 }
181
182 #[test]
183 fn missing_file_display() {
184 let err = HwpxError::MissingFile { path: "Contents/header.xml".into() };
185 assert!(err.to_string().contains("header.xml"));
186 assert_eq!(err.code(), HwpxErrorCode::MissingFile);
187 }
188
189 #[test]
190 fn xml_parse_display() {
191 let err = HwpxError::XmlParse {
192 file: "Contents/section0.xml".into(),
193 detail: "unexpected element 'foo'".into(),
194 };
195 let msg = err.to_string();
196 assert!(msg.contains("section0.xml"));
197 assert!(msg.contains("unexpected element"));
198 assert_eq!(err.code(), HwpxErrorCode::XmlParse);
199 }
200
201 #[test]
202 fn invalid_attribute_display() {
203 let err = HwpxError::InvalidAttribute {
204 element: "hh:charPr".into(),
205 attribute: "height".into(),
206 value: "abc".into(),
207 };
208 let msg = err.to_string();
209 assert!(msg.contains("hh:charPr"));
210 assert!(msg.contains("height"));
211 assert!(msg.contains("abc"));
212 assert_eq!(err.code(), HwpxErrorCode::InvalidAttribute);
213 }
214
215 #[test]
216 fn index_out_of_bounds_display() {
217 let err = HwpxError::IndexOutOfBounds { kind: "charPrIDRef", index: 99, max: 5 };
218 let msg = err.to_string();
219 assert!(msg.contains("charPrIDRef"));
220 assert!(msg.contains("99"));
221 assert!(msg.contains("5"));
222 assert_eq!(err.code(), HwpxErrorCode::IndexOutOfBounds);
223 }
224
225 #[test]
226 fn invalid_structure_display() {
227 let err = HwpxError::InvalidStructure { detail: "section has no paragraphs".into() };
228 assert!(err.to_string().contains("no paragraphs"));
229 assert_eq!(err.code(), HwpxErrorCode::InvalidStructure);
230 }
231
232 #[test]
233 fn io_error_conversion() {
234 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
235 let err: HwpxError = io_err.into();
236 assert_eq!(err.code(), HwpxErrorCode::Io);
237 assert!(err.to_string().contains("file not found"));
238 }
239
240 #[test]
241 fn error_code_display() {
242 assert_eq!(HwpxErrorCode::Zip.to_string(), "E4000");
243 assert_eq!(HwpxErrorCode::InvalidMimetype.to_string(), "E4001");
244 assert_eq!(HwpxErrorCode::MissingFile.to_string(), "E4002");
245 assert_eq!(HwpxErrorCode::XmlParse.to_string(), "E4003");
246 assert_eq!(HwpxErrorCode::InvalidAttribute.to_string(), "E4004");
247 assert_eq!(HwpxErrorCode::IndexOutOfBounds.to_string(), "E4005");
248 assert_eq!(HwpxErrorCode::InvalidStructure.to_string(), "E4006");
249 assert_eq!(HwpxErrorCode::Io.to_string(), "E4007");
250 assert_eq!(HwpxErrorCode::Core.to_string(), "E4008");
251 assert_eq!(HwpxErrorCode::Foundation.to_string(), "E4009");
252 assert_eq!(HwpxErrorCode::XmlSerialize.to_string(), "E4010");
253 }
254
255 #[test]
256 fn xml_serialize_error_display() {
257 let err = HwpxError::XmlSerialize { detail: "missing field".into() };
258 assert!(err.to_string().contains("missing field"));
259 assert_eq!(err.code(), HwpxErrorCode::XmlSerialize);
260 }
261
262 #[test]
263 fn error_codes_are_in_4000_range() {
264 let codes = [
265 HwpxErrorCode::Zip,
266 HwpxErrorCode::InvalidMimetype,
267 HwpxErrorCode::MissingFile,
268 HwpxErrorCode::XmlParse,
269 HwpxErrorCode::InvalidAttribute,
270 HwpxErrorCode::IndexOutOfBounds,
271 HwpxErrorCode::InvalidStructure,
272 HwpxErrorCode::Io,
273 HwpxErrorCode::Core,
274 HwpxErrorCode::Foundation,
275 HwpxErrorCode::XmlSerialize,
276 ];
277 for code in codes {
278 let val = code as u16;
279 assert!((4000..4100).contains(&val), "code {val} not in 4000-4099");
280 }
281 }
282
283 #[test]
284 fn hwpx_result_type_alias_works() {
285 fn example() -> HwpxResult<u32> {
286 Ok(42)
287 }
288 assert_eq!(example().unwrap(), 42);
289 }
290
291 #[test]
292 fn hwpx_result_err_path() {
293 fn example() -> HwpxResult<u32> {
294 Err(HwpxError::Zip("test".into()))
295 }
296 assert!(example().is_err());
297 }
298
299 #[test]
300 fn error_is_send_and_sync() {
301 fn assert_send<T: Send>() {}
302 fn assert_sync<T: Sync>() {}
303 assert_send::<HwpxError>();
304 assert_sync::<HwpxError>();
305 }
306
307 #[test]
308 fn foundation_error_conversion() {
309 let fe = hwpforge_foundation::FoundationError::EmptyIdentifier { item: "FontId".into() };
310 let err: HwpxError = fe.into();
311 assert_eq!(err.code(), HwpxErrorCode::Foundation);
312 }
313}