Skip to content

Commit b78cad2

Browse files
authored
Merge pull request #32 from lookbusy1344/claude/issue-31-20250912-1026
Refactor project into logical modules
2 parents 8ae6d2d + d57b4a6 commit b78cad2

File tree

15 files changed

+598
-446
lines changed

15 files changed

+598
-446
lines changed

src/cli/args.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::ffi::OsString;
2+
use std::str::FromStr;
3+
4+
use anyhow::{Result, anyhow};
5+
use pico_args::Arguments;
6+
7+
use crate::cli::config::{ConfigSettings, HELP};
8+
use crate::core::types::{DEFAULT_HASH, GIT_VERSION_SHORT, HashAlgorithm, OutputEncoding, VERSION};
9+
10+
pub fn process_command_line(mut pargs: Arguments) -> Result<ConfigSettings> {
11+
let algo_str: Option<String> = pargs.opt_value_from_str(["-a", "--algorithm"])?;
12+
let algo = parse_hash_algorithm(algo_str.as_deref()).map_err(|_| {
13+
anyhow!(
14+
"Algorithm can be: CRC32, MD5, SHA1, SHA2 / SHA2-256 / SHA-256, SHA2-224, SHA2-384, SHA2-512, SHA3 / SHA3-256, SHA3-384, SHA3-512, WHIRLPOOL, BLAKE2S-256, BLAKE2B-512. Default is {DEFAULT_HASH:?}",
15+
)
16+
})?;
17+
18+
let encoding_str: Option<String> = pargs.opt_value_from_str(["-e", "--encoding"])?;
19+
let encoding = parse_hash_encoding(encoding_str.as_deref())
20+
.map_err(|_| anyhow!("Encoding can be: Hex, Base64, Base32. Default is Hex",))?;
21+
22+
let encoding = match encoding {
23+
OutputEncoding::Unspecified if algo == HashAlgorithm::CRC32 => OutputEncoding::U32,
24+
OutputEncoding::Unspecified => OutputEncoding::Hex,
25+
other => other,
26+
};
27+
28+
if algo == HashAlgorithm::CRC32 && encoding != OutputEncoding::U32 {
29+
return Err(anyhow!(
30+
"CRC32 must use U32 encoding, and U32 encoding can only be used with CRC32"
31+
));
32+
}
33+
34+
let mut config = ConfigSettings::new(
35+
pargs.contains(["-d", "--debug"]),
36+
pargs.contains(["-x", "--exclude-filenames"]),
37+
pargs.contains(["-s", "--single-thread"]),
38+
pargs.contains(["-c", "--case-sensitive"]),
39+
pargs.contains(["-n", "--no-progress"]),
40+
algo,
41+
encoding,
42+
pargs.opt_value_from_str(["-l", "--limit"])?,
43+
);
44+
45+
let remaining_args = args_finished(pargs)?;
46+
47+
let supplied_paths = remaining_args
48+
.into_iter()
49+
.map(|arg| arg.to_string_lossy().to_string())
50+
.collect();
51+
52+
config.set_supplied_paths(supplied_paths);
53+
54+
Ok(config)
55+
}
56+
57+
pub fn parse_hash_algorithm(algorithm: Option<&str>) -> Result<HashAlgorithm, strum::ParseError> {
58+
match algorithm {
59+
Some(algo_str) if !algo_str.is_empty() => HashAlgorithm::from_str(algo_str),
60+
_ => Ok(DEFAULT_HASH),
61+
}
62+
}
63+
64+
pub fn parse_hash_encoding(encoding: Option<&str>) -> Result<OutputEncoding, strum::ParseError> {
65+
match encoding {
66+
Some(enc_str) if !enc_str.is_empty() => OutputEncoding::from_str(enc_str),
67+
_ => Ok(OutputEncoding::Unspecified),
68+
}
69+
}
70+
71+
pub fn show_help(longform: bool) {
72+
println!(
73+
"File hasher for various algorithms. Version {} ({})",
74+
VERSION.unwrap_or("?"),
75+
GIT_VERSION_SHORT
76+
);
77+
if longform {
78+
println!("{HELP}");
79+
}
80+
println!("Default algorithm is {DEFAULT_HASH:?}");
81+
}
82+
83+
fn args_finished(args: Arguments) -> Result<Vec<OsString>> {
84+
let unused = args.finish();
85+
86+
for arg in &unused {
87+
if arg.to_string_lossy().starts_with('-') {
88+
return Err(anyhow!("Unknown argument: {}", arg.to_string_lossy()));
89+
}
90+
}
91+
92+
Ok(unused)
93+
}

src/cli/config.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use crate::core::types::{HashAlgorithm, OutputEncoding};
2+
3+
#[allow(clippy::struct_excessive_bools)]
4+
#[readonly::make]
5+
#[derive(Debug)]
6+
pub struct ConfigSettings {
7+
pub debug_mode: bool,
8+
pub exclude_fn: bool,
9+
pub single_thread: bool,
10+
pub case_sensitive: bool,
11+
pub no_progress: bool,
12+
pub algorithm: HashAlgorithm,
13+
pub encoding: OutputEncoding,
14+
pub limit_num: Option<usize>,
15+
pub supplied_paths: Vec<String>,
16+
}
17+
18+
impl ConfigSettings {
19+
#[allow(clippy::fn_params_excessive_bools)]
20+
#[allow(clippy::too_many_arguments)]
21+
pub fn new(
22+
debug_mode: bool,
23+
exclude_fn: bool,
24+
single_thread: bool,
25+
case_sensitive: bool,
26+
no_progress: bool,
27+
algorithm: HashAlgorithm,
28+
encoding: OutputEncoding,
29+
limit_num: Option<usize>,
30+
) -> Self {
31+
Self {
32+
debug_mode,
33+
exclude_fn,
34+
single_thread,
35+
case_sensitive,
36+
no_progress,
37+
algorithm,
38+
encoding,
39+
limit_num,
40+
supplied_paths: Vec::new(),
41+
}
42+
}
43+
44+
pub fn set_supplied_paths(&mut self, paths: Vec<String>) {
45+
self.supplied_paths = paths;
46+
}
47+
}
48+
49+
pub const HELP: &str = "\
50+
USAGE:
51+
hash_rust.exe [flags] [options] file glob
52+
FLAGS:
53+
-h, --help Prints help information
54+
-d, --debug Debug messages
55+
-c, --case-sensitive Case-sensitive glob matching
56+
-x, --exclude-filenames Exclude filenames from output
57+
-s, --single-thread Single-threaded (not multi-threaded)
58+
-n, --no-progress Suppress progress display (for scripts)
59+
OPTIONS:
60+
-a, --algorithm [algorithm] Hash algorithm to use
61+
-e, --encoding [encoding] Output encoding (Hex, Base64, Base32. Default is Hex)
62+
-l, --limit [num] Limit number of files processed
63+
64+
Algorithm can be:
65+
CRC32, MD5, SHA1, WHIRLPOOL, BLAKE2S-256, BLAKE2B-512,
66+
SHA2 / SHA2-256 / SHA-256, SHA2-224, SHA2-384, SHA2-512,
67+
SHA3 / SHA3-256 (default), SHA3-384, SHA3-512";

src/cli/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod args;
2+
pub mod config;
3+
4+
pub use args::{process_command_line, show_help};

src/core/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod types;
2+
pub mod worker;
3+
4+
pub use worker::worker_func;

src/core/types.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::fmt;
2+
3+
use git_version::git_version;
4+
use strum::EnumString;
5+
6+
pub const DEFAULT_HASH: HashAlgorithm = HashAlgorithm::SHA3_256;
7+
pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
8+
pub const GIT_VERSION_SHORT: &str = git_version!(args = ["--abbrev=14", "--always", "--dirty=+"]);
9+
10+
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumString)]
11+
#[strum(ascii_case_insensitive)]
12+
pub enum HashAlgorithm {
13+
#[strum(serialize = "CRC32", serialize = "CRC-32")]
14+
CRC32,
15+
#[strum(serialize = "MD5", serialize = "MD-5")]
16+
MD5,
17+
#[strum(serialize = "SHA1", serialize = "SHA-1")]
18+
SHA1,
19+
#[strum(
20+
serialize = "SHA2",
21+
serialize = "SHA2-256",
22+
serialize = "SHA2_256",
23+
serialize = "SHA_256",
24+
serialize = "SHA-256"
25+
)]
26+
SHA2_256,
27+
#[strum(serialize = "SHA2-224", serialize = "SHA2_224")]
28+
SHA2_224,
29+
#[strum(serialize = "SHA2-384", serialize = "SHA2_384")]
30+
SHA2_384,
31+
#[strum(serialize = "SHA3", serialize = "SHA3-256", serialize = "SHA3_256")]
32+
SHA3_256,
33+
#[strum(serialize = "SHA2-512", serialize = "SHA2_512")]
34+
SHA2_512,
35+
#[strum(serialize = "SHA3-384", serialize = "SHA3_384")]
36+
SHA3_384,
37+
#[strum(serialize = "SHA3-512", serialize = "SHA3_512")]
38+
SHA3_512,
39+
#[strum(serialize = "WHIRLPOOL")]
40+
Whirlpool,
41+
#[strum(serialize = "BLAKE2B-512", serialize = "BLAKE2B_512")]
42+
Blake2B512,
43+
#[strum(serialize = "BLAKE2S-256", serialize = "BLAKE2S_256")]
44+
Blake2S256,
45+
}
46+
47+
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumString)]
48+
#[strum(ascii_case_insensitive)]
49+
pub enum OutputEncoding {
50+
Hex,
51+
Base64,
52+
Base32,
53+
U32,
54+
Unspecified,
55+
}
56+
57+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58+
pub struct BasicHash(pub String);
59+
60+
impl fmt::Display for BasicHash {
61+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62+
write!(f, "{}", self.0)
63+
}
64+
}

src/core/worker.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use std::fmt::Display;
2+
use std::time::{Duration, Instant};
3+
4+
use anyhow::Result;
5+
use rayon::prelude::*;
6+
7+
use crate::cli::config::ConfigSettings;
8+
use crate::core::types::BasicHash;
9+
use crate::hash::algorithms::call_hasher;
10+
use crate::io::files::get_required_filenames;
11+
use crate::progress::ProgressManager;
12+
13+
pub fn worker_func(config: &ConfigSettings) -> Result<()> {
14+
if config.debug_mode {
15+
show_initial_info(config);
16+
}
17+
18+
let paths = get_required_filenames(config)?;
19+
20+
if paths.is_empty() {
21+
if config.debug_mode {
22+
eprintln!("No files found");
23+
}
24+
return Ok(());
25+
}
26+
27+
if config.debug_mode {
28+
eprintln!("Files to hash: {paths:?}");
29+
}
30+
31+
if config.single_thread || paths.len() == 1 {
32+
file_hashes_st(config, &paths);
33+
} else {
34+
file_hashes_mt(config, &paths);
35+
}
36+
37+
Ok(())
38+
}
39+
40+
fn show_initial_info(config: &ConfigSettings) {
41+
crate::cli::args::show_help(false);
42+
eprintln!();
43+
eprintln!("Config: {config:?}");
44+
if config.supplied_paths.is_empty() {
45+
eprintln!("No path specified, reading from stdin");
46+
} else {
47+
eprintln!(
48+
"Paths: {} file path(s) supplied",
49+
config.supplied_paths.len()
50+
);
51+
}
52+
}
53+
54+
fn file_hashes_st<S>(config: &ConfigSettings, paths: &[S])
55+
where
56+
S: AsRef<str> + Display + Send + Sync,
57+
{
58+
if config.debug_mode {
59+
eprintln!("Single-threaded mode");
60+
eprintln!("Algorithm: {:?}", config.algorithm);
61+
}
62+
63+
for pathstr in paths {
64+
let file_hash = hash_with_progress(config, pathstr.as_ref().to_string());
65+
66+
match file_hash {
67+
Ok(basic_hash) => {
68+
if config.exclude_fn {
69+
println!("{basic_hash}");
70+
} else {
71+
println!("{basic_hash} {pathstr}");
72+
}
73+
}
74+
Err(e) => eprintln!("File error for '{pathstr}': {e}"),
75+
}
76+
}
77+
}
78+
79+
fn file_hashes_mt<S>(config: &ConfigSettings, paths: &[S])
80+
where
81+
S: AsRef<str> + Sync + Display,
82+
{
83+
if config.debug_mode {
84+
eprintln!("Multi-threaded mode");
85+
eprintln!("Algorithm: {:?}", config.algorithm);
86+
}
87+
88+
let overall_progress = if config.no_progress {
89+
None
90+
} else {
91+
ProgressManager::create_overall_progress(paths.len(), config.debug_mode)
92+
};
93+
94+
paths.par_iter().for_each(|pathstr| {
95+
let file_hash = hash_with_progress(config, pathstr.as_ref().to_string());
96+
97+
if let Some(ref pb) = overall_progress {
98+
pb.inc(1);
99+
}
100+
101+
match file_hash {
102+
Ok(basic_hash) => {
103+
if config.exclude_fn {
104+
println!("{basic_hash}");
105+
} else {
106+
println!("{basic_hash} {pathstr}");
107+
}
108+
}
109+
110+
Err(e) => eprintln!("File error for '{pathstr}': {e}"),
111+
}
112+
});
113+
114+
if let Some(pb) = overall_progress {
115+
pb.finish_with_message("Complete!");
116+
}
117+
}
118+
119+
fn hash_with_progress<S>(config: &ConfigSettings, pathstr: S) -> Result<BasicHash>
120+
where
121+
S: AsRef<str> + Display + Clone + Send + 'static,
122+
{
123+
let pathstr_clone = pathstr.clone();
124+
125+
let progress_handle = if config.no_progress {
126+
None
127+
} else {
128+
ProgressManager::create_file_progress(pathstr_clone.clone(), config.debug_mode)
129+
};
130+
131+
let start_time = Instant::now();
132+
let result = call_hasher(config.algorithm, config.encoding, pathstr);
133+
let elapsed = start_time.elapsed();
134+
135+
if let Some(handle) = progress_handle {
136+
handle.finish(config.debug_mode);
137+
}
138+
139+
if config.debug_mode && elapsed >= Duration::from_millis(ProgressManager::threshold_millis()) {
140+
eprintln!(
141+
"File '{}' took {:.2}s to hash",
142+
pathstr_clone,
143+
elapsed.as_secs_f64()
144+
);
145+
}
146+
147+
result
148+
}

0 commit comments

Comments
 (0)