Skip to content

Commit c6b2312

Browse files
committed
Fix issues from code review: glob handling, I/O optimization, validation, and UX
1 parent 162c146 commit c6b2312

File tree

7 files changed

+89
-31
lines changed

7 files changed

+89
-31
lines changed

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ HashRust is a CLI file hashing utility written in Rust that supports multiple ha
3030

3131
### Module Structure
3232
- `main.rs` - CLI argument parsing, main worker function, and threading coordination
33-
- `classes.rs` - Core types: `HashAlgorithm` enum, `OutputEncoding` enum, `ConfigSettings` struct, `BasicHash` wrapper
34-
- `hasher.rs` - Generic hashing implementation using Digest trait, file I/O optimization for small/large files
35-
- `crc32.rs` - Custom CRC32 implementation (separate from other hash algorithms)
33+
- `core/types.rs` - Core types: `HashAlgorithm` enum, `OutputEncoding` enum, `BasicHash` wrapper
34+
- `cli/config.rs` - Configuration: `ConfigSettings` struct
35+
- `hash/` - Hashing logic and algorithm implementations
36+
- `io/` - File handling and glob pattern matching
3637
- `unit_tests.rs` - Unit test module
3738

3839
### Key Design Patterns
@@ -69,7 +70,7 @@ HashRust is a CLI file hashing utility written in Rust that supports multiple ha
6970
### Performance Considerations
7071
- Multi-threading is default behavior
7172
- Buffer size optimized for typical file sizes
72-
- Uses BufReader for efficient file I/O
73+
- Direct file I/O with large buffers for performance
7374
- Release builds use LTO and panic=abort for size/speed
7475

7576
## Development Workflow

src/cli/args.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ pub fn process_command_line(mut pargs: Arguments) -> Result<ConfigSettings> {
2525
other => other,
2626
};
2727

28-
if algo == HashAlgorithm::CRC32 && encoding != OutputEncoding::U32 {
28+
if (algo == HashAlgorithm::CRC32 && encoding != OutputEncoding::U32)
29+
|| (algo != HashAlgorithm::CRC32 && encoding == OutputEncoding::U32)
30+
{
2931
return Err(anyhow!(
3032
"CRC32 must use U32 encoding, and U32 encoding can only be used with CRC32"
3133
));

src/core/worker.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ where
6161
}
6262

6363
for pathstr in paths {
64-
let file_hash = hash_with_progress(config, pathstr.as_ref().to_string());
64+
let file_hash = hash_with_progress(config, pathstr.as_ref().to_string(), false);
6565

6666
match file_hash {
6767
Ok(basic_hash) => {
@@ -92,7 +92,11 @@ where
9292
};
9393

9494
paths.par_iter().for_each(|pathstr| {
95-
let file_hash = hash_with_progress(config, pathstr.as_ref().to_string());
95+
let file_hash = hash_with_progress(
96+
config,
97+
pathstr.as_ref().to_string(),
98+
overall_progress.is_some(),
99+
);
96100

97101
if let Some(ref pb) = overall_progress {
98102
pb.inc(1);
@@ -116,13 +120,17 @@ where
116120
}
117121
}
118122

119-
fn hash_with_progress<S>(config: &ConfigSettings, pathstr: S) -> Result<BasicHash>
123+
fn hash_with_progress<S>(
124+
config: &ConfigSettings,
125+
pathstr: S,
126+
suppress_spinner: bool,
127+
) -> Result<BasicHash>
120128
where
121129
S: AsRef<str> + Display + Clone + Send + 'static,
122130
{
123131
let pathstr_clone = pathstr.clone();
124132

125-
let progress_handle = if config.no_progress {
133+
let progress_handle = if config.no_progress || suppress_spinner {
126134
None
127135
} else {
128136
ProgressManager::create_file_progress(pathstr_clone.clone(), config.debug_mode)

src/hash/digest_impl.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fs::File;
2-
use std::io::{BufReader, Read};
2+
use std::io::Read;
33
use std::path::Path;
44

55
use byteorder::{BigEndian, ByteOrder};
@@ -17,12 +17,11 @@ fn hash_file<D: Digest>(filename: impl AsRef<str>) -> anyhow::Result<Output<D>>
1717
return hash_file_whole::<D>(filename);
1818
}
1919

20-
let file = File::open(filename.as_ref())?;
21-
let mut reader = BufReader::new(file);
20+
let mut file = File::open(filename.as_ref())?;
2221
let mut buffer = build_heap_buffer(BUFFER_SIZE);
2322
let mut hasher = D::new();
2423

25-
while let Ok(bytes_read) = reader.read(&mut buffer) {
24+
while let Ok(bytes_read) = file.read(&mut buffer) {
2625
if bytes_read == 0 {
2726
break;
2827
}

src/io/files.rs

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,45 @@ fn get_paths_matching_glob(config: &ConfigSettings) -> Result<Vec<String>> {
5050
let mut result = Vec::new();
5151

5252
for pattern in &config.supplied_paths {
53-
let glob_matches: Vec<_> = glob::glob_with(pattern, glob_settings)?
54-
.filter_map(|entry| match entry {
55-
Ok(path) if path.is_file() => Some(path.to_string_lossy().into_owned()),
56-
_ => None,
57-
})
58-
.collect();
53+
let glob_result = glob::glob_with(pattern, glob_settings);
5954

60-
if glob_matches.is_empty() {
61-
if file_exists(pattern) {
62-
result.push(pattern.clone());
63-
} else if !pattern.contains(['*', '?', '[', ']']) {
64-
let path = std::path::Path::new(pattern);
65-
if path.exists() && path.is_dir() {
66-
if config.debug_mode {
67-
eprintln!("Ignoring directory: {pattern}");
55+
match glob_result {
56+
Ok(paths) => {
57+
let glob_matches: Vec<_> = paths
58+
.filter_map(|entry| match entry {
59+
Ok(path) if path.is_file() => Some(path.to_string_lossy().into_owned()),
60+
_ => None,
61+
})
62+
.collect();
63+
64+
if glob_matches.is_empty() {
65+
if file_exists(pattern) {
66+
result.push(pattern.clone());
67+
} else if !pattern.contains(['*', '?', '[', ']']) {
68+
let path = std::path::Path::new(pattern);
69+
if path.exists() && path.is_dir() {
70+
if config.debug_mode {
71+
eprintln!("Ignoring directory: {pattern}");
72+
}
73+
} else {
74+
return Err(anyhow::anyhow!("File not found: {}", pattern));
75+
}
6876
}
6977
} else {
70-
return Err(anyhow::anyhow!("File not found: {}", pattern));
78+
result.extend(glob_matches);
79+
}
80+
}
81+
Err(_) => {
82+
// If glob fails (e.g. invalid pattern), treat as literal file
83+
if file_exists(pattern) {
84+
result.push(pattern.clone());
85+
} else {
86+
return Err(anyhow::anyhow!(
87+
"File not found or invalid glob: {}",
88+
pattern
89+
));
7190
}
7291
}
73-
} else {
74-
result.extend(glob_matches);
7592
}
7693
}
7794

src/progress.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
use indicatif::{ProgressBar, ProgressStyle};
88
use std::fmt::Display;
99
use std::sync::{
10+
Arc,
1011
atomic::{AtomicUsize, Ordering},
1112
mpsc,
12-
Arc,
1313
};
1414
use std::time::Duration;
1515

tests/integration_tests.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,37 @@ fn test_crc32_with_invalid_encoding_error() {
222222
assert!(stderr.to_lowercase().contains("error"));
223223
}
224224

225+
#[test]
226+
fn test_u32_encoding_with_non_crc32_error() {
227+
let test_content = "test";
228+
let temp_dir = std::env::temp_dir();
229+
let test_file = temp_dir.join("test_u32_invalid.txt");
230+
fs::write(&test_file, test_content).expect("Failed to write test file");
231+
232+
let output = Command::new("cargo")
233+
.args(&[
234+
"run",
235+
"--",
236+
"-a",
237+
"MD5",
238+
"-e",
239+
"U32",
240+
test_file.to_str().unwrap(),
241+
])
242+
.output()
243+
.expect("Failed to execute hash_rust");
244+
245+
fs::remove_file(&test_file).ok();
246+
247+
// Should fail with non-zero exit code
248+
assert!(!output.status.success());
249+
let stderr = String::from_utf8_lossy(&output.stderr);
250+
251+
// Should contain error message about incompatible algorithm/encoding combination
252+
assert!(stderr.to_lowercase().contains("error"));
253+
assert!(stderr.contains("CRC32 must use U32 encoding"));
254+
}
255+
225256
#[test]
226257
fn test_empty_file_path_error() {
227258
// Test with no file arguments

0 commit comments

Comments
 (0)