From fd99d56cb087c24f5354fc3d3d46d463a47f64fb Mon Sep 17 00:00:00 2001 From: ololduck Date: Thu, 26 Sep 2024 19:32:49 +0200 Subject: [PATCH] feat: replace the use of eyre with our own error type This is done to enable users of our library to react to certain types without resorting to string comparison and parsing to understand what happened. This is done in accordance to eyre's docs, who disapprove of leaking the `Report` type in public interfaces. --- Cargo.toml | 6 +-- src/epub.rs | 39 +++++++++------ src/lib.rs | 59 ++++++++++++++++++++++- src/zip.rs | 2 +- src/zip_command.rs | 89 ++++++++++++++++++++++------------- src/zip_command_or_library.rs | 3 +- src/zip_library.rs | 46 +++++++++++------- 7 files changed, 173 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a853fcf..eb07465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,13 @@ zip-command = ["tempfile"] zip-library = ["libzip", "libzip/time"] [dependencies] -eyre = "0.6" +thiserror = "1.0.64" once_cell = "1" upon = "0.7" chrono = { version = "0.4", default-features = false, features = ["clock", "std", "wasmbind"] } uuid = { version = "1", features = ["v4"] } -tempfile = { version = "3", optional = true } -libzip = { version = "0.6", optional = true, default-features = false, features = ["deflate"], package = "zip"} +tempfile = { version = "3", optional = true } +libzip = { version = "0.6", optional = true, default-features = false, features = ["deflate"], package = "zip" } html-escape = "0.2" log = "0.4" diff --git a/src/epub.rs b/src/epub.rs index 9c4b357..3af0065 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -6,6 +6,7 @@ use crate::templates; use crate::toc::{Toc, TocElement}; use crate::zip::Zip; use crate::ReferenceType; +use crate::Result; use crate::{common, EpubContent}; use std::io; @@ -13,8 +14,6 @@ use std::io::Read; use std::path::Path; use std::str::FromStr; -use eyre::{bail, Context, Result}; - /// Represents the EPUB version. /// /// Currently, this library supports EPUB 2.0.1 and 3.0.1. @@ -47,15 +46,15 @@ impl ToString for PageDirection { } } -impl std::str::FromStr for PageDirection { - type Err = eyre::Report; +impl FromStr for PageDirection { + type Err = crate::Error; fn from_str(s: &str) -> std::result::Result { let s = s.to_lowercase(); match s.as_ref() { "rtl" => Ok(PageDirection::Rtl), "ltr" => Ok(PageDirection::Ltr), - _ => bail!("Invalid page direction: {}", s), + _ => Err(crate::Error::PageDirectionError(s)), } } } @@ -243,7 +242,7 @@ impl EpubBuilder { } "license" => self.metadata.license = Some(value.into()), "toc_name" => self.metadata.toc_name = value.into(), - s => bail!("invalid metadata '{}'", s), + s => Err(crate::Error::InvalidMetadataError(s.to_string()))?, } Ok(self) } @@ -269,16 +268,16 @@ impl EpubBuilder { } /// Tells whether fields should be HTML-escaped. - /// + /// /// * `true`: fields such as titles, description, and so on will be HTML-escaped everywhere (default) - /// * `false`: fields will be left as is (letting you in charge of making + /// * `false`: fields will be left as is (letting you in charge of making /// sure they do not contain anything illegal, e.g. < and > characters) pub fn escape_html(&mut self, val: bool) { self.escape_html = val; } /// Sets the language of the EPUB - /// + /// /// This is quite important as EPUB renderers rely on it /// for e.g. hyphenating words. pub fn set_lang>(&mut self, value: S) { @@ -649,7 +648,7 @@ impl EpubBuilder { items: common::indent(items.join("\n"), 2), // Not escaped: XML content itemrefs: common::indent(itemrefs.join("\n"), 2), // Not escaped: XML content date_modified: html_escape::encode_text(&date_modified.to_string()), - uuid: html_escape::encode_text(&uuid), + uuid: html_escape::encode_text(&uuid), guide: common::indent(guide.join("\n"), 2), // Not escaped: XML content date_published: if let Some(date) = date_published { date.to_string() } else { String::new() }, } @@ -659,7 +658,12 @@ impl EpubBuilder { match self.version { EpubVersion::V20 => templates::v2::CONTENT_OPF.render(&data).to_writer(&mut res), EpubVersion::V30 => templates::v3::CONTENT_OPF.render(&data).to_writer(&mut res), - }.wrap_err("could not render template for content.opf")?; + } + .map_err(|e| crate::Error::TemplateError { + msg: "could not render template for content.opf".to_string(), + cause: e.into(), + })?; + //.wrap_err("could not render template for content.opf")?; Ok(res) } @@ -678,7 +682,10 @@ impl EpubBuilder { templates::TOC_NCX .render(&data) .to_writer(&mut res) - .wrap_err("error rendering toc.ncx template")?; + .map_err(|e| crate::Error::TemplateError { + msg: "error rendering toc.ncx template".to_string(), + cause: e.into(), + })?; Ok(res) } @@ -738,12 +745,16 @@ impl EpubBuilder { String::new() }, }; - + let mut res: Vec = vec![]; match self.version { EpubVersion::V20 => templates::v2::NAV_XHTML.render(&data).to_writer(&mut res), EpubVersion::V30 => templates::v3::NAV_XHTML.render(&data).to_writer(&mut res), - }.wrap_err("error rendering nav.xhtml template")?; + } + .map_err(|e| crate::Error::TemplateError { + msg: "error rendering nav.xhtml template".to_string(), + cause: e.into(), + })?; Ok(res) } } diff --git a/src/lib.rs b/src/lib.rs index 8da2a37..bcc8ee9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,6 +143,7 @@ pub use epub::EpubVersion; pub use epub::PageDirection; pub use epub_content::EpubContent; pub use epub_content::ReferenceType; +use libzip::result::ZipError; pub use toc::Toc; pub use toc::TocElement; #[cfg(feature = "zip-command")] @@ -153,5 +154,59 @@ pub use zip_command_or_library::ZipCommandOrLibrary; #[cfg(feature = "libzip")] pub use zip_library::ZipLibrary; -/// Re-exports the result type used across the library. -pub use eyre::Result; +/// Error type of this crate. Each variant represent a type of event that may happen during this crate's operations. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// An error caused while processing a template or its rendering. + #[error("{msg}: {cause:?}")] + TemplateError { + /// A message explaining what was happening when we recieved this error. + msg: String, + /// The root cause of the error. + // Box the error, since it is quite large (at least 136 bytes, thanks clippy!) + cause: Box, + }, + /// An error returned when encountering an unknown [`PageDirection`]. + #[error("Invalid page direction specification: {0}")] + PageDirectionError(String), + /// An error returned when an unknown metadata key has been encountered. + #[error("Invalid metadata key: {0}")] + InvalidMetadataError(String), + /// An error returned when attempting to access the filesystem + #[error("{msg}: {cause:?}")] + IoError { + /// A message explaining what was happening when we recieved this error. + msg: String, + /// The root cause of the error. + cause: std::io::Error, + }, + /// An error returned when something happened while invoking a zip program. See [`ZipCommand`]. + #[error("Error while executing zip command: {0}")] + ZipCommandError(String), + /// An error returned when the zip library itself returned an error. See [`ZipLibrary`]. + #[error(transparent)] + ZipError(#[from] ZipError), + /// An error returned when the zip library itself returned an error, but with an additional message. See [`ZipLibrary`]. + #[error("{msg}: {cause:?}")] + ZipErrorWithMessage { + /// A message explaining what was happening when we recieved this error. + msg: String, + /// The root cause of the error. + cause: ZipError, + }, + /// An error returned when an invalid [`Path`] has been encountered during epub processing. + #[error("Invalid path: {0}")] + InvalidPath(String), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Error::IoError { + msg: format!("{value:?}"), + cause: value, + } + } +} + +/// A more convenient shorthand for functions returning an error in this crate. +pub type Result = std::result::Result; diff --git a/src/zip.rs b/src/zip.rs index 4d95fb8..5f31aa9 100644 --- a/src/zip.rs +++ b/src/zip.rs @@ -6,7 +6,7 @@ use std::io::Read; use std::io::Write; use std::path::Path; -use eyre::Result; +use crate::Result; /// An abstraction over possible Zip implementations. /// diff --git a/src/zip_command.rs b/src/zip_command.rs index 207e934..e56ce21 100644 --- a/src/zip_command.rs +++ b/src/zip_command.rs @@ -3,6 +3,7 @@ // this file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::zip::Zip; +use crate::Result; use std::fs; use std::fs::DirBuilder; @@ -14,10 +15,6 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; -use eyre::bail; -use eyre::Context; -use eyre::Result; - /// Zip files using the system `zip` command. /// /// Create a temporary directory, write temp files in that directory, and then @@ -37,7 +34,10 @@ pub struct ZipCommand { impl ZipCommand { /// Creates a new ZipCommand, using default setting to create a temporary directory. pub fn new() -> Result { - let temp_dir = tempfile::TempDir::new().wrap_err("could not create temporary directory")?; + let temp_dir = tempfile::TempDir::new().map_err(|e| crate::Error::IoError { + msg: "could not create temporary directory".to_string(), + cause: e, + })?; let zip = ZipCommand { command: String::from("zip"), temp_dir, @@ -51,8 +51,10 @@ impl ZipCommand { /// # Arguments /// * `temp_path`: the path where a temporary directory should be created. pub fn new_in>(temp_path: P) -> Result { - let temp_dir = - tempfile::TempDir::new_in(temp_path).wrap_err("could not create temporary directory")?; + let temp_dir = tempfile::TempDir::new_in(temp_path).map_err(|e| crate::Error::IoError { + msg: "could not create temporary directory".to_string(), + cause: e, + })?; let zip = ZipCommand { command: String::from("zip"), temp_dir, @@ -73,13 +75,16 @@ impl ZipCommand { .current_dir(self.temp_dir.path()) .arg("-v") .output() - .wrap_err_with(|| format!("failed to run command {name}", name = self.command))?; + .map_err(|e| crate::Error::IoError { + msg: format!("failed to run command {name}", name = self.command), + cause: e, + })?; if !output.status.success() { - bail!( - "command {name} didn't return successfully: {output}", + return Err(crate::Error::ZipCommandError(format!( + "command {name} did not exit successfully: {output}", name = self.command, output = String::from_utf8_lossy(&output.stderr) - ); + ))); } Ok(()) } @@ -93,25 +98,28 @@ impl ZipCommand { DirBuilder::new() .recursive(true) .create(dest_dir) - .wrap_err_with(|| { - format!( + .map_err(|e| crate::Error::IoError { + msg: format!( "could not create temporary directory in {path}", path = dest_dir.display() - ) + ), + cause: e, })?; } - let mut f = File::create(&dest_file).wrap_err_with(|| { - format!( + let mut f = File::create(&dest_file).map_err(|e| crate::Error::IoError { + msg: format!( "could not write to temporary file {file}", file = path.as_ref().display() - ) + ), + cause: e, })?; - io::copy(&mut content, &mut f).wrap_err_with(|| { - format!( + io::copy(&mut content, &mut f).map_err(|e| crate::Error::IoError { + msg: format!( "could not write to temporary file {file}", file = path.as_ref().display() - ) + ), + cause: e, })?; Ok(()) } @@ -121,11 +129,11 @@ impl Zip for ZipCommand { fn write_file, R: Read>(&mut self, path: P, content: R) -> Result<()> { let path = path.as_ref(); if path.starts_with("..") || path.is_absolute() { - bail!( + return Err(crate::Error::InvalidPath(format!( "file {} refers to a path outside the temporary directory. This is \ verbotten!", path.display() - ); + ))); } self.add_to_tmp_dir(path, content)?; @@ -142,13 +150,18 @@ impl Zip for ZipCommand { .arg("output.epub") .arg("mimetype") .output() - .wrap_err_with(|| format!("failed to run command {name}", name = self.command))?; + .map_err(|e| { + crate::Error::ZipCommandError(format!( + "failed to run command {name}: {e:?}", + name = self.command + )) + })?; if !output.status.success() { - bail!( + return Err(crate::Error::ZipCommandError(format!( "command {name} didn't return successfully: {output}", name = self.command, output = String::from_utf8_lossy(&output.stderr) - ); + ))); } let mut command = Command::new(&self.command); @@ -160,20 +173,30 @@ impl Zip for ZipCommand { command.arg(format!("{}", file.display())); } - let output = command - .output() - .wrap_err_with(|| format!("failed to run command {name}", name = self.command))?; + let output = command.output().map_err(|e| { + crate::Error::ZipCommandError(format!( + "failed to run command {name}: {e:?}", + name = self.command + )) + })?; if output.status.success() { - let mut f = File::open(self.temp_dir.path().join("output.epub")) - .wrap_err("error reading temporary epub file")?; - io::copy(&mut f, &mut to).wrap_err("error writing result of the zip command")?; + let mut f = File::open(self.temp_dir.path().join("output.epub")).map_err(|e| { + crate::Error::IoError { + msg: "error reading temporary epub file".to_string(), + cause: e, + } + })?; + io::copy(&mut f, &mut to).map_err(|e| crate::Error::IoError { + msg: "error writing result of the zip command".to_string(), + cause: e, + })?; Ok(()) } else { - bail!( + Err(crate::Error::ZipCommandError(format!( "command {name} didn't return successfully: {output}", name = self.command, output = String::from_utf8_lossy(&output.stderr) - ); + ))) } } } diff --git a/src/zip_command_or_library.rs b/src/zip_command_or_library.rs index 6107957..60b843f 100644 --- a/src/zip_command_or_library.rs +++ b/src/zip_command_or_library.rs @@ -2,9 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with // this file, You can obtain one at https://mozilla.org/MPL/2.0/. -use eyre::Result; - use crate::zip::Zip; +use crate::Result; use crate::ZipCommand; use crate::ZipLibrary; diff --git a/src/zip_library.rs b/src/zip_library.rs index dda137e..73d708c 100644 --- a/src/zip_library.rs +++ b/src/zip_library.rs @@ -11,8 +11,7 @@ use std::io::Read; use std::io::Write; use std::path::Path; -use eyre::Context; -use eyre::Result; +use crate::Result; use libzip::write::FileOptions; use libzip::CompressionMethod; use libzip::ZipWriter; @@ -43,15 +42,16 @@ impl ZipLibrary { let mut writer = ZipWriter::new(Cursor::new(vec![])); writer.set_comment(""); // Fix issues with some readers - writer - .start_file( - "mimetype", - FileOptions::default().compression_method(CompressionMethod::Stored), - ) - .wrap_err("could not create mimetype in epub")?; + writer.start_file( + "mimetype", + FileOptions::default().compression_method(CompressionMethod::Stored), + )?; writer .write(b"application/epub+zip") - .wrap_err("could not write mimetype in epub")?; + .map_err(|e| crate::Error::IoError { + msg: "could not write mimetype in epub".to_string(), + cause: e, + })?; Ok(ZipLibrary { writer }) } @@ -65,19 +65,33 @@ impl Zip for ZipLibrary { file = file.replace('\\', "/"); } let options = FileOptions::default(); - self.writer - .start_file(file.clone(), options) - .wrap_err_with(|| format!("could not create file '{}' in epub", file))?; - io::copy(&mut content, &mut self.writer) - .wrap_err_with(|| format!("could not write file '{}' in epub", file))?; + self.writer.start_file(file.clone(), options).map_err(|e| { + crate::Error::ZipErrorWithMessage { + msg: format!("could not create file '{}' in epub", file), + cause: e, + } + })?; + io::copy(&mut content, &mut self.writer).map_err(|e| crate::Error::IoError { + msg: format!("could not write file '{}' in epub", file), + cause: e, + })?; Ok(()) } fn generate(&mut self, mut to: W) -> Result<()> { - let cursor = self.writer.finish().wrap_err("error writing zip file")?; + let cursor = self + .writer + .finish() + .map_err(|e| crate::Error::ZipErrorWithMessage { + msg: "error writing zip file".to_string(), + cause: e, + })?; let bytes = cursor.into_inner(); to.write_all(bytes.as_ref()) - .wrap_err("error writing zip file")?; + .map_err(|e| crate::Error::IoError { + msg: "error writing to file".to_string(), + cause: e, + })?; Ok(()) } }