summaryrefslogtreecommitdiff
path: root/utils/sha1_hash
diff options
context:
space:
mode:
Diffstat (limited to 'utils/sha1_hash')
-rw-r--r--utils/sha1_hash/Cargo.toml9
-rw-r--r--utils/sha1_hash/res/story.txt48
-rw-r--r--utils/sha1_hash/res/story_crlf.sha11
-rw-r--r--utils/sha1_hash/res/story_lf.sha11
-rw-r--r--utils/sha1_hash/src/lib.rs257
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"
+ );
+ }
+}