use std::path::Path;
fn clean_path(s: &str) -> String {
let mut normalized = s.replace('\\', "/");
while let Some(stripped) = normalized.strip_prefix("./") {
normalized = stripped.to_string();
}
while normalized.contains("/./") {
normalized = normalized.replace("/./", "/");
}
if normalized.ends_with("/.") {
normalized.truncate(normalized.len() - 2);
}
normalized
}
pub fn short_hash(s: &str) -> String {
let cleaned = clean_path(s);
let mut hex = blake3::hash(cleaned.as_bytes()).to_hex().to_string();
hex.truncate(16);
hex
}
pub fn redact_path(path: &str) -> String {
let cleaned = clean_path(path);
let ext = Path::new(&cleaned)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let ext = if ext.len() <= 8 && ext.chars().all(|c| c.is_ascii_alphanumeric()) {
ext
} else {
""
};
let mut out = short_hash(&cleaned);
if !ext.is_empty() {
out.push('.');
out.push_str(ext);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_short_hash_length() {
let hash = short_hash("test");
assert_eq!(hash.len(), 16);
}
#[test]
fn test_short_hash_deterministic() {
let h1 = short_hash("same input");
let h2 = short_hash("same input");
assert_eq!(h1, h2);
}
#[test]
fn test_short_hash_different_inputs() {
let h1 = short_hash("input1");
let h2 = short_hash("input2");
assert_ne!(h1, h2);
}
#[test]
fn test_redact_path_preserves_extension() {
let redacted = redact_path("src/lib.rs");
assert!(redacted.ends_with(".rs"));
}
#[test]
fn test_redact_path_no_extension() {
let redacted = redact_path("Makefile");
assert_eq!(redacted.len(), 16);
assert!(!redacted.contains('.'));
}
#[test]
fn test_redact_path_double_extension() {
let redacted = redact_path("archive.tar.gz");
assert!(redacted.ends_with(".gz"));
}
#[test]
fn test_redact_path_deterministic() {
let r1 = redact_path("src/main.rs");
let r2 = redact_path("src/main.rs");
assert_eq!(r1, r2);
}
#[test]
fn test_short_hash_normalizes_separators() {
let h1 = short_hash("src/lib");
let h2 = short_hash("src\\lib");
assert_eq!(h1, h2);
}
#[test]
fn test_short_hash_normalizes_mixed_separators() {
let h1 = short_hash("crates/foo/src/lib");
let h2 = short_hash("crates\\foo\\src\\lib");
let h3 = short_hash("crates/foo\\src/lib");
assert_eq!(h1, h2);
assert_eq!(h2, h3);
}
#[test]
fn test_redact_path_normalizes_separators() {
let r1 = redact_path("src/main.rs");
let r2 = redact_path("src\\main.rs");
assert_eq!(r1, r2);
}
#[test]
fn test_redact_path_normalizes_deep_paths() {
let r1 = redact_path("crates/tokmd/src/commands/run.rs");
let r2 = redact_path("crates\\tokmd\\src\\commands\\run.rs");
assert_eq!(r1, r2);
assert!(r1.ends_with(".rs"));
}
#[test]
fn test_short_hash_normalizes_dot_prefix() {
assert_eq!(short_hash("src/lib.rs"), short_hash("./src/lib.rs"));
}
#[test]
fn test_short_hash_normalizes_interior_dot_segments() {
assert_eq!(
short_hash("crates/foo/./src/lib.rs"),
short_hash("crates/foo/src/lib.rs")
);
}
#[test]
fn test_redact_path_normalizes_dot_prefix() {
assert_eq!(redact_path("src/main.rs"), redact_path("./src/main.rs"));
}
}