1use thiserror::Error;
8
9#[derive(Debug, Error)]
15pub enum EpubError {
16 #[error("Archive error: {source}")]
21 ArchiveError { source: zip::result::ZipError },
22
23 #[error("Decode error: The data is empty.")]
28 EmptyDataError,
29
30 #[cfg(feature = "builder")]
31 #[error("Epub builder error: {source}")]
32 EpubBuilderError { source: EpubBuilderError },
33
34 #[error(
40 "Failed parsing XML error: Unknown problems occurred during XML parsing, causing parsing failure."
41 )]
42 FailedParsingXml,
43
44 #[error("IO error: {source}")]
45 IOError { source: std::io::Error },
46
47 #[error(
52 "Missing required attribute: The \"{attribute}\" attribute is a must attribute for the \"{tag}\" element."
53 )]
54 MissingRequiredAttribute { tag: String, attribute: String },
55
56 #[error("Mutex error: Mutex was poisoned.")]
61 MutexError,
62
63 #[error("Non-canonical epub: The \"{expected_file}\" file was not found.")]
68 NonCanonicalEpub { expected_file: String },
69
70 #[error("Non-canonical file: The \"{tag}\" elements was not found.")]
75 NonCanonicalFile { tag: String },
76
77 #[error(
82 "No supported file format: The fallback resource does not contain the file format you support."
83 )]
84 NoSupportedFileFormat,
85
86 #[error("Relative link leakage: Path \"{path}\" is out of container range.")]
91 RelativeLinkLeakage { path: String },
92
93 #[error("Resource Id Not Exist: There is no resource item with id \"{id}\".")]
97 ResourceIdNotExist { id: String },
98
99 #[error("Resource not found: Unable to find resource from \"{resource}\".")]
104 ResourceNotFound { resource: String },
105
106 #[error(
111 "Unrecognized EPUB version: Unable to identify version number and version characteristics from epub file"
112 )]
113 UnrecognizedEpubVersion,
114
115 #[error("Unsupported encryption method: The \"{method}\" encryption method is not supported.")]
124 UnsupportedEncryptedMethod { method: String },
125
126 #[error(
130 "Unusable compression method: The \"{file}\" file uses the unsupported \"{method}\" compression method."
131 )]
132 UnusableCompressionMethod { file: String, method: String },
133
134 #[error("Decode error: {source}")]
139 Utf8DecodeError { source: std::string::FromUtf8Error },
140
141 #[error("Decode error: {source}")]
146 Utf16DecodeError { source: std::string::FromUtf16Error },
147
148 #[cfg(feature = "builder")]
152 #[error("WalkDir error: {source}")]
153 WalkDirError { source: walkdir::Error },
154
155 #[error("QuickXml error: {source}")]
159 QuickXmlError { source: quick_xml::Error },
160}
161
162impl From<zip::result::ZipError> for EpubError {
163 fn from(value: zip::result::ZipError) -> Self {
164 EpubError::ArchiveError { source: value }
165 }
166}
167
168impl From<quick_xml::Error> for EpubError {
169 fn from(value: quick_xml::Error) -> Self {
170 EpubError::QuickXmlError { source: value }
171 }
172}
173
174impl From<std::io::Error> for EpubError {
175 fn from(value: std::io::Error) -> Self {
176 EpubError::IOError { source: value }
177 }
178}
179
180impl From<std::string::FromUtf8Error> for EpubError {
181 fn from(value: std::string::FromUtf8Error) -> Self {
182 EpubError::Utf8DecodeError { source: value }
183 }
184}
185
186impl From<std::string::FromUtf16Error> for EpubError {
187 fn from(value: std::string::FromUtf16Error) -> Self {
188 EpubError::Utf16DecodeError { source: value }
189 }
190}
191
192impl<T> From<std::sync::PoisonError<T>> for EpubError {
193 fn from(_value: std::sync::PoisonError<T>) -> Self {
194 EpubError::MutexError
195 }
196}
197
198#[cfg(feature = "builder")]
199impl From<EpubBuilderError> for EpubError {
200 fn from(value: EpubBuilderError) -> Self {
201 EpubError::EpubBuilderError { source: value }
202 }
203}
204
205#[cfg(feature = "builder")]
206impl From<walkdir::Error> for EpubError {
207 fn from(value: walkdir::Error) -> Self {
208 EpubError::WalkDirError { source: value }
209 }
210}
211
212#[cfg(test)]
213impl PartialEq for EpubError {
214 fn eq(&self, other: &Self) -> bool {
215 match (self, other) {
216 (
217 Self::MissingRequiredAttribute { tag: l_tag, attribute: l_attribute },
218 Self::MissingRequiredAttribute { tag: r_tag, attribute: r_attribute },
219 ) => l_tag == r_tag && l_attribute == r_attribute,
220
221 (
222 Self::NonCanonicalEpub { expected_file: l_expected_file },
223 Self::NonCanonicalEpub { expected_file: r_expected_file },
224 ) => l_expected_file == r_expected_file,
225
226 (Self::NonCanonicalFile { tag: l_tag }, Self::NonCanonicalFile { tag: r_tag }) => {
227 l_tag == r_tag
228 }
229
230 (
231 Self::RelativeLinkLeakage { path: l_path },
232 Self::RelativeLinkLeakage { path: r_path },
233 ) => l_path == r_path,
234
235 (Self::ResourceIdNotExist { id: l_id }, Self::ResourceIdNotExist { id: r_id }) => {
236 l_id == r_id
237 }
238
239 (
240 Self::ResourceNotFound { resource: l_resource },
241 Self::ResourceNotFound { resource: r_resource },
242 ) => l_resource == r_resource,
243
244 (
245 Self::UnsupportedEncryptedMethod { method: l_method },
246 Self::UnsupportedEncryptedMethod { method: r_method },
247 ) => l_method == r_method,
248
249 (
250 Self::UnusableCompressionMethod { file: l_file, method: l_method },
251 Self::UnusableCompressionMethod { file: r_file, method: r_method },
252 ) => l_file == r_file && l_method == r_method,
253
254 (
255 Self::Utf8DecodeError { source: l_source },
256 Self::Utf8DecodeError { source: r_source },
257 ) => l_source == r_source,
258
259 #[cfg(feature = "builder")]
260 (
261 Self::EpubBuilderError { source: l_source },
262 Self::EpubBuilderError { source: r_source },
263 ) => l_source == r_source,
264
265 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
266 }
267 }
268}
269
270#[cfg(feature = "builder")]
277#[derive(Debug, Error)]
278#[cfg_attr(test, derive(PartialEq))]
279pub enum EpubBuilderError {
280 #[error(
287 "A manifest with id '{manifest_id}' should not use a relative path starting with '../'."
288 )]
289 IllegalManifestPath { manifest_id: String },
290
291 #[error("A rootfile path should be a relative path and not start with '../'.")]
297 IllegalRootfilePath,
298
299 #[error("The footnote locate must be in the range of [0, {max_locate}].")]
303 InvalidFootnoteLocate { max_locate: usize },
304
305 #[error("{error}")]
309 InvalidMathMLFormat { error: String },
310
311 #[error("The '{target_path}' target path is invalid.")]
316 InvalidTargetPath { target_path: String },
317
318 #[error("Circular reference detected in fallback chain for '{fallback_chain}'.")]
322 ManifestCircularReference { fallback_chain: String },
323
324 #[error("Fallback resource '{manifest_id}' does not exist in manifest.")]
328 ManifestNotFound { manifest_id: String },
329
330 #[error("Requires at least one 'title', 'language', and 'identifier' with id 'pub-id'.")]
335 MissingNecessaryMetadata,
336
337 #[error("The block '{block_type}' is missing necessary data '{missing_data}'")]
341 MissingNecessaryBlockData {
342 block_type: String,
343 missing_data: String,
344 },
345
346 #[error("Navigation information is not set.")]
350 NavigationInfoUninitalized,
351
352 #[error("The file format is not current block expected.")]
356 NotExpectedFileFormat,
357
358 #[error("Need at least one rootfile.")]
362 MissingRootfile,
363
364 #[error("Spine item '{idref}' references a manifest item that does not exist.")]
370 SpineManifestNotFound { idref: String },
371
372 #[error("Expect a file, but '{target_path}' is not a file.")]
376 TargetIsNotFile { target_path: String },
377
378 #[error("There are too many items with 'nav' property in the manifest.")]
384 TooManyNavFlags,
385
386 #[error("Unable to analyze the file '{file_path}' type.")]
390 UnknownFileFormat { file_path: String },
391}
392
393#[cfg(test)]
394mod from_trait_tests {
395 use zip::result::ZipError;
396
397 use std::io;
398
399 use super::*;
400
401 #[test]
402 fn test_from_zip_error() {
403 let zip_err = ZipError::FileNotFound;
404 let epub_err = zip_err.into();
405
406 match epub_err {
407 EpubError::ArchiveError { source } => {
408 assert!(matches!(source, ZipError::FileNotFound));
409 }
410 _ => panic!("Expected EpubError::ArchiveError"),
411 }
412 }
413
414 #[test]
415 fn test_from_quick_xml_error() {
416 let io_err = io::Error::new(io::ErrorKind::InvalidData, "xml parse error");
417 let xml_err = quick_xml::Error::Io(io_err.into());
418 let epub_err = xml_err.into();
419
420 match epub_err {
421 EpubError::QuickXmlError { source } => {
422 assert!(format!("{}", source).contains("xml parse error"));
423 }
424 _ => panic!("Expected EpubError::QuickXmlError"),
425 }
426 }
427
428 #[test]
429 fn test_from_io_error() {
430 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
431 let epub_err = io_err.into();
432
433 match epub_err {
434 EpubError::IOError { source } => {
435 assert_eq!(source.kind(), io::ErrorKind::NotFound);
436 }
437 _ => panic!("Expected EpubError::IOError"),
438 }
439 }
440
441 #[test]
442 fn test_from_utf8_error() {
443 let invalid_utf8 = vec![0x80, 0x81];
444 let utf8_err = String::from_utf8(invalid_utf8).unwrap_err();
445 let epub_err = utf8_err.into();
446
447 match epub_err {
448 EpubError::Utf8DecodeError { .. } => {}
449 _ => panic!("Expected EpubError::Utf8DecodeError"),
450 }
451 }
452
453 #[test]
454 fn test_from_utf16_error() {
455 let invalid_utf16 = vec![0xD800];
456 let utf16_err = String::from_utf16(&invalid_utf16).unwrap_err();
457 let epub_err = utf16_err.into();
458
459 match epub_err {
460 EpubError::Utf16DecodeError { .. } => {}
461 _ => panic!("Expected EpubError::Utf16DecodeError"),
462 }
463 }
464
465 #[test]
466 fn test_from_poison_error() {
467 use std::sync::{Arc, Mutex};
468 use std::thread;
469
470 let mutex = Arc::new(Mutex::new(42));
471
472 let mutex_clone = Arc::clone(&mutex);
473 let handle = thread::spawn(move || {
474 let _guard = mutex_clone.lock().unwrap();
475 panic!("panic to poison mutex");
476 });
477
478 let _ = handle.join();
479
480 let result = mutex.lock();
481 assert!(result.is_err());
482
483 if let Err(poison_err) = result {
484 let epub_err: EpubError = poison_err.into();
485 assert!(matches!(epub_err, EpubError::MutexError));
486 }
487 }
488
489 #[cfg(feature = "builder")]
490 #[test]
491 fn test_from_epub_builder_error() {
492 let builder_err = EpubBuilderError::MissingRootfile;
493 let epub_err: EpubError = builder_err.into();
494
495 match epub_err {
496 EpubError::EpubBuilderError { .. } => {}
497 _ => panic!("Expected EpubError::EpubBuilderError"),
498 }
499 }
500
501 #[cfg(feature = "builder")]
502 #[test]
503 fn test_from_walkdir_error() {
504 let walk_err = walkdir::WalkDir::new("nonexistent_path_12345")
505 .into_iter()
506 .next()
507 .unwrap()
508 .unwrap_err();
509 let epub_err: EpubError = walk_err.into();
510
511 match epub_err {
512 EpubError::WalkDirError { .. } => {}
513 _ => panic!("Expected EpubError::WalkDirError"),
514 }
515 }
516}