From 053565c4fe24891ded14efbbe39b353532c421e5 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 22:38:01 +0100 Subject: [PATCH 01/23] Move lots of functionality under lib.rs --- src/cli.rs | 1 + src/client.rs | 75 +++++++++++++++++++++++++++++++++++--------- src/client/config.rs | 12 +++++++ src/crc32.rs | 9 +++++- src/install.rs | 41 +++++++++++++++++------- src/lib.rs | 32 +++++++++++++++++++ src/main.rs | 21 ++++++------- src/messages.rs | 25 +++++++++++++++ src/products.rs | 2 ++ src/progressbar.rs | 17 ++++++++++ src/shasums.rs | 24 +++++++++++--- src/signature.rs | 26 ++++++++++++--- src/tmpfile.rs | 25 ++++++++++++--- 13 files changed, 259 insertions(+), 51 deletions(-) create mode 100644 src/lib.rs diff --git a/src/cli.rs b/src/cli.rs index 93b9f57..f5f1d0a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -96,6 +96,7 @@ fn is_valid_install_dir(s: &str) -> Result { Ok(path.to_path_buf()) } +#[allow(clippy::too_many_lines)] fn create_app() -> Command { let app = Command::new(crate_name!()) .version(crate_version!()) diff --git a/src/client.rs b/src/client.rs index 55d3ef9..4ee163a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -26,6 +26,9 @@ const USER_AGENT: &str = concat!( env!("CARGO_PKG_VERSION"), ); +/// A [`Client`] for downloading [HashiCorp](https://www.hashicorp.com) +/// products. +#[derive(Debug)] pub struct Client { api_url: String, client: reqwest::Client, @@ -33,8 +36,13 @@ pub struct Client { } impl Client { - // Return a new reqwest client with our user-agent + /// Creates a new [`Client`] with the given [`ClientConfig`]. + /// + /// # Errors + /// + /// Errors if failing to build the [`reqwest::Client`]. pub fn new(config: ClientConfig) -> Result { + // Get a new reqwest client with our user-agent let client = reqwest::ClientBuilder::new() .gzip(true) .user_agent(USER_AGENT) @@ -49,7 +57,15 @@ impl Client { Ok(client) } - // Version check the given product via the checkpoint API + /// Checks the current version of the given `product` against the + /// [HashiCorp](https://www.hashicorp.com) checkpoint API. + /// + /// # Errors + /// + /// Errors if: + /// - Failing to parse the created checkpoint URL + /// - Failing to get the product version + /// - Failing to create a [`ProductVersion`] pub async fn check_version(&self, product: &str) -> Result { // We to_string here for the test scenario. #![allow(clippy::to_string_in_format_args)] @@ -69,8 +85,19 @@ impl Client { Ok(resp) } - // Download from the given URL to the output file. - pub async fn download(&self, url: Url, tmpfile: &mut TmpFile) -> Result<()> { + /// Downloads content from the given `url` to `tmpfile`. + /// + /// # Errors + /// + /// Errors if: + /// - Failing to make a request to the given `url` + /// - Failing to download the content from the given `url` + /// - Failing to write the downloaded content to the `tmpfile` + pub async fn download( + &self, + url: Url, + tmpfile: &mut TmpFile, + ) -> Result<()> { let file = tmpfile.handle()?; // Start the GET and attempt to get a content-length @@ -98,8 +125,8 @@ impl Client { Ok(()) } - // Perform an HTTP GET on the given URL - pub async fn get(&self, url: Url) -> Result { + /// Perform an HTTP GET on the given `url`. + async fn get(&self, url: Url) -> Result { let resp = self.client .get(url) .send() @@ -108,8 +135,9 @@ impl Client { Ok(resp) } - // Perform an HTTP GET on the given URL and return the result as Bytes - pub async fn get_bytes(&self, url: Url) -> Result { + /// Perform an HTTP GET on the given `url` and return the result as + /// [`Bytes`]. + async fn get_bytes(&self, url: Url) -> Result { let resp = self.get(url) .await? .bytes() @@ -118,8 +146,9 @@ impl Client { Ok(resp) } - // Perform an HTTP GET on the given URL and return the result as a String - pub async fn get_text(&self, url: Url) -> Result { + /// Perform an HTTP GET on the given `url` and return the result as a + /// `String`. + async fn get_text(&self, url: Url) -> Result { let resp = self.get(url) .await? .text() @@ -128,7 +157,12 @@ impl Client { Ok(resp) } - // Get the shasums for the given product version and return a new Shasums. + /// Get the checksums for the given [`ProductVersion`] and return a new + /// [`Shasums`]. + /// + /// # Errors + /// + /// Errors when failing to get the shasum file. pub async fn get_shasums( &self, version: &ProductVersion, @@ -140,8 +174,14 @@ impl Client { Ok(shasums) } - // Get the signature for the given ProductVersion and return a new - // Signature. + /// Get the signature for the given [`ProductVersion`] and return a new + /// [`Signature`]. + /// + /// # Errors + /// + /// Errors if: + /// - Failing to get the shasums signature + /// - Failing to create a [`Signature`] pub async fn get_signature( &self, version: &ProductVersion, @@ -153,7 +193,14 @@ impl Client { Ok(signature) } - // Get the ProductVersion for a given product and version. + /// Get the [`ProductVersion`] for a given `product` and `version`. + /// + /// # Errors + /// + /// Errors if: + /// - Failing to get the version from the remote server + /// - Failing to deserialize the obtained version into a + /// [`ProductVersion`] pub async fn get_version( &self, product: &str, diff --git a/src/client/config.rs b/src/client/config.rs index 7bb0771..2ce6dd6 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,20 +1,32 @@ // Client configuration + +/// [`ClientConfig`] is a configuration for [`Client`]. #[derive(Debug, Default)] pub struct ClientConfig { + /// Control the output of colour in the crate messages and progress bars. pub no_color: bool, + + /// Controls the output of text in the crate. pub quiet: bool, } impl ClientConfig { + /// Create a new [`ClientConfig`] with the default settings. + #[must_use] pub fn new() -> Self { Self::default() } + /// `no_color` controls the output of colours in the various output of the + /// crate. + #[must_use] pub fn no_color(mut self, no_color: bool) -> Self { self.no_color = no_color; self } + /// `quiet` controls the various text output of the crate. + #[must_use] pub fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self diff --git a/src/crc32.rs b/src/crc32.rs index d392ecd..6a460f8 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -15,7 +15,14 @@ use std::path::Path; // Buffer size, 256KiB const BUFFER_SIZE: usize = 256 * 1_024; -// Check the given `path`'s CRC32 against the `expected` CRC32. +/// Check the given `path`'s CRC32 against the `expected` CRC32. +/// +/// # Errors +/// +/// Errors if: +/// - Failing to open the given `path` +/// - Failing to read from the given `path` +/// - If the CRC32 of the given `path` doesn't match the `expected` value pub fn check

(path: P, expected: u32) -> Result<()> where P: AsRef, diff --git a/src/install.rs b/src/install.rs index 237ed70..255707c 100644 --- a/src/install.rs +++ b/src/install.rs @@ -2,7 +2,7 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] use super::crc32; -use super::Messages; +use super::messages::Messages; use anyhow::{ anyhow, Result, @@ -32,7 +32,13 @@ use std::fs::Permissions; #[cfg(target_family = "unix")] use std::os::unix::fs::PermissionsExt; -// Find a suitable bindir for installing to. +/// `bin_dir` finds a suitable executable directory to install a product to. +/// +/// # Errors +/// +/// Errors if: +/// - Failing to find a suitable executable directory +/// - Failing to create the executable directory if needed pub fn bin_dir() -> Result { if let Some(dir) = dirs::executable_dir() { // Attempt to create the directory if it doesn't exist @@ -53,9 +59,10 @@ pub fn bin_dir() -> Result { } } -// Extracts a given `zipfile` to a temporary file under `dir`. Also checks -// the CRC32 of the extracted file to make sure extraction was successful. -// Returns a TempPath which the caller is responsible for persisting. +/// Extracts a given `zipfile` to a temporary file under `dir`. Also checks +/// the CRC32 of the extracted file to make sure extraction was successful. +/// Returns a [`tempfile::TempPath`] which the caller is responsible for +/// persisting. fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { // Get a tempfile to extract to under the dest path let mut tmpfile = NamedTempFile::new_in(dir)?; @@ -73,8 +80,21 @@ fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { Ok(tmpfile) } -// Installs files from the gien `zipfile` under the directory at `dir`. -pub fn install(messages: &Messages, zipfile: &mut F, dir: &Path) -> Result<()> +/// Installs files from the given `zipfile` under the directory at `dir`. +/// +/// # Errors +/// +/// Can error if: +/// - Installation directory doesn't exist +/// - Failing to get a file index from the `zipfile` +/// - Failing to extract files from the `zipfile` +/// - Failing to persist the extracted file +/// - Failing to set file permissions on the extracted file +pub fn install( + messages: &Messages, + zipfile: &mut F, + dir: &Path, +) -> Result<()> where F: Read + Seek, { @@ -98,10 +118,9 @@ where let filename = file.name(); // Attempt to get the basename of the filename - let basename = Path::new(filename).file_name() - .ok_or_else(|| { - anyhow!("Couldn't get basename from: {}", filename) - })?; + let basename = Path::new(filename) + .file_name() + .ok_or_else(|| anyhow!("Couldn't get basename from: {filename}"))?; // Finally get a pathbuf of the basename let filename = Path::new(basename).to_path_buf(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0c07def --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,32 @@ +//! hcdl: Easily update Hashicorp tools +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::redundant_field_names)] + +/// Client for downloading products. +pub mod client; + +/// Handle checking the CRC32 of files extracted from zipfiles. +pub mod crc32; + +/// Handle extracting and installing downloaded product. +pub mod install; + +/// Various output messages. +pub mod messages; + +/// List of [HashiCorp](https://www.hashicorp.com/) products. +pub mod products; + +/// Handle drawing progress bars during download and install. +pub mod progressbar; + +/// Handle file checksums. +pub mod shasums; + +/// Handles for checking file signatures. +pub mod signature; + +/// Wrapper for handling a tempfile. +pub mod tmpfile; diff --git a/src/main.rs b/src/main.rs index d10be59..6164023 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,28 +4,25 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::redundant_field_names)] use anyhow::Result; +use hcdl::{ + client, + install, + products, + shasums, +}; +use hcdl::messages::Messages; +use hcdl::tmpfile::TmpFile; use std::path::PathBuf; use std::process::exit; mod cli; -mod client; -mod crc32; -mod install; -mod messages; -mod products; -mod progressbar; -mod shasums; -mod signature; -mod tmpfile; - -use messages::Messages; -use tmpfile::TmpFile; #[cfg(feature = "shell_completion")] use clap_complete::Shell; const LATEST: &str = "latest"; +#[allow(clippy::too_many_lines)] #[tokio::main] async fn main() -> Result<()> { let matches = cli::parse_args(); diff --git a/src/messages.rs b/src/messages.rs index 8531b4e..33a7c58 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -4,11 +4,15 @@ use anyhow::Error; use std::path::Path; +/// Handler for the various message we need to output. pub struct Messages { quiet: bool, } impl Messages { + /// Crates a new [`Messages`] handler. If `quiet` is set to `true`, no + /// output will be given. + #[must_use] pub fn new(quiet: bool) -> Self { Self { quiet: quiet, @@ -21,34 +25,41 @@ impl Messages { } } + #[allow(clippy::unused_self)] fn stderr(&self, msg: &str) { eprintln!("{msg}"); } + /// Output when the checksum of the file is bad. pub fn checksum_bad(&self, filename: &str) { let msg = format!("SHA256 of {filename} did not match."); self.stderr(&msg); } + /// Output when the checksum of the file is good. pub fn checksum_ok(&self, filename: &str) { let msg = format!("SHA256 of {filename} OK."); self.stdout(&msg); } + /// Output when the download of a file is starting. pub fn downloading(&self, filename: &str) { let msg = format!("Downloading {filename}..."); self.stdout(&msg); } + /// Output when download only mode is used to indicate the downloaded file + /// will not be deleted. pub fn download_only(&self, filename: &str) { let msg = format!("Download only mode, keeping {filename}."); self.stdout(&msg); } + /// Output when a file is being extracted. pub fn extracting_file(&self, filename: &Path, dest: &Path) { let msg = format!( "-> Extracting '{filename}' to '{dest}'...", @@ -59,34 +70,41 @@ impl Messages { self.stdout(&msg); } + /// Output when we can't find a product build for the specified OS and + /// architecture. pub fn find_build_failed(&self, os: &str, arch: &str) { let msg = format!("Could not find build for {os}-{arch}"); self.stderr(&msg); } + /// Output when a product installation has failed. pub fn installation_failed(&self, error: &Error) { let msg = format!("Installation failed with error: {error}"); self.stderr(&msg); } + /// Output when a product installation was successful. pub fn installation_successful(&self) { self.stdout("Installation successful."); } + /// Output when a zipfile has been kept instead of being deleted. pub fn keep_zipfile(&self, filename: &str) { let msg = format!("Keeping zipfile {filename} in current directory."); self.stdout(&msg); } + /// Output when checking for the latest product version. pub fn latest_version(&self, latest: &str) { let msg = format!("Latest version: {latest}"); self.stdout(&msg); } + /// Output when the product list was requested. pub fn list_products(&self, products: &[&str]) { let msg = format!( "Products: {products}", @@ -96,6 +114,8 @@ impl Messages { self.stdout(&msg); } + /// Output when an installation is attempted for a product OS that doesn't + /// match the current OS. pub fn os_mismatch(&self, os: &str, requested: &str) { let msg = format!( "Product downloaded for different OS, {os} != {requested}", @@ -104,18 +124,21 @@ impl Messages { self.stdout(&msg); } + /// Output when signature verification has failed. pub fn signature_verification_failed(&self, error: &Error) { let msg = format!("Verification failed, error: {error}"); self.stderr(&msg); } + /// Output when signature verification is successful. pub fn signature_verification_success(&self, signature: &str) { let msg = format!("Verified against {signature}."); self.stdout(&msg); } + /// Output when installation of the product is skipped. pub fn skipped_install(&self, filename: &str) { let msg = format!( "Skipping install and keeping zipfile '{filename}' in current \ @@ -125,6 +148,7 @@ impl Messages { self.stdout(&msg); } + /// Output when content is being unzipped. pub fn unzipping(&self, zipfile: &str, dest: &Path) { let msg = format!( "Unzipping contents of '{zipfile}' to '{dest}'", @@ -134,6 +158,7 @@ impl Messages { self.stdout(&msg); } + /// Output when a signature is being verified. pub fn verifying_signature(&self, shasums: &str) { let msg = format!( "Downloading and verifying signature of {shasums}...", diff --git a/src/products.rs b/src/products.rs index e2c7da4..607154d 100644 --- a/src/products.rs +++ b/src/products.rs @@ -2,6 +2,8 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] +/// A list of [HashiCorp](https://www.hashicorp.com/) products that this crate +/// can download. pub const PRODUCTS_LIST: &[&str] = &[ "consul", "nomad", diff --git a/src/progressbar.rs b/src/progressbar.rs index fc22c28..42f791d 100644 --- a/src/progressbar.rs +++ b/src/progressbar.rs @@ -31,6 +31,7 @@ const PROGRESS_TEMPLATE_NO_COLOR: &str = concat!( " {msg}", ); +/// A builder for [`ProgressBar`]. #[derive(Default)] pub struct ProgressBarBuilder { no_color: bool, @@ -39,25 +40,36 @@ pub struct ProgressBarBuilder { } impl ProgressBarBuilder { + /// Create a new [`ProgressBarBuilder`] with the default settings. + #[must_use] pub fn new() -> Self { Self::default() } + /// Disable the [`ProgressBar`] colours. + #[must_use] pub fn no_color(mut self, no_color: bool) -> Self { self.no_color = no_color; self } + /// Set quiet mode on the [`ProgressBar`], no output will be drawn. + #[must_use] pub fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self } + /// Set the size of the [`ProgressBar`]. + #[must_use] pub fn size(mut self, size: Option) -> Self { self.size = size; self } + /// Build and return the [`ProgressBar`]. + #[allow(clippy::missing_panics_doc)] + #[must_use] pub fn build(self) -> ProgressBar { // No progress bar for quiet mode. if self.quiet { @@ -79,6 +91,8 @@ impl ProgressBarBuilder { PROGRESS_TEMPLATE }; + // We shouldn't ever panic here, since our progress bar templates + // are not user provided, we've tested them. let style = ProgressStyle::default_bar() .template(template) .unwrap() @@ -107,15 +121,18 @@ impl ProgressBarBuilder { } } +/// A wrapper for an [`indicatif::ProgressBar`]. pub struct ProgressBar { bar: indicatif::ProgressBar, } impl ProgressBar { + /// Wraps the given writer with the [`ProgressBar`]. pub fn wrap_write(&self, writer: W) -> ProgressBarIter { self.bar.wrap_write(writer) } + /// Flags the [`ProgressBar`] as finished and prints a final message. pub fn finished(&self) { self.bar.finish_with_message(PROGRESS_FINISHED_MSG); } diff --git a/src/shasums.rs b/src/shasums.rs index bfbe508..fe4d83b 100644 --- a/src/shasums.rs +++ b/src/shasums.rs @@ -1,7 +1,7 @@ // shasums: Handle checking of files against shasums #![forbid(unsafe_code)] #![forbid(missing_docs)] -use super::TmpFile; +use crate::tmpfile::TmpFile; use anyhow::{ anyhow, Result, @@ -13,17 +13,24 @@ use sha2::{ use std::collections::HashMap; use std::io; +/// This enum represents the outcome of shasum verification. #[derive(Debug, Eq, PartialEq)] pub enum Checksum { - OK, + /// Sha checksum did not verify. Bad, + + /// Sha checksum verified correctly. + OK, } +/// [`Shasums`] represents a downloaded shasum file. pub struct Shasums { content: String, } impl Shasums { + /// Create a new [`Shasums`] from the given shasum content. + #[must_use] pub fn new(shasums: String) -> Self { Self { content: shasums, @@ -51,7 +58,15 @@ impl Shasums { hash } - // Check the shasum of the specified file + /// Check the shasum of the given `tmpfile` against our [`Shasums`] + /// content. + /// + /// # Errors + /// + /// Can error if: + /// - Failing to find the shasum for the `tmpfile` filename + /// - Failing to obtain a handle for the `tmpfile` + /// - Failing to hash the file content pub fn check(&self, tmpfile: &mut TmpFile) -> Result { let filename = tmpfile.filename(); let shasum = self.shasum(filename) @@ -74,7 +89,8 @@ impl Shasums { Ok(res) } - // Return a reference to the shasums + /// Return a reference to the [`Shasums`] content. + #[must_use] pub fn content(&self) -> &str { &self.content } diff --git a/src/signature.rs b/src/signature.rs index ebb2dd4..bc07340 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -33,6 +33,7 @@ const HASHICORP_GPG_KEY_FILENAME: &str = "hashicorp.asc"; #[cfg(feature = "embed_gpg_key")] const HASHICORP_GPG_KEY: &str = include_str!("../gpg/hashicorp.asc"); +/// Handle checking `signature` against `public_key`. #[derive(Debug)] pub struct Signature { // The public key @@ -54,6 +55,11 @@ impl PartialEq for Signature { } impl Signature { + /// Create a new [`Signature`] handler from the given `signature`. + /// + /// # Errors + /// + /// Can error if failing to get the public key. pub fn new(signature: Bytes) -> Result { let public_key = get_public_key()?; @@ -65,27 +71,37 @@ impl Signature { Ok(signature) } + /// Create a new [`Signature`] handler from the given `signature` and + /// `public_key`. + /// + /// # Errors + /// + /// Can error if failing to read the public key or the signature. pub fn with_public_key(signature: Bytes, public_key: &str) -> Result { let mut cursor = Cursor::new(public_key.as_bytes()); let public_key = SignedPublicKey::from_armor_single(&mut cursor)?; - let public_key = public_key.0; - let reader = BufReader::new(signature.reader()); let signature = StandaloneSignature::from_bytes(reader)?; let signature = Self { signature: signature, - public_key: public_key, + public_key: public_key.0, }; Ok(signature) } - // We have to check the signature against all public subkeys and the - // overall public key. + /// Check the given [`Shasums`] content against the [`Signature`]. + /// + /// # Errors + /// + /// Will return an error if unable to verify the signature against the + /// public key or any of its subkeys. pub fn check(&self, shasums: &Shasums) -> Result<()> { let shasums = shasums.content().as_bytes(); + // We have to check the signature against all public subkeys and the + // overall public key. for subkey in &self.public_key.public_subkeys { match self.signature.verify(&subkey, shasums) { Err(_) => continue, diff --git a/src/tmpfile.rs b/src/tmpfile.rs index 9c51ea5..b13eec2 100644 --- a/src/tmpfile.rs +++ b/src/tmpfile.rs @@ -13,13 +13,18 @@ use tempfile::NamedTempFile; #[cfg(target_family = "unix")] use std::os::unix::fs::OpenOptionsExt; +/// Wrapper for a [`tempfile::NamedTempFile`]. pub struct TmpFile { tmpfile: NamedTempFile, filename: String, } impl TmpFile { - // Make a new TmpFile for filename + /// Make a new [`TmpFile`] for filename. + /// + /// # Errors + /// + /// Can error if unable to create a [`NamedTempFile`]. pub fn new(filename: &str) -> Result { let tmp = Self { filename: filename.to_owned(), @@ -29,19 +34,31 @@ impl TmpFile { Ok(tmp) } - // Return the tmpfile filename + /// Return the tmpfile filename + #[must_use] pub fn filename(&self) -> &str { &self.filename } - // Return a handle that has been rewound to 0 + /// Return a [`NamedTempFile`] handle that has been rewound to 0. + /// + /// # Errors + /// + /// Can error if seeking to the beginning of the `tmpfile` fails. pub fn handle(&mut self) -> Result<&mut NamedTempFile> { self.tmpfile.seek(SeekFrom::Start(0))?; Ok(&mut self.tmpfile) } - // Persist the file into our current directory as self.filename + /// Persist the file into our current directory as self.filename + /// + /// # Errors + /// + /// Can error under various common IO issues such as: + /// - Failure to open file for writing + /// - Attempting to get the file handle for the `tmpfile` + /// - Issues while writing to the `tmpfile` pub fn persist(&mut self) -> Result<()> { let dest = Path::new(&self.filename); let mut options = OpenOptions::new(); From eaae7299087860316ea04e2df44e1d387a566864 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 22:55:25 +0100 Subject: [PATCH 02/23] Bump MSRV and update CHANGELOG --- CHANGELOG.md | 3 ++- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfe461..96fc796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## v0.13.0 - - Bump MSRV to 1.65.0 + - Bump MSRV to 1.68.2 - Bump dependencies + - Moved lots of functionality under `lib.rs` ## v0.12.0 diff --git a/Cargo.toml b/Cargo.toml index 319fb74..4cdd40f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" homepage = "https://github.com/phyber/hcdl" repository = "https://github.com/phyber/hcdl" -rust-version = "1.65.0" +rust-version = "1.68.2" resolver = "2" categories = [ "command-line-utilities", diff --git a/README.md b/README.md index 5e1df47..cd42e1c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Installation `hcdl` is available for install from [crates.io] if you have a stable [Rust] -toolchain of at least v1.65.0 installed. +toolchain of at least v1.68.2 installed. This can be done with the standard Cargo install command: From a506b3efe8f7f629dcfdc4e84d75388e2ee3d6c5 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 23:24:21 +0100 Subject: [PATCH 03/23] Fix install dependence on outputting messages --- src/install.rs | 21 ++++++++++++++------- src/main.rs | 12 ++++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/install.rs b/src/install.rs index 255707c..21cc0e8 100644 --- a/src/install.rs +++ b/src/install.rs @@ -2,7 +2,6 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] use super::crc32; -use super::messages::Messages; use anyhow::{ anyhow, Result, @@ -90,8 +89,12 @@ fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { /// - Failing to extract files from the `zipfile` /// - Failing to persist the extracted file /// - Failing to set file permissions on the extracted file +// +// type_complexity is allowed here, since attempting to make the suggested +// type alias results in other complains with no good compiler suggestions. +#[allow(clippy::type_complexity)] pub fn install( - messages: &Messages, + message_fn: Option<&dyn Fn(&Path, &Path)>, zipfile: &mut F, dir: &Path, ) -> Result<()> @@ -125,7 +128,10 @@ where // Finally get a pathbuf of the basename let filename = Path::new(basename).to_path_buf(); - messages.extracting_file(&filename, dir); + // If a message function was given, call it. + if let Some(message_fn) = message_fn { + message_fn(&filename, dir); + } // Extract the file let tmpfile = extract(&mut file, dir)?; @@ -159,10 +165,11 @@ mod tests { // This should really be mocked, but for now we have a real file we // can open from the test-data. - let mut file = File::open(&test_file).unwrap(); - let dest = Path::new(test_file).to_path_buf(); - let messages = Messages::new(false); - let res = install(&messages, &mut file, &dest); + let mut file = File::open(&test_file).unwrap(); + let dest = Path::new(test_file).to_path_buf(); + let message_fn = |_: &Path, _: &Path| {}; + + let res = install(Some(&message_fn), &mut file, &dest); assert!(res.is_err()); } diff --git a/src/main.rs b/src/main.rs index 6164023..790b1a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,10 @@ use hcdl::{ }; use hcdl::messages::Messages; use hcdl::tmpfile::TmpFile; -use std::path::PathBuf; +use std::path::{ + Path, + PathBuf, +}; use std::process::exit; mod cli; @@ -182,7 +185,12 @@ async fn main() -> Result<()> { messages.unzipping(filename, &bin_dir); let mut zip_handle = tmpfile.handle()?; - match install::install(&messages, &mut zip_handle, &bin_dir) { + + let extract_message = |filename: &Path, path: &Path| { + messages.extracting_file(filename, path); + }; + + match install::install(Some(&extract_message), &mut zip_handle, &bin_dir) { Ok(_) => messages.installation_successful(), Err(e) => { messages.installation_failed(&e); From 6c13611643705b23f17c124dadfcda51844132bc Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 23:26:38 +0100 Subject: [PATCH 04/23] Remove messages from the lib API --- src/lib.rs | 3 --- src/main.rs | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0c07def..017a806 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,9 +13,6 @@ pub mod crc32; /// Handle extracting and installing downloaded product. pub mod install; -/// Various output messages. -pub mod messages; - /// List of [HashiCorp](https://www.hashicorp.com/) products. pub mod products; diff --git a/src/main.rs b/src/main.rs index 790b1a2..94d409f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use hcdl::{ products, shasums, }; -use hcdl::messages::Messages; use hcdl::tmpfile::TmpFile; use std::path::{ Path, @@ -19,6 +18,9 @@ use std::path::{ use std::process::exit; mod cli; +mod messages; + +use messages::Messages; #[cfg(feature = "shell_completion")] use clap_complete::Shell; From dbf3ce8eba03018746da9a2429905b98b65c4a18 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 23:27:32 +0100 Subject: [PATCH 05/23] Take care of various clippys --- src/client.rs | 21 +++++++-------------- src/install.rs | 4 ++-- src/main.rs | 2 -- src/shasums.rs | 4 ++-- src/signature.rs | 8 ++++---- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4ee163a..652f2fc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,12 +67,9 @@ impl Client { /// - Failing to get the product version /// - Failing to create a [`ProductVersion`] pub async fn check_version(&self, product: &str) -> Result { - // We to_string here for the test scenario. - #![allow(clippy::to_string_in_format_args)] let url = format!( "{api}/{product}/latest", api = self.api_url, - product = product, ); let url = Url::parse(&url)?; @@ -206,13 +203,9 @@ impl Client { product: &str, version: &str, ) -> Result { - // We to_string here for the test scenario. - #![allow(clippy::to_string_in_format_args)] let url = format!( "{api}/{product}/{version}", api = self.api_url, - product = product, - version = version, ); let url = Url::parse(&url)?; @@ -255,7 +248,7 @@ mod tests { // Builds up the path to the test file fn data_path(filename: &str) -> String { - format!("{}{}", TEST_DATA_DIR, filename) + format!("{TEST_DATA_DIR}{filename}") } fn read_file_bytes(path: &PathBuf) -> Bytes { @@ -317,7 +310,7 @@ mod tests { async fn test_get_bytes() { let mut server = mockito::Server::new_async().await; let server_url = server.url(); - let url = Url::parse(&format!("{}/test.txt", server_url)).unwrap(); + let url = Url::parse(&format!("{server_url}/test.txt")).unwrap(); let expected = "Test text\n"; let data = data_path("test.txt"); @@ -351,22 +344,22 @@ mod tests { name: "terraform".into(), timestamp_created: DateTime::::from_str("2020-05-27T16:55:35.000Z").unwrap(), timestamp_updated: DateTime::::from_str("2020-05-27T16:55:35.000Z").unwrap(), - url_shasums: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS", server_url)).unwrap(), + url_shasums: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS")).unwrap(), version: "0.12.26".into(), builds: vec![ Build { arch: "amd64".into(), os: "freebsd".into(), - url: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_freebsd_amd64.zip", server_url)).unwrap(), + url: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_freebsd_amd64.zip")).unwrap(), }, Build { arch: "amd64".into(), os: "linux".into(), - url: Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_linux_amd64.zip", server_url)).unwrap(), + url: Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_linux_amd64.zip")).unwrap(), }, ], url_shasums_signatures: vec![ - Url::parse(&format!("{}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS.sig", server_url)).unwrap(), + Url::parse(&format!("{server_url}/terraform/0.12.26/terraform_0.12.26_SHA256SUMS.sig")).unwrap(), ], }; @@ -391,7 +384,7 @@ mod tests { async fn test_get_text() { let mut server = mockito::Server::new_async().await; let server_url = server.url(); - let url = Url::parse(&format!("{}/test.txt", server_url)).unwrap(); + let url = Url::parse(&format!("{server_url}/test.txt")).unwrap(); let expected = Bytes::from("Test text\n"); let data = data_path("test.txt"); diff --git a/src/install.rs b/src/install.rs index 21cc0e8..88ec2a5 100644 --- a/src/install.rs +++ b/src/install.rs @@ -103,8 +103,8 @@ where { if !dir.is_dir() { let err = anyhow!( - "install: Destination '{}' is not a directory", - dir.display(), + "install: Destination '{path}' is not a directory", + path = dir.display(), ); return Err(err); diff --git a/src/main.rs b/src/main.rs index 94d409f..083abf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ //! hcdl: Easily update Hashicorp tools #![forbid(unsafe_code)] #![forbid(missing_docs)] -#![allow(clippy::module_name_repetitions)] -#![allow(clippy::redundant_field_names)] use anyhow::Result; use hcdl::{ client, diff --git a/src/shasums.rs b/src/shasums.rs index fe4d83b..f00e33f 100644 --- a/src/shasums.rs +++ b/src/shasums.rs @@ -70,7 +70,7 @@ impl Shasums { pub fn check(&self, tmpfile: &mut TmpFile) -> Result { let filename = tmpfile.filename(); let shasum = self.shasum(filename) - .ok_or_else(|| anyhow!("Couldn't find shasum for {}", filename))?; + .ok_or_else(|| anyhow!("Couldn't find shasum for {filename}"))?; let mut file = tmpfile.handle()?; let mut hasher = Sha256::new(); @@ -169,7 +169,7 @@ mod tests { assert_eq!( res.unwrap_err().to_string(), - format!("Couldn't find shasum for {}", test_data_path), + format!("Couldn't find shasum for {test_data_path}"), ); } diff --git a/src/signature.rs b/src/signature.rs index bc07340..4e6992f 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -152,8 +152,8 @@ fn get_public_key_path() -> Result { // Ensure that the data dir exists if !path.exists() || !path.is_dir() { let msg = anyhow!( - "Data directory {} does not exist or is not a directory", - path.display(), + "Data directory {dir} does not exist or is not a directory", + dir = path.display(), ); return Err(msg); @@ -165,9 +165,9 @@ fn get_public_key_path() -> Result { // Ensure that the GPG key exists if !path.exists() || !path.is_file() { let msg = format!( - "GPG key file {} does not exist or it not a file.\n\ + "GPG key file {path} does not exist or it not a file.\n\ Check https://www.hashicorp.com/security to find the GPG key", - path.display(), + path = path.display(), ); return Err(anyhow!(msg)); From f3c44b8879421b3ec84813623e24e04d5c55a19f Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Sun, 25 Jun 2023 23:35:13 +0100 Subject: [PATCH 06/23] Remove products from the lib API --- src/lib.rs | 3 --- src/main.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 017a806..345da84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,9 +13,6 @@ pub mod crc32; /// Handle extracting and installing downloaded product. pub mod install; -/// List of [HashiCorp](https://www.hashicorp.com/) products. -pub mod products; - /// Handle drawing progress bars during download and install. pub mod progressbar; diff --git a/src/main.rs b/src/main.rs index 083abf8..19b918a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ use anyhow::Result; use hcdl::{ client, install, - products, shasums, }; use hcdl::tmpfile::TmpFile; @@ -17,6 +16,7 @@ use std::process::exit; mod cli; mod messages; +mod products; use messages::Messages; From 9add6ee073315c196e3589c2beb62ba9aade9f0f Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 18:03:49 +0100 Subject: [PATCH 07/23] Remove messaging from install and instead return extracted files --- src/install.rs | 16 ++++++---------- src/main.rs | 17 ++++++++--------- src/messages.rs | 4 ++-- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/install.rs b/src/install.rs index 88ec2a5..46efd82 100644 --- a/src/install.rs +++ b/src/install.rs @@ -94,10 +94,9 @@ fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { // type alias results in other complains with no good compiler suggestions. #[allow(clippy::type_complexity)] pub fn install( - message_fn: Option<&dyn Fn(&Path, &Path)>, zipfile: &mut F, dir: &Path, -) -> Result<()> +) -> Result> where F: Read + Seek, { @@ -110,6 +109,7 @@ where return Err(err); } + let mut extracted_files = Vec::new(); let mut zip = ZipArchive::new(zipfile).expect("new ziparchive"); for i in 0..zip.len() { @@ -128,11 +128,6 @@ where // Finally get a pathbuf of the basename let filename = Path::new(basename).to_path_buf(); - // If a message function was given, call it. - if let Some(message_fn) = message_fn { - message_fn(&filename, dir); - } - // Extract the file let tmpfile = extract(&mut file, dir)?; @@ -145,9 +140,11 @@ where if let Some(mode) = file.unix_mode() { fs::set_permissions(&dest, Permissions::from_mode(mode))?; } + + extracted_files.push(filename); } - Ok(()) + Ok(extracted_files) } #[cfg(test)] @@ -167,9 +164,8 @@ mod tests { // can open from the test-data. let mut file = File::open(&test_file).unwrap(); let dest = Path::new(test_file).to_path_buf(); - let message_fn = |_: &Path, _: &Path| {}; - let res = install(Some(&message_fn), &mut file, &dest); + let res = install(&mut file, &dest); assert!(res.is_err()); } diff --git a/src/main.rs b/src/main.rs index 19b918a..d5bcef4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,7 @@ use hcdl::{ shasums, }; use hcdl::tmpfile::TmpFile; -use std::path::{ - Path, - PathBuf, -}; +use std::path::PathBuf; use std::process::exit; mod cli; @@ -186,12 +183,14 @@ async fn main() -> Result<()> { let mut zip_handle = tmpfile.handle()?; - let extract_message = |filename: &Path, path: &Path| { - messages.extracting_file(filename, path); - }; + match install::install(&mut zip_handle, &bin_dir) { + Ok(extracted_files) => { + for file in extracted_files { + messages.extracted_file(&file, &bin_dir); + } - match install::install(Some(&extract_message), &mut zip_handle, &bin_dir) { - Ok(_) => messages.installation_successful(), + messages.installation_successful(); + }, Err(e) => { messages.installation_failed(&e); diff --git a/src/messages.rs b/src/messages.rs index 33a7c58..4e42dd6 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -60,9 +60,9 @@ impl Messages { } /// Output when a file is being extracted. - pub fn extracting_file(&self, filename: &Path, dest: &Path) { + pub fn extracted_file(&self, filename: &Path, dest: &Path) { let msg = format!( - "-> Extracting '{filename}' to '{dest}'...", + "-> Extracted '{filename}' to '{dest}'...", filename = filename.display(), dest = dest.display(), ); From 678ea8a51ca89ed517fc26ea6cfdcb663e86345c Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 19:19:23 +0100 Subject: [PATCH 08/23] Fix a clippy issue in messages --- src/messages.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messages.rs b/src/messages.rs index 4e42dd6..92bf2ae 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -15,7 +15,7 @@ impl Messages { #[must_use] pub fn new(quiet: bool) -> Self { Self { - quiet: quiet, + quiet, } } From 8d6d18b9ceb5b150e6b28e713305a05774eb2184 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 19:21:30 +0100 Subject: [PATCH 09/23] Document items that cargo doc complained about --- src/client.rs | 14 ++++++++++---- src/client/build.rs | 11 ++++++++--- src/client/config.rs | 2 +- src/client/product_version.rs | 30 +++++++++++++++++++++--------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/client.rs b/src/client.rs index 652f2fc..3b05d65 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,9 +12,15 @@ use std::io::prelude::*; use std::io::BufWriter; use url::Url; -mod build; -mod config; -mod product_version; +/// Re-export of `build`. +pub mod build; + +/// Re-export of `config`. +pub mod config; + +/// Re-export of `product_version`. +pub mod product_version; + pub use config::ClientConfig; use product_version::ProductVersion; @@ -65,7 +71,7 @@ impl Client { /// Errors if: /// - Failing to parse the created checkpoint URL /// - Failing to get the product version - /// - Failing to create a [`ProductVersion`] + /// - Failing to create a [`crate::client::product_version::ProductVersion`] pub async fn check_version(&self, product: &str) -> Result { let url = format!( "{api}/{product}/latest", diff --git a/src/client/build.rs b/src/client/build.rs index e940e76..146d282 100644 --- a/src/client/build.rs +++ b/src/client/build.rs @@ -4,10 +4,15 @@ use serde::Deserialize; use url::Url; -// Represents a single build of a HashiCorp product +/// Represents a single build of a [HashiCorp](https://hashicorp.io) product. #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub struct Build { + /// The `arch` of the build. pub arch: String, - pub os: String, - pub url: Url, + + /// The `os` of the build. + pub os: String, + + /// The [`Url`] where the build can be found. + pub url: Url, } diff --git a/src/client/config.rs b/src/client/config.rs index 2ce6dd6..24d9e52 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,6 +1,6 @@ // Client configuration -/// [`ClientConfig`] is a configuration for [`Client`]. +/// [`ClientConfig`] is a configuration for [`crate::client::Client`]. #[derive(Debug, Default)] pub struct ClientConfig { /// Control the output of colour in the crate messages and progress bars. diff --git a/src/client/product_version.rs b/src/client/product_version.rs index ba440e8..5261f13 100644 --- a/src/client/product_version.rs +++ b/src/client/product_version.rs @@ -12,21 +12,33 @@ use serde::{ }; use std::fmt; use std::str::FromStr; -use super::build::Build; use url::Url; -// Represents a single version of a HashiCorp product +pub use super::build::Build; + +/// Represents a single version of a [HashiCorp](https://hashicorp.io) product. #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub struct ProductVersion { - pub builds: Vec, - pub name: String, - pub url_shasums: Url, + /// A `Vec` of [`Build`] for the [`ProductVersion`]. + pub builds: Vec, + + /// The name of the product. + pub name: String, + + /// The [`Url`] that the product shasums can be found at. + pub url_shasums: Url, + + /// The [`Url`] that the product's shasums signature can be found at. pub url_shasums_signatures: Vec, - pub version: String, + /// The version number of the product. + pub version: String, + + /// A timestamp representing when the product version was created. #[serde(deserialize_with = "deserialize_from_str")] pub timestamp_created: DateTime, + /// A timestamp representing when the product version was updated. #[serde(deserialize_with = "deserialize_from_str")] pub timestamp_updated: DateTime, } @@ -42,7 +54,7 @@ where } impl ProductVersion { - // Pull a specific build out of the product version builds. + /// Pull a specific [`Build`] out of the [`ProductVersion`] `builds`. pub fn build(&self, arch: &str, os: &str) -> Option<&Build> { let filtered: Vec<&Build> = self.builds .iter() @@ -57,12 +69,12 @@ impl ProductVersion { } } - // Create and return the shasums signature URL. + /// Create and return the shasums signature URL. pub fn shasums_signature_url(&self) -> Url { self.url_shasums_signatures.first().unwrap().clone() } - // Create and return the shasums URL. + /// Create and return the shasums URL. pub fn shasums_url(&self) -> Url { self.url_shasums.clone() } From 6f46fefb6f4cf8dd170892aa2b2ed6d23498bcbe Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 20:40:19 +0100 Subject: [PATCH 10/23] Move install and crc32 errors to thiserror --- Cargo.lock | 1 + Cargo.toml | 1 + src/crc32.rs | 15 +++----------- src/error.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ src/install.rs | 29 ++++++++------------------ src/lib.rs | 3 +++ src/messages.rs | 3 ++- 7 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index ab81fe2..1c8764c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,6 +956,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", + "thiserror", "tokio", "url", "zip", diff --git a/Cargo.toml b/Cargo.toml index 4cdd40f..b4c9c97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ pgp = "0.10" serde_json = "1.0" sha2 = "0.10" tempfile = "3.6" +thiserror = "1.0" [dependencies.clap] version = "4.3.8" diff --git a/src/crc32.rs b/src/crc32.rs index 6a460f8..7b50c6b 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -1,9 +1,6 @@ // crc32: Handle checking CRC32 of a given file against the expected CRC32. #![forbid(unsafe_code)] -use anyhow::{ - anyhow, - Result, -}; +use super::error::Crc32Error; use crc32fast::Hasher; use std::fs::File; use std::io::{ @@ -23,7 +20,7 @@ const BUFFER_SIZE: usize = 256 * 1_024; /// - Failing to open the given `path` /// - Failing to read from the given `path` /// - If the CRC32 of the given `path` doesn't match the `expected` value -pub fn check

(path: P, expected: u32) -> Result<()> +pub fn check

(path: P, expected: u32) -> Result<(), Crc32Error> where P: AsRef, { @@ -44,13 +41,7 @@ where let result = hasher.finalize(); if result != expected { - let msg = anyhow!( - "Error CRC32: Expected: {:#010x}, Got: {:#010x}", - expected, - result, - ); - - return Err(msg); + return Err(Crc32Error::UnexpectedCrc32(expected, result)); } Ok(()) diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..567f485 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,55 @@ +// Library errors. + +use std::path::PathBuf; +use thiserror::Error; + +/// Errors encountered in the [`crc32`] module. +#[derive(Debug, Error)] +pub enum Crc32Error { + /// Returned if there's an IO error while calculating the CRC32 for the + /// extracted file. + #[error("io error")] + IoError(#[from] std::io::Error), + + /// Returned if there's a problem with the calculated CRC32 for the + /// extracted file. + #[error("unexpected crc32. expected: {0:#010x}, got: {1:#010x}")] + UnexpectedCrc32(u32, u32), +} + +/// Errors encountered in the [`install`] module. +#[derive(Debug, Error)] +pub enum InstallError { + /// Returned if there's an error while calculating the CRC32 for the + /// installed file. + #[error("crc32 error")] + Crc32(#[from] Crc32Error), + + // Could not find suitable install-dir. Consider passing --install-dir to + // manually specify. + /// Returned if there's no executable directory. + #[error("no executable dir")] + NoExecutableDir, + + /// Returned if the installation path is not a directory. + #[error("install: destination '{0}' is not a directory")] + NoInstallDir(PathBuf), + + /// Returned if there's an error while persisting the tempfile to it's + /// proper destination. + #[error("error persisting file")] + PathPersist(#[from] tempfile::PathPersistError), + + /// Returned if there's an error while setting the installed file's + /// permissions. + #[error("set permissions error")] + SetPermissions(#[from] std::io::Error), + + /// Returned if there's an error while getting the zip file basename. + #[error("couldn't get zip file basename from '{0}'")] + ZipFileBasename(String), + + /// Returned if there's an error while getting the zip file index. + #[error("zip index error")] + ZipIndex(#[from] zip::result::ZipError), +} diff --git a/src/install.rs b/src/install.rs index 46efd82..8597e2b 100644 --- a/src/install.rs +++ b/src/install.rs @@ -2,10 +2,7 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] use super::crc32; -use anyhow::{ - anyhow, - Result, -}; +use super::error::InstallError; use std::fs; use std::io::{ self, @@ -38,7 +35,7 @@ use std::os::unix::fs::PermissionsExt; /// Errors if: /// - Failing to find a suitable executable directory /// - Failing to create the executable directory if needed -pub fn bin_dir() -> Result { +pub fn bin_dir() -> Result { if let Some(dir) = dirs::executable_dir() { // Attempt to create the directory if it doesn't exist if !dir.exists() { @@ -49,12 +46,7 @@ pub fn bin_dir() -> Result { } else { // If we get None, we're likely on Windows. - let msg = concat!( - "Could not find suitable install-dir.\n", - "Consider passing --install-dir to manually specify", - ); - - Err(anyhow!(msg)) + Err(InstallError::NoExecutableDir) } } @@ -62,7 +54,7 @@ pub fn bin_dir() -> Result { /// the CRC32 of the extracted file to make sure extraction was successful. /// Returns a [`tempfile::TempPath`] which the caller is responsible for /// persisting. -fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { +fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { // Get a tempfile to extract to under the dest path let mut tmpfile = NamedTempFile::new_in(dir)?; @@ -96,17 +88,12 @@ fn extract(mut zipfile: &mut ZipFile, dir: &Path) -> Result { pub fn install( zipfile: &mut F, dir: &Path, -) -> Result> +) -> Result, InstallError> where F: Read + Seek, { if !dir.is_dir() { - let err = anyhow!( - "install: Destination '{path}' is not a directory", - path = dir.display(), - ); - - return Err(err); + return Err(InstallError::NoInstallDir(dir.to_path_buf())); } let mut extracted_files = Vec::new(); @@ -123,7 +110,9 @@ where // Attempt to get the basename of the filename let basename = Path::new(filename) .file_name() - .ok_or_else(|| anyhow!("Couldn't get basename from: {filename}"))?; + .ok_or_else(|| { + InstallError::ZipFileBasename(filename.to_string()) + })?; // Finally get a pathbuf of the basename let filename = Path::new(basename).to_path_buf(); diff --git a/src/lib.rs b/src/lib.rs index 345da84..0fe89b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,9 @@ pub mod client; /// Handle checking the CRC32 of files extracted from zipfiles. pub mod crc32; +/// This module contains the error types that the library can return. +pub mod error; + /// Handle extracting and installing downloaded product. pub mod install; diff --git a/src/messages.rs b/src/messages.rs index 92bf2ae..956e6cb 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -2,6 +2,7 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] use anyhow::Error; +use hcdl::error::InstallError; use std::path::Path; /// Handler for the various message we need to output. @@ -79,7 +80,7 @@ impl Messages { } /// Output when a product installation has failed. - pub fn installation_failed(&self, error: &Error) { + pub fn installation_failed(&self, error: &InstallError) { let msg = format!("Installation failed with error: {error}"); self.stderr(&msg); From c2c54be2509fc6755a73fb154135705711d53ea8 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 21:10:41 +0100 Subject: [PATCH 11/23] Move signature over to thiserror --- src/error.rs | 25 +++++++++++++++++++++++++ src/messages.rs | 8 +++++--- src/signature.rs | 46 +++++++++++++++++----------------------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/error.rs b/src/error.rs index 567f485..18345a0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -53,3 +53,28 @@ pub enum InstallError { #[error("zip index error")] ZipIndex(#[from] zip::result::ZipError), } + +/// Errors encountered in the [`signature`] module. +#[derive(Debug, Error)] +pub enum SignatureError { + /// Returned if the GPG key path does not exist or is not a file. + #[error("gpg key file '{0}' does not exist or is not a file")] + GpgKey(PathBuf), + + /// Returned when there's an IO error dealing with signature data. + #[error(transparent)] + IoError(#[from] std::io::Error), + + /// Returned if there's no XDG shared data directory returned. + #[error("couldn't find shared data directory")] + NoSharedDataDir, + + /// Returned when the XDG shared data path returned does not exist or is + /// not a directory. + #[error("data directory '{0}' does not exist or is not a directory")] + NoSharedDataDirExists(PathBuf), + + /// Returned when the shasum signatures couldn't be verified. + #[error(transparent)] + Pgp(#[from] pgp::errors::Error), +} diff --git a/src/messages.rs b/src/messages.rs index 956e6cb..2eab3ce 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,8 +1,10 @@ // Messages output by other parts of the program #![forbid(unsafe_code)] #![forbid(missing_docs)] -use anyhow::Error; -use hcdl::error::InstallError; +use hcdl::error::{ + InstallError, + SignatureError, +}; use std::path::Path; /// Handler for the various message we need to output. @@ -126,7 +128,7 @@ impl Messages { } /// Output when signature verification has failed. - pub fn signature_verification_failed(&self, error: &Error) { + pub fn signature_verification_failed(&self, error: &SignatureError) { let msg = format!("Verification failed, error: {error}"); self.stderr(&msg); diff --git a/src/signature.rs b/src/signature.rs index 4e6992f..80acb34 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,11 +1,8 @@ // signature: Check GPG signatures #![forbid(unsafe_code)] #![forbid(missing_docs)] +use super::error::SignatureError; use super::shasums::Shasums; -use anyhow::{ - anyhow, - Result, -}; use bytes::{ Buf, Bytes, @@ -60,7 +57,7 @@ impl Signature { /// # Errors /// /// Can error if failing to get the public key. - pub fn new(signature: Bytes) -> Result { + pub fn new(signature: Bytes) -> Result { let public_key = get_public_key()?; let signature = Self::with_public_key( @@ -77,7 +74,10 @@ impl Signature { /// # Errors /// /// Can error if failing to read the public key or the signature. - pub fn with_public_key(signature: Bytes, public_key: &str) -> Result { + pub fn with_public_key( + signature: Bytes, + public_key: &str, + ) -> Result { let mut cursor = Cursor::new(public_key.as_bytes()); let public_key = SignedPublicKey::from_armor_single(&mut cursor)?; let reader = BufReader::new(signature.reader()); @@ -97,7 +97,7 @@ impl Signature { /// /// Will return an error if unable to verify the signature against the /// public key or any of its subkeys. - pub fn check(&self, shasums: &Shasums) -> Result<()> { + pub fn check(&self, shasums: &Shasums) -> Result<(), SignatureError> { let shasums = shasums.content().as_bytes(); // We have to check the signature against all public subkeys and the @@ -110,16 +110,15 @@ impl Signature { } // One last attempt, check against the main public key. - match self.signature.verify(&self.public_key, shasums) { - Err(_) => Err(anyhow!("Couldn't verify signature")), - Ok(()) => Ok(()), - } + self.signature.verify(&self.public_key, shasums)?; + + Ok(()) } } // Read a file's content into a String #[cfg(any(test, not(feature = "embed_gpg_key")))] -fn read_file_content(path: &PathBuf) -> Result { +fn read_file_content(path: &PathBuf) -> Result { let file = File::open(&path)?; let mut reader = BufReader::new(file); let mut contents = String::new(); @@ -131,7 +130,7 @@ fn read_file_content(path: &PathBuf) -> Result { // Find the path where the GPG key should be stored. #[cfg(not(feature = "embed_gpg_key"))] -fn get_public_key_path() -> Result { +fn get_public_key_path() -> Result { // During tests we short circuit the path discovery to just take the // GPG key from the test-data directory. let path = if cfg!(test) { @@ -147,16 +146,11 @@ fn get_public_key_path() -> Result { } else { let mut path = dirs::data_dir() - .ok_or_else(|| anyhow!("Couldn't find shared data directory"))?; + .ok_or_else(|| SignatureError::NoSharedDataDir)?; // Ensure that the data dir exists if !path.exists() || !path.is_dir() { - let msg = anyhow!( - "Data directory {dir} does not exist or is not a directory", - dir = path.display(), - ); - - return Err(msg); + return Err(SignatureError::NoSharedDataDirExists(path)); } path = path.join(env!("CARGO_PKG_NAME")); @@ -164,13 +158,7 @@ fn get_public_key_path() -> Result { // Ensure that the GPG key exists if !path.exists() || !path.is_file() { - let msg = format!( - "GPG key file {path} does not exist or it not a file.\n\ - Check https://www.hashicorp.com/security to find the GPG key", - path = path.display(), - ); - - return Err(anyhow!(msg)); + return Err(SignatureError::GpgKey(path)); } path @@ -181,7 +169,7 @@ fn get_public_key_path() -> Result { // Locate and read the GPG key. #[cfg(not(feature = "embed_gpg_key"))] -fn get_public_key() -> Result { +fn get_public_key() -> Result { let path = get_public_key_path()?; let public_key = read_file_content(&path)?; @@ -192,7 +180,7 @@ fn get_public_key() -> Result { // embed_gpg_key feature. #[cfg(feature = "embed_gpg_key")] #[allow(clippy::unnecessary_wraps)] -fn get_public_key() -> Result { +fn get_public_key() -> Result { let public_key = HASHICORP_GPG_KEY.to_string(); Ok(public_key) From 2591026c9191fcfa7bb1cc7e04fa79913a35d8de Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 21:34:21 +0100 Subject: [PATCH 12/23] Fix up tests in crc32 and signature --- src/crc32.rs | 4 ++-- src/error.rs | 8 ++++++-- src/signature.rs | 9 +++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/crc32.rs b/src/crc32.rs index 7b50c6b..a23752f 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -41,7 +41,7 @@ where let result = hasher.finalize(); if result != expected { - return Err(Crc32Error::UnexpectedCrc32(expected, result)); + return Err(Crc32Error::UnexpectedCrc32(result, expected)); } Ok(()) @@ -63,7 +63,7 @@ mod tests { assert_eq!( result.unwrap_err().to_string(), - "Error CRC32: Expected: 0x00000000, Got: 0x891bc0e8", + "unexpected crc32: 0x891bc0e8, wanted: 0x00000000", ); } diff --git a/src/error.rs b/src/error.rs index 18345a0..564a5db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,12 +8,12 @@ use thiserror::Error; pub enum Crc32Error { /// Returned if there's an IO error while calculating the CRC32 for the /// extracted file. - #[error("io error")] + #[error(transparent)] IoError(#[from] std::io::Error), /// Returned if there's a problem with the calculated CRC32 for the /// extracted file. - #[error("unexpected crc32. expected: {0:#010x}, got: {1:#010x}")] + #[error("unexpected crc32: {0:#010x}, wanted: {1:#010x}")] UnexpectedCrc32(u32, u32), } @@ -77,4 +77,8 @@ pub enum SignatureError { /// Returned when the shasum signatures couldn't be verified. #[error(transparent)] Pgp(#[from] pgp::errors::Error), + + /// Returned when the signature couldn't be verified. + #[error("couldn't verify signature")] + Verification, } diff --git a/src/signature.rs b/src/signature.rs index 80acb34..dcc0ea2 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -110,7 +110,8 @@ impl Signature { } // One last attempt, check against the main public key. - self.signature.verify(&self.public_key, shasums)?; + self.signature.verify(&self.public_key, shasums) + .map_err(|_err| SignatureError::Verification)?; Ok(()) } @@ -192,7 +193,7 @@ mod tests { use std::path::Path; // Read a file's contents into Bytes - fn read_file_bytes(path: &PathBuf) -> Result { + fn read_file_bytes(path: &PathBuf) -> Result { let file = File::open(&path)?; let mut reader = BufReader::new(file); let mut contents = Vec::new(); @@ -316,7 +317,7 @@ mod tests { assert_eq!( res.unwrap_err().to_string(), - "Couldn't verify signature", + "couldn't verify signature", ) } @@ -367,7 +368,7 @@ mod tests { assert_eq!( res.unwrap_err().to_string(), - "Couldn't verify signature", + "couldn't verify signature", ) } } From 2d4cde0d6c76a17c288c37f75b090b9884425987 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 21:36:59 +0100 Subject: [PATCH 13/23] Fix clippy issues in ProductVersion --- src/client/product_version.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/product_version.rs b/src/client/product_version.rs index 5261f13..713f87d 100644 --- a/src/client/product_version.rs +++ b/src/client/product_version.rs @@ -55,6 +55,7 @@ where impl ProductVersion { /// Pull a specific [`Build`] out of the [`ProductVersion`] `builds`. + #[must_use] pub fn build(&self, arch: &str, os: &str) -> Option<&Build> { let filtered: Vec<&Build> = self.builds .iter() @@ -70,11 +71,18 @@ impl ProductVersion { } /// Create and return the shasums signature URL. + /// + /// # Panics + /// + /// This function could panic if there are no signature URLs returned by + /// the API. + #[must_use] pub fn shasums_signature_url(&self) -> Url { self.url_shasums_signatures.first().unwrap().clone() } /// Create and return the shasums URL. + #[must_use] pub fn shasums_url(&self) -> Url { self.url_shasums.clone() } From 75f3b4bff2d61f20801ae509ebf19b43a98297e2 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 21:44:54 +0100 Subject: [PATCH 14/23] Move tmpfile to thiserror --- src/error.rs | 8 ++++++++ src/tmpfile.rs | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index 564a5db..967cab0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -82,3 +82,11 @@ pub enum SignatureError { #[error("couldn't verify signature")] Verification, } + +/// Errors encountered in the [`tmpfile`] module. +#[derive(Debug, Error)] +pub enum TmpFileError { + /// Returned if IO errors are encountered. + #[error(transparent)] + IoError(#[from] std::io::Error), +} diff --git a/src/tmpfile.rs b/src/tmpfile.rs index b13eec2..4608216 100644 --- a/src/tmpfile.rs +++ b/src/tmpfile.rs @@ -1,5 +1,5 @@ // Handles a tmpfile for downloading -use anyhow::Result; +use super::error::TmpFileError; use std::fs::OpenOptions; use std::io::{ self, @@ -25,7 +25,7 @@ impl TmpFile { /// # Errors /// /// Can error if unable to create a [`NamedTempFile`]. - pub fn new(filename: &str) -> Result { + pub fn new(filename: &str) -> Result { let tmp = Self { filename: filename.to_owned(), tmpfile: NamedTempFile::new()?, @@ -45,7 +45,7 @@ impl TmpFile { /// # Errors /// /// Can error if seeking to the beginning of the `tmpfile` fails. - pub fn handle(&mut self) -> Result<&mut NamedTempFile> { + pub fn handle(&mut self) -> Result<&mut NamedTempFile, TmpFileError> { self.tmpfile.seek(SeekFrom::Start(0))?; Ok(&mut self.tmpfile) @@ -59,7 +59,7 @@ impl TmpFile { /// - Failure to open file for writing /// - Attempting to get the file handle for the `tmpfile` /// - Issues while writing to the `tmpfile` - pub fn persist(&mut self) -> Result<()> { + pub fn persist(&mut self) -> Result<(), TmpFileError> { let dest = Path::new(&self.filename); let mut options = OpenOptions::new(); From baaaf7b877f0ac475862c476231e09ed5ffc8eec Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 21:59:33 +0100 Subject: [PATCH 15/23] Move shasums to thiserror --- src/error.rs | 16 ++++++++++++++++ src/shasums.rs | 17 ++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index 967cab0..964dab7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,6 +54,22 @@ pub enum InstallError { ZipIndex(#[from] zip::result::ZipError), } +/// Errors encountered in the [`shasums`] module. +#[derive(Debug, Error)] +pub enum ShasumsError { + /// Returned if there's an error while calculating the shasum for the file. + #[error("io error while hashing file")] + Hashing, + + /// Returned when the shasum for a file could not be found. + #[error("couldn't find shasum for {0}")] + NoShasumForFile(String), + + /// Returned if there's a [`TmpFileError`] while hashing the file. + #[error(transparent)] + TmpFile(#[from] TmpFileError), +} + /// Errors encountered in the [`signature`] module. #[derive(Debug, Error)] pub enum SignatureError { diff --git a/src/shasums.rs b/src/shasums.rs index f00e33f..e8ce927 100644 --- a/src/shasums.rs +++ b/src/shasums.rs @@ -2,10 +2,7 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] use crate::tmpfile::TmpFile; -use anyhow::{ - anyhow, - Result, -}; +use super::error::ShasumsError; use sha2::{ Digest, Sha256, @@ -67,15 +64,21 @@ impl Shasums { /// - Failing to find the shasum for the `tmpfile` filename /// - Failing to obtain a handle for the `tmpfile` /// - Failing to hash the file content - pub fn check(&self, tmpfile: &mut TmpFile) -> Result { + pub fn check( + &self, + tmpfile: &mut TmpFile, + ) -> Result { let filename = tmpfile.filename(); let shasum = self.shasum(filename) - .ok_or_else(|| anyhow!("Couldn't find shasum for {filename}"))?; + .ok_or_else(|| { + ShasumsError::NoShasumForFile(filename.to_string()) + })?; let mut file = tmpfile.handle()?; let mut hasher = Sha256::new(); - io::copy(&mut file, &mut hasher)?; + io::copy(&mut file, &mut hasher) + .map_err(|_err| ShasumsError::Hashing)?; let hash = hasher.finalize(); From ea2dabea4bcfee6307e940098ff3d5792e290d40 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 22:31:47 +0100 Subject: [PATCH 16/23] Move client to thiserror --- src/client.rs | 55 ++++++++++++++++++++++++++++++++------------------- src/error.rs | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3b05d65..f29ae3f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,7 +5,7 @@ use crate::progressbar::ProgressBarBuilder; use crate::shasums::Shasums; use crate::signature::Signature; use crate::tmpfile::TmpFile; -use anyhow::Result; +use super::error::ClientError; use bytes::Bytes; use reqwest::Response; use std::io::prelude::*; @@ -47,12 +47,13 @@ impl Client { /// # Errors /// /// Errors if failing to build the [`reqwest::Client`]. - pub fn new(config: ClientConfig) -> Result { + pub fn new(config: ClientConfig) -> Result { // Get a new reqwest client with our user-agent let client = reqwest::ClientBuilder::new() .gzip(true) .user_agent(USER_AGENT) - .build()?; + .build() + .map_err(|_err| ClientError::ClientBuilder)?; let client = Self { api_url: RELEASES_API.to_string(), @@ -72,18 +73,23 @@ impl Client { /// - Failing to parse the created checkpoint URL /// - Failing to get the product version /// - Failing to create a [`crate::client::product_version::ProductVersion`] - pub async fn check_version(&self, product: &str) -> Result { + pub async fn check_version( + &self, + product: &str, + ) -> Result { let url = format!( "{api}/{product}/latest", api = self.api_url, ); - let url = Url::parse(&url)?; + let url = Url::parse(&url) + .map_err(|_err| ClientError::Url("check_version"))?; let resp = self.get(url) .await? .json::() - .await?; + .await + .map_err(|_err| ClientError::ProductVersion)?; Ok(resp) } @@ -100,7 +106,7 @@ impl Client { &self, url: Url, tmpfile: &mut TmpFile, - ) -> Result<()> { + ) -> Result<(), ClientError> { let file = tmpfile.handle()?; // Start the GET and attempt to get a content-length @@ -118,7 +124,11 @@ impl Client { let mut writer = pb.wrap_write(writer); // Start downloading chunks. - while let Some(chunk) = resp.chunk().await? { + while let Some(chunk) = resp + .chunk() + .await + .map_err(|_| ClientError::Chunk)? + { // Write the chunk to the output file. writer.write_all(&chunk)?; } @@ -129,33 +139,36 @@ impl Client { } /// Perform an HTTP GET on the given `url`. - async fn get(&self, url: Url) -> Result { + async fn get(&self, url: Url) -> Result { let resp = self.client - .get(url) + .get(url.clone()) .send() - .await?; + .await + .map_err(|_err| ClientError::Get(url))?; Ok(resp) } /// Perform an HTTP GET on the given `url` and return the result as /// [`Bytes`]. - async fn get_bytes(&self, url: Url) -> Result { + async fn get_bytes(&self, url: Url) -> Result { let resp = self.get(url) .await? .bytes() - .await?; + .await + .map_err(|_err| ClientError::GetBytes)?; Ok(resp) } /// Perform an HTTP GET on the given `url` and return the result as a /// `String`. - async fn get_text(&self, url: Url) -> Result { + async fn get_text(&self, url: Url) -> Result { let resp = self.get(url) .await? .text() - .await?; + .await + .map_err(|_err| ClientError::GetText)?; Ok(resp) } @@ -169,7 +182,7 @@ impl Client { pub async fn get_shasums( &self, version: &ProductVersion, - ) -> Result { + ) -> Result { let url = version.shasums_url(); let shasums = self.get_text(url).await?; let shasums = Shasums::new(shasums); @@ -188,7 +201,7 @@ impl Client { pub async fn get_signature( &self, version: &ProductVersion, - ) -> Result { + ) -> Result { let url = version.shasums_signature_url(); let signature = self.get_bytes(url).await?; let signature = Signature::new(signature)?; @@ -208,18 +221,20 @@ impl Client { &self, product: &str, version: &str, - ) -> Result { + ) -> Result { let url = format!( "{api}/{product}/{version}", api = self.api_url, ); - let url = Url::parse(&url)?; + let url = Url::parse(&url) + .map_err(|_err| ClientError::Url("get_version"))?; let resp = self.get(url) .await? .json::() - .await?; + .await + .map_err(|_err| ClientError::ProductVersion)?; Ok(resp) } diff --git a/src/error.rs b/src/error.rs index 964dab7..3bf7536 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,51 @@ use std::path::PathBuf; use thiserror::Error; +/// Errors encountered in the [`client`] module. +#[derive(Debug, Error)] +pub enum ClientError { + /// Returned when encountering an error building the [`Client`]. + #[error("couldn't build http client")] + ClientBuilder, + + /// Returned if there's an error downloading a chunk of content. + #[error("couldn't download chunk of content")] + Chunk, + + /// Returned when there's an error getting a [`Url`]. + #[error("couldn't get url '{0}'")] + Get(url::Url), + + /// Returned if there's an error getting the [`Bytes`] from a GET request. + #[error("couldn't get bytes from get request")] + GetBytes, + + /// Returned if there's an error getting a [`String`] from a GET request. + #[error("couldn't get text from get request")] + GetText, + + /// Returned if there's an IO error while downloading content. + #[error(transparent)] + IoError(#[from] std::io::Error), + + /// Returned if there's an error parsing the [`ProductVersion`]. + #[error("couldn't parse product version")] + ProductVersion, + + /// Returned when there's an error getting a [`Signature`] for the + /// [`ProductVersion`]. + #[error(transparent)] + Signature(#[from] SignatureError), + + /// Returned if there's [`TmpFile`] error while downloading content. + #[error(transparent)] + TmpFile(#[from] TmpFileError), + + /// Returned if there's an error parsing a [`Url`]. + #[error("couldn't parse {0} url")] + Url(&'static str), +} + /// Errors encountered in the [`crc32`] module. #[derive(Debug, Error)] pub enum Crc32Error { From eefa67bb55dca2936f38f334032c1b608722edad Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Mon, 26 Jun 2023 22:32:48 +0100 Subject: [PATCH 17/23] Fix tests in shasums --- src/shasums.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shasums.rs b/src/shasums.rs index e8ce927..6e18322 100644 --- a/src/shasums.rs +++ b/src/shasums.rs @@ -172,7 +172,7 @@ mod tests { assert_eq!( res.unwrap_err().to_string(), - format!("Couldn't find shasum for {test_data_path}"), + format!("couldn't find shasum for {test_data_path}"), ); } From 524b283c027f7e3e19c08942ca19b92d87124d4d Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 11:30:04 +0100 Subject: [PATCH 18/23] Add bin and lib sections in Cargo.toml --- Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b4c9c97..4c59e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,14 @@ exclude = [ ".github", ] +[[bin]] +name = "hcdl" +path = "src/main.rs" + +[lib] +name = "hcdl" +path = "src/lib.rs" + [features] default = [ "embed_gpg_key", From 4536e1219164e74d709ae81686dcb667a8d1262a Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 12:43:52 +0100 Subject: [PATCH 19/23] Fix a minor clippy issues in signature --- src/signature.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/signature.rs b/src/signature.rs index dcc0ea2..91eca90 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -120,7 +120,7 @@ impl Signature { // Read a file's content into a String #[cfg(any(test, not(feature = "embed_gpg_key")))] fn read_file_content(path: &PathBuf) -> Result { - let file = File::open(&path)?; + let file = File::open(path)?; let mut reader = BufReader::new(file); let mut contents = String::new(); From 9457b2d809c2fe13e311f2f446b9b39d2370a868 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 17:51:50 +0100 Subject: [PATCH 20/23] Add doctest examples to crc32 --- src/crc32.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/crc32.rs b/src/crc32.rs index a23752f..10c7431 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -1,6 +1,6 @@ // crc32: Handle checking CRC32 of a given file against the expected CRC32. #![forbid(unsafe_code)] -use super::error::Crc32Error; +use crate::error::Crc32Error; use crc32fast::Hasher; use std::fs::File; use std::io::{ @@ -20,6 +20,33 @@ const BUFFER_SIZE: usize = 256 * 1_024; /// - Failing to open the given `path` /// - Failing to read from the given `path` /// - If the CRC32 of the given `path` doesn't match the `expected` value +/// +/// # Examples +/// +/// Check that the CRC32 of the crate's test-data file is correct. +/// +/// ``` +/// use hcdl::crc32; +/// +/// let path = concat!(env!("CARGO_MANIFEST_DIR"), "/test-data/test.txt"); +/// let result = crc32::check(path, 0x891bc0e8); +/// +/// assert!(result.is_ok()); +/// ``` +/// +/// The CRC32 was not what was expected. +/// +/// ``` +/// use hcdl::{ +/// crc32, +/// error::Crc32Error, +/// }; +/// +/// let path = concat!(env!("CARGO_MANIFEST_DIR"), "/test-data/test.txt"); +/// let result = crc32::check(path, 0x12345678); +/// +/// assert!(matches!(result.unwrap_err(), Crc32Error::UnexpectedCrc32(_, _))); +/// ``` pub fn check

(path: P, expected: u32) -> Result<(), Crc32Error> where P: AsRef, From 0714ff57887d5e9a16ac9536563ee856554ffed3 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 17:52:18 +0100 Subject: [PATCH 21/23] Simplify crate imports in client --- src/client.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index f29ae3f..6b8c0db 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,13 @@ // client: HTTP client and associated methods #![forbid(unsafe_code)] #![forbid(missing_docs)] -use crate::progressbar::ProgressBarBuilder; -use crate::shasums::Shasums; -use crate::signature::Signature; -use crate::tmpfile::TmpFile; -use super::error::ClientError; +use crate::{ + error::ClientError, + progressbar::ProgressBarBuilder, + shasums::Shasums, + signature::Signature, + tmpfile::TmpFile, +}; use bytes::Bytes; use reqwest::Response; use std::io::prelude::*; From e2d4b8dc0dda7ae4f6cc61db677b1d2d407f9134 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 17:52:38 +0100 Subject: [PATCH 22/23] Remove a random newline in error --- src/error.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 3bf7536..b7bd959 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ // Library errors. - use std::path::PathBuf; use thiserror::Error; From 0926f26f7748d7d94601d493c174e0467ac78fb9 Mon Sep 17 00:00:00 2001 From: David O'Rourke Date: Tue, 27 Jun 2023 19:08:25 +0100 Subject: [PATCH 23/23] Derive Debug on more lib structs --- src/progressbar.rs | 1 + src/shasums.rs | 1 + src/tmpfile.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/progressbar.rs b/src/progressbar.rs index 42f791d..e00ebd2 100644 --- a/src/progressbar.rs +++ b/src/progressbar.rs @@ -122,6 +122,7 @@ impl ProgressBarBuilder { } /// A wrapper for an [`indicatif::ProgressBar`]. +#[derive(Debug)] pub struct ProgressBar { bar: indicatif::ProgressBar, } diff --git a/src/shasums.rs b/src/shasums.rs index 6e18322..620586a 100644 --- a/src/shasums.rs +++ b/src/shasums.rs @@ -21,6 +21,7 @@ pub enum Checksum { } /// [`Shasums`] represents a downloaded shasum file. +#[derive(Debug)] pub struct Shasums { content: String, } diff --git a/src/tmpfile.rs b/src/tmpfile.rs index 4608216..c29c261 100644 --- a/src/tmpfile.rs +++ b/src/tmpfile.rs @@ -14,6 +14,7 @@ use tempfile::NamedTempFile; use std::os::unix::fs::OpenOptionsExt; /// Wrapper for a [`tempfile::NamedTempFile`]. +#[derive(Debug)] pub struct TmpFile { tmpfile: NamedTempFile, filename: String,