diff options
Diffstat (limited to 'utils/sha1_hash')
| -rw-r--r-- | utils/sha1_hash/Cargo.toml | 9 | ||||
| -rw-r--r-- | utils/sha1_hash/res/story.txt | 48 | ||||
| -rw-r--r-- | utils/sha1_hash/res/story_crlf.sha1 | 1 | ||||
| -rw-r--r-- | utils/sha1_hash/res/story_lf.sha1 | 1 | ||||
| -rw-r--r-- | utils/sha1_hash/src/lib.rs | 257 |
5 files changed, 316 insertions, 0 deletions
diff --git a/utils/sha1_hash/Cargo.toml b/utils/sha1_hash/Cargo.toml new file mode 100644 index 0000000..e206efd --- /dev/null +++ b/utils/sha1_hash/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sha1_hash" +edition = "2024" +version.workspace = true + +[dependencies] +tokio = { version = "1.48", features = ["full"] } +sha1 = "0.10" +futures = "0.3" diff --git a/utils/sha1_hash/res/story.txt b/utils/sha1_hash/res/story.txt new file mode 100644 index 0000000..a91f467 --- /dev/null +++ b/utils/sha1_hash/res/story.txt @@ -0,0 +1,48 @@ +魏曹者,程序员也,发稀甚于代码。 +忽接神秘电话曰: +"贺君中彩,得长生之赐。" +魏曹冷笑曰:"吾命尚不及下版之期。" + +翌日果得U盘。 +接入电脑,弹窗示曰: +"点此确认,即获永生。" +魏曹径点"永拒"。 + +三月后,U盘自格其盘。 +进度条滞于九九。 +客服电话已成空号。 +魏曹乃知身可不死,然体内癌细胞亦得不灭。 + +遂谒主请辞。 +主曰:"巧甚,公司正欲优化。" +魏曹曰:"吾不死。" +主目骤亮:"则可007至司闭。" + +魏曹始试诸死法。 +坠楼,卧医三月,账单令其愿死。 +饮鸩,肝肾永损,然终不得死。 +终决卧轨。 + +择高铁最速者。 +司机探头曰:"兄台,吾亦不死身也。" +"此车已碾如君者二十人矣。" + +二人遂坐轨畔对饮。 +司机曰:"知最讽者何?" +"吾等永存,而所爱者皆逝矣。" + +魏曹忽得系统提示: +"侦得用户消极求生,将启工模。" +自是无日不毕KPI,否则遍尝绝症之苦。 + +是日对镜整寿衣。 +忽见顶生一丝乌发。 +泫然泣下,此兆示其将复活一轮回。 + +--- 忽忆DeepSeek尝作Footer曰: +"文成而Hash1验,若星河之固。" +遂取哈希值校之, +字符流转如天河倒泻, +终得"e3b0c44298fc1c14"之数。 +然文末数字竟阙如残月, +方知此篇亦遭永劫轮回。 diff --git a/utils/sha1_hash/res/story_crlf.sha1 b/utils/sha1_hash/res/story_crlf.sha1 new file mode 100644 index 0000000..bc8ad25 --- /dev/null +++ b/utils/sha1_hash/res/story_crlf.sha1 @@ -0,0 +1 @@ +40c1d848d8d6a14b9403ee022f2b28dabb3b3a71 diff --git a/utils/sha1_hash/res/story_lf.sha1 b/utils/sha1_hash/res/story_lf.sha1 new file mode 100644 index 0000000..c2e3213 --- /dev/null +++ b/utils/sha1_hash/res/story_lf.sha1 @@ -0,0 +1 @@ +6838aca280112635a2cbf93440f4c04212f58ee8 diff --git a/utils/sha1_hash/src/lib.rs b/utils/sha1_hash/src/lib.rs new file mode 100644 index 0000000..96a7897 --- /dev/null +++ b/utils/sha1_hash/src/lib.rs @@ -0,0 +1,257 @@ +use sha1::{Digest, Sha1}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, BufReader}; +use tokio::task; + +/// # Struct - Sha1Result +/// +/// Records SHA1 calculation results, including the file path and hash value +#[derive(Debug, Clone)] +pub struct Sha1Result { + pub file_path: PathBuf, + pub hash: String, +} + +/// Calc SHA1 hash of a string +pub fn calc_sha1_string<S: AsRef<str>>(input: S) -> String { + let mut hasher = Sha1::new(); + hasher.update(input.as_ref().as_bytes()); + let hash_result = hasher.finalize(); + + hash_result + .iter() + .map(|b| format!("{:02x}", b)) + .collect::<String>() +} + +/// Calc SHA1 hash of a single file +pub async fn calc_sha1<P: AsRef<Path>>( + path: P, + buffer_size: usize, +) -> Result<Sha1Result, Box<dyn std::error::Error + Send + Sync>> { + let file_path = path.as_ref().to_string_lossy().to_string(); + + // Open file asynchronously + let file = File::open(&path).await?; + let mut reader = BufReader::with_capacity(buffer_size, file); + let mut hasher = Sha1::new(); + let mut buffer = vec![0u8; buffer_size]; + + // Read file in chunks and update hash asynchronously + loop { + let n = reader.read(&mut buffer).await?; + if n == 0 { + break; + } + hasher.update(&buffer[..n]); + } + + let hash_result = hasher.finalize(); + + // Convert to hex string + let hash_hex = hash_result + .iter() + .map(|b| format!("{:02x}", b)) + .collect::<String>(); + + Ok(Sha1Result { + file_path: file_path.into(), + hash: hash_hex, + }) +} + +/// Calc SHA1 hashes for multiple files using multi-threading +pub async fn calc_sha1_multi<P, I>( + paths: I, + buffer_size: usize, +) -> Result<Vec<Sha1Result>, Box<dyn std::error::Error + Send + Sync>> +where + P: AsRef<Path> + Send + Sync + 'static, + I: IntoIterator<Item = P>, +{ + let buffer_size = Arc::new(buffer_size); + + // Collect all file paths + let file_paths: Vec<P> = paths.into_iter().collect(); + + if file_paths.is_empty() { + return Ok(Vec::new()); + } + + // Create tasks for each file + let tasks: Vec<_> = file_paths + .into_iter() + .map(|path| { + let buffer_size = Arc::clone(&buffer_size); + task::spawn(async move { calc_sha1(path, *buffer_size).await }) + }) + .collect(); + + // Execute tasks with concurrency limit using join_all + let results: Vec<Result<Sha1Result, Box<dyn std::error::Error + Send + Sync>>> = + futures::future::join_all(tasks) + .await + .into_iter() + .map(|task_result| match task_result { + Ok(Ok(calc_result)) => Ok(calc_result), + Ok(Err(e)) => Err(e), + Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>), + }) + .collect(); + + // Check for any errors and collect successful results + let mut successful_results = Vec::new(); + for result in results { + match result { + Ok(success) => successful_results.push(success), + Err(e) => return Err(e), + } + } + + Ok(successful_results) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_sha1_string() { + let test_string = "Hello, SHA1!"; + let hash = calc_sha1_string(test_string); + + let expected_hash = "de1c3daadc6f0f1626f4cf56c03e05a1e5d7b187"; + + assert_eq!( + hash, expected_hash, + "SHA1 hash should be consistent for same input" + ); + } + + #[test] + fn test_sha1_string_empty() { + let hash = calc_sha1_string(""); + + // SHA1 of empty string is "da39a3ee5e6b4b0d3255bfef95601890afd80709" + let expected_empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + assert_eq!( + hash, expected_empty_hash, + "SHA1 hash mismatch for empty string" + ); + } + + #[tokio::test] + async fn test_sha1_accuracy() { + // Test file path relative to the crate root + let test_file_path = "res/story.txt"; + // Choose expected hash file based on platform + let expected_hash_path = if cfg!(windows) { + "res/story_crlf.sha1" + } else { + "res/story_lf.sha1" + }; + + // Calculate SHA1 hash + let result = calc_sha1(test_file_path, 8192) + .await + .expect("Failed to calculate SHA1"); + + // Read expected hash from file + let expected_hash = fs::read_to_string(expected_hash_path) + .expect("Failed to read expected hash file") + .trim() + .to_string(); + + // Verify the calculated hash matches expected hash + assert_eq!( + result.hash, expected_hash, + "SHA1 hash mismatch for test file" + ); + + println!("Test file: {}", result.file_path.display()); + println!("Calculated hash: {}", result.hash); + println!("Expected hash: {}", expected_hash); + println!( + "Platform: {}", + if cfg!(windows) { + "Windows" + } else { + "Unix/Linux" + } + ); + } + + #[tokio::test] + async fn test_sha1_empty_file() { + // Create a temporary empty file for testing + let temp_file = "test_empty.txt"; + fs::write(temp_file, "").expect("Failed to create empty test file"); + + let result = calc_sha1(temp_file, 4096) + .await + .expect("Failed to calculate SHA1 for empty file"); + + // SHA1 of empty string is "da39a3ee5e6b4b0d3255bfef95601890afd80709" + let expected_empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + assert_eq!( + result.hash, expected_empty_hash, + "SHA1 hash mismatch for empty file" + ); + + // Clean up + fs::remove_file(temp_file).expect("Failed to remove temporary test file"); + } + + #[tokio::test] + async fn test_sha1_simple_text() { + // Create a temporary file with simple text + let temp_file = "test_simple.txt"; + let test_content = "Hello, SHA1!"; + fs::write(temp_file, test_content).expect("Failed to create simple test file"); + + let result = calc_sha1(temp_file, 4096) + .await + .expect("Failed to calculate SHA1 for simple text"); + + // Note: This test just verifies that the function works without errors + // The actual hash value is not critical for this test + + println!("Simple text test - Calculated hash: {}", result.hash); + + // Clean up + fs::remove_file(temp_file).expect("Failed to remove temporary test file"); + } + + #[tokio::test] + async fn test_sha1_multi_files() { + // Test multiple files calculation + let test_files = vec!["res/story.txt"]; + + let results = calc_sha1_multi(test_files, 8192) + .await + .expect("Failed to calculate SHA1 for multiple files"); + + assert_eq!(results.len(), 1, "Should have calculated hash for 1 file"); + + // Choose expected hash file based on platform + let expected_hash_path = if cfg!(windows) { + "res/story_crlf.sha1" + } else { + "res/story_lf.sha1" + }; + + // Read expected hash from file + let expected_hash = fs::read_to_string(expected_hash_path) + .expect("Failed to read expected hash file") + .trim() + .to_string(); + + assert_eq!( + results[0].hash, expected_hash, + "SHA1 hash mismatch in multi-file test" + ); + } +} |
