summaryrefslogtreecommitdiff
path: root/crates/utils
diff options
context:
space:
mode:
Diffstat (limited to 'crates/utils')
-rw-r--r--crates/utils/sha1_hash/Cargo.toml9
-rw-r--r--crates/utils/sha1_hash/res/story.sha11
-rw-r--r--crates/utils/sha1_hash/res/story.txt48
-rw-r--r--crates/utils/sha1_hash/src/lib.rs197
-rw-r--r--crates/utils/string_proc/src/format_path.rs44
5 files changed, 296 insertions, 3 deletions
diff --git a/crates/utils/sha1_hash/Cargo.toml b/crates/utils/sha1_hash/Cargo.toml
new file mode 100644
index 0000000..e206efd
--- /dev/null
+++ b/crates/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/crates/utils/sha1_hash/res/story.sha1 b/crates/utils/sha1_hash/res/story.sha1
new file mode 100644
index 0000000..c2e3213
--- /dev/null
+++ b/crates/utils/sha1_hash/res/story.sha1
@@ -0,0 +1 @@
+6838aca280112635a2cbf93440f4c04212f58ee8
diff --git a/crates/utils/sha1_hash/res/story.txt b/crates/utils/sha1_hash/res/story.txt
new file mode 100644
index 0000000..a91f467
--- /dev/null
+++ b/crates/utils/sha1_hash/res/story.txt
@@ -0,0 +1,48 @@
+魏曹者,程序员也,发稀甚于代码。
+忽接神秘电话曰:
+"贺君中彩,得长生之赐。"
+魏曹冷笑曰:"吾命尚不及下版之期。"
+
+翌日果得U盘。
+接入电脑,弹窗示曰:
+"点此确认,即获永生。"
+魏曹径点"永拒"。
+
+三月后,U盘自格其盘。
+进度条滞于九九。
+客服电话已成空号。
+魏曹乃知身可不死,然体内癌细胞亦得不灭。
+
+遂谒主请辞。
+主曰:"巧甚,公司正欲优化。"
+魏曹曰:"吾不死。"
+主目骤亮:"则可007至司闭。"
+
+魏曹始试诸死法。
+坠楼,卧医三月,账单令其愿死。
+饮鸩,肝肾永损,然终不得死。
+终决卧轨。
+
+择高铁最速者。
+司机探头曰:"兄台,吾亦不死身也。"
+"此车已碾如君者二十人矣。"
+
+二人遂坐轨畔对饮。
+司机曰:"知最讽者何?"
+"吾等永存,而所爱者皆逝矣。"
+
+魏曹忽得系统提示:
+"侦得用户消极求生,将启工模。"
+自是无日不毕KPI,否则遍尝绝症之苦。
+
+是日对镜整寿衣。
+忽见顶生一丝乌发。
+泫然泣下,此兆示其将复活一轮回。
+
+--- 忽忆DeepSeek尝作Footer曰:
+"文成而Hash1验,若星河之固。"
+遂取哈希值校之,
+字符流转如天河倒泻,
+终得"e3b0c44298fc1c14"之数。
+然文末数字竟阙如残月,
+方知此篇亦遭永劫轮回。
diff --git a/crates/utils/sha1_hash/src/lib.rs b/crates/utils/sha1_hash/src/lib.rs
new file mode 100644
index 0000000..4373d50
--- /dev/null
+++ b/crates/utils/sha1_hash/src/lib.rs
@@ -0,0 +1,197 @@
+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;
+
+#[derive(Debug, Clone)]
+pub struct Sha1Result {
+ pub file_path: PathBuf,
+ pub hash: 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;
+
+ #[tokio::test]
+ async fn test_sha1_accuracy() {
+ // Test file path relative to the crate root
+ let test_file_path = "res/story.txt";
+ let expected_hash_path = "res/story.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);
+ }
+
+ #[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");
+
+ // Read expected hash from file
+ let expected_hash = fs::read_to_string("res/story.sha1")
+ .expect("Failed to read expected hash file")
+ .trim()
+ .to_string();
+
+ assert_eq!(
+ results[0].hash, expected_hash,
+ "SHA1 hash mismatch in multi-file test"
+ );
+ }
+}
diff --git a/crates/utils/string_proc/src/format_path.rs b/crates/utils/string_proc/src/format_path.rs
index 2d8927b..a3493f5 100644
--- a/crates/utils/string_proc/src/format_path.rs
+++ b/crates/utils/string_proc/src/format_path.rs
@@ -3,6 +3,7 @@ use std::path::PathBuf;
/// Format path str
pub fn format_path_str(path: impl Into<String>) -> Result<String, std::io::Error> {
let path_str = path.into();
+ let ends_with_slash = path_str.ends_with('/');
// ANSI Strip
let cleaned = strip_ansi_escapes::strip(&path_str);
@@ -27,10 +28,43 @@ pub fn format_path_str(path: impl Into<String>) -> Result<String, std::io::Error
.filter(|c| !unfriendly_chars.contains(c))
.collect();
- if result.ends_with('/') {
- Ok(result)
+ // Handle ".." path components
+ let path_buf = PathBuf::from(&result);
+ let normalized_path = normalize_path(&path_buf);
+ result = normalized_path.to_string_lossy().replace('\\', "/");
+
+ // Restore trailing slash if original path had one
+ if ends_with_slash && !result.ends_with('/') {
+ result.push('/');
+ }
+
+ Ok(result)
+}
+
+/// Normalize path by resolving ".." components without requiring file system access
+fn normalize_path(path: &PathBuf) -> PathBuf {
+ let mut components = Vec::new();
+
+ for component in path.components() {
+ match component {
+ std::path::Component::ParentDir => {
+ if !components.is_empty() {
+ components.pop();
+ }
+ }
+ std::path::Component::CurDir => {
+ // Skip current directory components
+ }
+ _ => {
+ components.push(component);
+ }
+ }
+ }
+
+ if components.is_empty() {
+ PathBuf::from(".")
} else {
- Ok(result)
+ components.iter().collect()
}
}
@@ -58,6 +92,10 @@ mod tests {
format_path_str("/home/user/file.txt")?,
"/home/user/file.txt"
);
+ assert_eq!(
+ format_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?,
+ "/home/my_user/DOCS/JVCS_TEST/Vault/"
+ );
Ok(())
}