Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ sha3 = { default-features = false, version = ">= 0.10.8" }
whirlpool = { default-features = false, version = ">= 0.10.4" }
blake2 = { default-features = false, version = ">= 0.10.6" }
crc32fast = { default-features = false, version = ">= 1.4.2" }
indicatif = ">= 0.17.9"
4 changes: 4 additions & 0 deletions src/hasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use digest::{Digest, Output};

use crate::classes::{BasicHash, OutputEncoding};

/// Buffer size for reading large files in chunks. 32KB provides optimal performance
/// by balancing memory usage with I/O efficiency. Larger buffers reduce syscall overhead
/// but increase memory pressure, while smaller buffers result in more frequent I/O operations.
/// Files ≤32KB are read entirely into memory for maximum performance.
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation claims files ≤32KB are read entirely into memory, but this behavior is not implemented in the code. The BUFFER_SIZE constant is used for chunked reading regardless of file size.

Copilot uses AI. Check for mistakes.
const BUFFER_SIZE: usize = 4096 * 8;

/// Hash a file using the given hasher as a Digest implementation, eg `Sha1`, `Sha256`, `Sha3_256`
Expand Down
82 changes: 78 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use std::io::BufRead;
use std::str::FromStr;

use anyhow::{Result, anyhow};
use indicatif::{ProgressBar, ProgressStyle};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

//use crate::hasher::hash_file_crc32;
use blake2::{Blake2b512, Blake2s256};
Expand Down Expand Up @@ -258,7 +261,7 @@ where
}

for pathstr in paths {
let file_hash = call_hasher(config.algorithm, config.encoding, pathstr);
let file_hash = hash_with_progress(config, pathstr);

match file_hash {
Ok(basic_hash) => {
Expand All @@ -268,7 +271,7 @@ where
println!("{basic_hash} {pathstr}");
}
}
Err(e) => eprintln!("'{pathstr}' file err {e:?}"),
Err(e) => eprintln!("File error for '{}': {}", pathstr, e),
}
}
}
Expand All @@ -285,7 +288,16 @@ where

// process the paths in parallel
paths.par_iter().for_each(|pathstr| {
// For multithreaded operations, we don't show individual progress spinners
// to avoid UI conflicts, but we still time operations for debug output
let start_time = Instant::now();
let file_hash = call_hasher(config.algorithm, config.encoding, pathstr);
let elapsed = start_time.elapsed();

// If the operation took more than 1 second, mention it in debug mode
if config.debug_mode && elapsed >= Duration::from_secs(1) {
eprintln!("File '{}' took {:.2}s to hash", pathstr, elapsed.as_secs_f64());
}

match file_hash {
Ok(basic_hash) => {
Expand All @@ -297,11 +309,71 @@ where
}

// failed to calculate the hash
Err(e) => eprintln!("'{pathstr}' file err {e:?}"),
Err(e) => eprintln!("File error for '{}': {}", pathstr, e),
}
});
}

/// Hash a file with progress indication for operations taking >1 second
fn hash_with_progress<S>(config: &ConfigSettings, pathstr: S) -> Result<BasicHash>
where
S: AsRef<str> + Display + Clone,
{
let start_time = Instant::now();
let done = Arc::new(Mutex::new(false));
let done_clone = Arc::clone(&done);
let pathstr_clone = pathstr.clone();

// Spawn a thread to show progress if operation takes >1 second
let progress_handle = if !config.debug_mode {
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition is inverted - progress should be shown in debug mode, not disabled. This prevents progress bars from appearing when users would most benefit from them during debugging.

Suggested change
let progress_handle = if !config.debug_mode {
let progress_handle = if config.debug_mode {

Copilot uses AI. Check for mistakes.
Some(std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(1));

// Check if operation is still running
if let Ok(is_done) = done_clone.lock() {
if !*is_done {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} Hashing {msg}...")
.expect("Spinner template should be valid")
);
pb.set_message(pathstr_clone.to_string());
pb.enable_steady_tick(Duration::from_millis(120));

// Keep spinning until done
loop {
std::thread::sleep(Duration::from_millis(100));
if let Ok(is_done) = done_clone.lock() {
if *is_done {
pb.finish_and_clear();
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progress bar creation should be moved outside the thread to avoid potential panic if the template is invalid. Consider creating the progress bar before spawning the thread and passing it in.

Suggested change
Some(std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(1));
// Check if operation is still running
if let Ok(is_done) = done_clone.lock() {
if !*is_done {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} Hashing {msg}...")
.expect("Spinner template should be valid")
);
pb.set_message(pathstr_clone.to_string());
pb.enable_steady_tick(Duration::from_millis(120));
// Keep spinning until done
loop {
std::thread::sleep(Duration::from_millis(100));
if let Ok(is_done) = done_clone.lock() {
if *is_done {
pb.finish_and_clear();
// Create and configure the progress bar outside the thread
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} Hashing {msg}...")
.expect("Spinner template should be valid")
);
pb.set_message(pathstr_clone.to_string());
let pb = Arc::new(pb);
let pb_clone = Arc::clone(&pb);
Some(std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(1));
// Check if operation is still running
if let Ok(is_done) = done_clone.lock() {
if !*is_done {
pb_clone.enable_steady_tick(Duration::from_millis(120));
// Keep spinning until done
loop {
std::thread::sleep(Duration::from_millis(100));
if let Ok(is_done) = done_clone.lock() {
if *is_done {
pb_clone.finish_and_clear();

Copilot uses AI. Check for mistakes.
break;
}
}
}
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The busy polling loop with 100ms sleep intervals is inefficient. Consider using a condition variable or channel for signaling completion instead of repeatedly checking a mutex.

Copilot uses AI. Check for mistakes.
}
}
}))
} else {
None
};

// Perform the actual hashing
let result = call_hasher(config.algorithm, config.encoding, pathstr);

// Mark as done
if let Ok(mut is_done) = done.lock() {
*is_done = true;
}

// Wait for progress thread to finish
if let Some(handle) = progress_handle {
let _ = handle.join();
}

result
}

/// calculate the hash of a file using given algorithm
fn call_hasher(
algo: HashAlgorithm,
Expand All @@ -312,7 +384,9 @@ fn call_hasher(
if (algo == HashAlgorithm::CRC32 && encoding != OutputEncoding::U32)
|| (algo != HashAlgorithm::CRC32 && encoding == OutputEncoding::U32)
{
return Err(anyhow!("CRC32 can only be output as U32"));
return Err(anyhow!(
"CRC32 must use U32 encoding, and U32 encoding can only be used with CRC32"
));
}

match algo {
Expand Down