mod extensions;
fn clean_path(s: &str) -> String {
let mut normalized = String::with_capacity(s.len());
let mut last_was_slash = false;
for ch in s.chars() {
let ch = if ch == '\\' { '/' } else { ch };
if ch == '/' {
if last_was_slash {
continue;
}
last_was_slash = true;
} else {
last_was_slash = false;
}
normalized.push(ch);
}
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 mut out = short_hash(&cleaned);
let filename = cleaned.split('/').next_back().unwrap_or(&cleaned);
let parts: Vec<&str> = filename.split('.').collect();
if parts.len() > 1 && !(parts.len() == 2 && parts[0].is_empty()) {
let extension_parts = if parts[0].is_empty() {
&parts[2..]
} else {
&parts[1..]
};
if let Some(suffix) = extensions::safe_path_extension_suffix(extension_parts) {
for ext in suffix {
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_strips_untrusted_short_extensions() {
for path in ["file.secret", "file.passwd", "file.pass1234"] {
let redacted = redact_path(path);
assert_eq!(redacted.len(), 16);
assert!(!redacted.contains('.'));
}
}
#[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(".tar.gz"));
}
#[test]
fn test_redact_path_preserves_only_known_compound_suffixes() {
let redacted = redact_path("fixture.json.rs");
assert!(redacted.ends_with(".rs"));
assert!(!redacted.ends_with(".json.rs"));
}
#[test]
fn test_redact_path_drops_unsafe_final_extension() {
let redacted = redact_path("secret.rs.bak");
assert_eq!(redacted.len(), 16);
assert!(!redacted.contains(".rs"));
assert!(!redacted.contains(".bak"));
}
#[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_short_hash_normalizes_double_slashes() {
assert_eq!(short_hash("src/lib.rs"), short_hash("src//lib.rs"));
assert_eq!(short_hash("src/lib.rs"), short_hash("src///lib.rs"));
assert_eq!(short_hash("src/lib.rs"), short_hash("src\\\\lib.rs"));
assert_eq!(short_hash("src/lib.rs"), short_hash("src\\\\/lib.rs"));
}
#[test]
fn test_redact_path_normalizes_double_slashes() {
assert_eq!(redact_path("src/main.rs"), redact_path("src//main.rs"));
assert_eq!(redact_path("src/main.rs"), redact_path("src///main.rs"));
assert_eq!(redact_path("src/main.rs"), redact_path("src\\\\main.rs"));
assert_eq!(redact_path("src/main.rs"), redact_path("src\\\\/main.rs"));
}
#[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"));
}
}