diff --git a/.cargo/README.md b/.cargo/README.md index d4cfeb5..d93088f 100644 --- a/.cargo/README.md +++ b/.cargo/README.md @@ -25,6 +25,14 @@ Alternatively, you can use the [`cargo add`](https://doc.rust-lang.org/cargo/com cargo add chksum-core ``` +## Features + +### Asynchronous Runtime + +* `async-runtime-tokio`: Enables async interface for Tokio runtime. + +By default, neither of these features is enabled. + ## Example Crates For implementation-specific examples, refer to the source code of the following crates: diff --git a/CHANGELOG.md b/CHANGELOG.md index 18954c0..3d82511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added async support for Tokio runtime. +- Added `doc_auto_cfg` feature. + ### Fixed - Added missing method comments to improve documentation clarity. diff --git a/Cargo.toml b/Cargo.toml index 24c687f..66c52b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,13 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +async-trait = { version = "0.1.80", optional = true } chksum-hash-core = "0.0.0" thiserror = "1.0.51" +tokio = { version = "1.37.0", features = ["fs", "io-util", "io-std"], optional = true } + +[features] +default = [] + +# async runtimes +async-runtime-tokio = ["async-trait", "tokio"] diff --git a/README.md b/README.md index 6c9169e..b6726ed 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,14 @@ Alternatively, you can use the [`cargo add`](https://doc.rust-lang.org/cargo/com cargo add chksum-core ``` +## Features + +### Asynchronous Runtime + +* `async-runtime-tokio`: Enables async interface for Tokio runtime. + +By default, neither of these features is enabled. + ## Example Crates For implementation-specific examples, refer to the source code of the following crates: diff --git a/src/lib.rs b/src/lib.rs index 7b072ef..aa6fd79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,14 @@ //! cargo add chksum-core //! ``` //! +//! # Features +//! +//! ## Asynchronous Runtime +//! +//! * `async-runtime-tokio`: Enables async interface for Tokio runtime. +//! +//! By default, neither of these features is enabled. +//! //! # Example Crates //! //! For implementation-specific examples, refer to the source code of the following crates: @@ -31,15 +39,20 @@ //! //! This crate is licensed under the MIT License. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] mod error; +#[cfg(feature = "async-runtime-tokio")] +mod tokio; use std::fmt::{Display, LowerHex, UpperHex}; use std::fs::{read_dir, DirEntry, File, ReadDir}; use std::io::{self, BufRead, BufReader, IsTerminal, Stdin, StdinLock}; use std::path::{Path, PathBuf}; +#[cfg(feature = "async-runtime-tokio")] +use async_trait::async_trait; #[doc(no_inline)] pub use chksum_hash_core as hash; @@ -70,6 +83,15 @@ where data.chksum::() } +/// Computes the hash of the given input. +#[cfg(feature = "async-runtime-tokio")] +pub async fn async_chksum(mut data: impl AsyncChksumable) -> Result +where + T: Hash + Send, +{ + data.chksum::().await +} + /// A trait for hash digests. pub trait Digest: Display { /// Returns a byte slice of the digest's contents. @@ -235,7 +257,7 @@ impl_chksumable!(PathBuf, &PathBuf, &mut PathBuf => { where H: Hash, { - self.as_path().chksum_with(hash) + Chksumable::chksum_with(&mut self.as_path(), hash) } }); @@ -267,7 +289,7 @@ impl_chksumable!(DirEntry, &DirEntry, &mut DirEntry => { where H: Hash, { - self.path().chksum_with(hash) + Chksumable::chksum_with(&mut self.path(), hash) } }); @@ -316,3 +338,38 @@ impl_chksumable!(StdinLock<'_>, &mut StdinLock<'_> => { Ok(()) } }); + +/// A trait for complex objects which must be processed chunk by chunk. +#[cfg(feature = "async-runtime-tokio")] +#[async_trait] +pub trait AsyncChksumable: Send { + /// Calculates the checksum of the object. + async fn chksum(&mut self) -> Result + where + H: Hash + Send, + { + let mut hash = H::default(); + self.chksum_with(&mut hash).await?; + Ok(hash.digest()) + } + + /// Updates the given hash instance with the data from the object. + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send; +} + +#[cfg(feature = "async-runtime-tokio")] +#[async_trait] +impl AsyncChksumable for T +where + T: Hashable + Send, +{ + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + self.hash_with(hash); + Ok(()) + } +} diff --git a/src/tokio.rs b/src/tokio.rs new file mode 100644 index 0000000..af0e432 --- /dev/null +++ b/src/tokio.rs @@ -0,0 +1,117 @@ +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use tokio::fs::{metadata, read_dir, DirEntry, File, ReadDir}; +use tokio::io::{AsyncBufReadExt as _, BufReader, Stdin}; + +use crate::{AsyncChksumable, Hash, Hashable, Result}; + +macro_rules! impl_async_chksumable { + ($($t:ty),+ => $i:tt) => { + $( + #[async_trait] + impl AsyncChksumable for $t $i + )* + }; +} + +impl_async_chksumable!(Path, &Path, &mut Path => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + let metadata = metadata(&self).await?; + if metadata.is_dir() { + read_dir(self).await?.chksum_with(hash).await + } else { + // everything treat as a file when it is not a directory + File::open(self).await?.chksum_with(hash).await + } + } + +}); + +impl_async_chksumable!(PathBuf, &PathBuf, &mut PathBuf => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + self.as_path().chksum_with(hash).await + } +}); + +// TODO: missing `&File` implementation +impl_async_chksumable!(File, &mut File => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + // TODO: tracking issue [tokio-rs/tokio#6407](github.com/tokio-rs/tokio/issues/6407) + // if self.is_terminal() { + // return Err(Error::IsTerminal); + // } + + let mut reader = BufReader::new(self); + loop { + let buffer = reader.fill_buf().await?; + let length = buffer.len(); + if length == 0 { + break; + } + buffer.hash_with(hash); + reader.consume(length); + } + Ok(()) + } +}); + +impl_async_chksumable!(DirEntry, &DirEntry, &mut DirEntry => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + self.path().chksum_with(hash).await + } +}); + +impl_async_chksumable!(ReadDir, &mut ReadDir => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + let mut dir_entries = Vec::new(); + while let Some(dir_entry) = self.next_entry().await? { + dir_entries.push(dir_entry); + } + dir_entries.sort_by_key(DirEntry::path); + for mut dir_entry in dir_entries { + dir_entry.chksum_with(hash).await?; + } + Ok(()) + } +}); + +// TODO: missing `&Stdin` implementation +impl_async_chksumable!(Stdin, &mut Stdin => { + async fn chksum_with(&mut self, hash: &mut H) -> Result<()> + where + H: Hash + Send, + { + // TODO: tracking issue [tokio-rs/tokio#6407](github.com/tokio-rs/tokio/issues/6407) + // if self.is_terminal() { + // return Err(Error::IsTerminal); + // } + + let mut reader = BufReader::new(self); + loop { + let buffer = reader.fill_buf().await?; + let length = buffer.len(); + if length == 0 { + break; + } + buffer.hash_with(hash); + reader.consume(length); + } + Ok(()) + } +});