#[allow(dead_code)]
pub fn redact_paths(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '/'
&& (result.is_empty()
|| result.ends_with(|ch: char| ch.is_whitespace() || ch == '"' || ch == '\''))
{
if let Some(&next) = chars.peek()
&& (next.is_alphanumeric() || next == '~')
{
let mut path = String::from(c);
path.push(next);
chars.next();
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == '\n' {
break;
}
path.push(ch);
chars.next();
}
if path.contains('/') && path.len() > 3 {
result.push_str("<PATH>");
} else {
result.push_str(&path);
}
continue;
}
result.push(c);
} else {
result.push(c);
}
}
result
}
#[allow(dead_code)]
pub fn redact_env_tokens(text: &str) -> String {
let mut result = String::new();
for line in text.lines() {
let redacted =
if line.contains("TOKEN=") || line.contains("KEY=") || line.contains("SECRET=") {
if let Some(eq_pos) = line.find('=') {
format!("{}=<REDACTED>", &line[..eq_pos])
} else {
line.to_string()
}
} else {
line.to_string()
};
if !result.is_empty() {
result.push('\n');
}
result.push_str(&redacted);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_unix_paths() {
let input = "align reads from /home/user/data/sample_R1.fastq.gz to reference";
let result = redact_paths(input);
assert!(result.contains("<PATH>"), "expected redaction in: {result}");
assert!(!result.contains("/home/user"), "path should be redacted");
}
#[test]
fn test_preserve_relative_paths() {
let input = "output to results/aligned/sample.bam";
let result = redact_paths(input);
assert_eq!(result, input, "relative paths should be preserved");
}
#[test]
fn test_redact_env_tokens() {
let input = "export OPENAI_API_KEY=sk-abc123xyz\nexport PATH=/usr/bin";
let result = redact_env_tokens(input);
assert!(result.contains("<REDACTED>"), "token should be redacted");
assert!(
!result.contains("sk-abc123xyz"),
"token value should be removed"
);
assert!(
result.contains("PATH="),
"non-secret env should be preserved"
);
}
#[test]
fn test_redact_multiple_paths() {
let input = "cp /home/user/input.bam /data/output.bam";
let result = redact_paths(input);
assert!(
!result.contains("/home/user"),
"first path should be redacted"
);
assert!(
!result.contains("/data/output"),
"second path should be redacted"
);
}
#[test]
fn test_redact_paths_preserves_flags() {
let input = "samtools sort --threads 8 --sort-by coordinate";
let result = redact_paths(input);
assert!(result.contains("--threads"));
assert!(result.contains("--sort-by"));
}
#[test]
fn test_redact_paths_empty_string() {
let result = redact_paths("");
assert_eq!(result, "");
}
#[test]
fn test_redact_env_tokens_secret() {
let input = "MY_SECRET=supersecretvalue";
let result = redact_env_tokens(input);
assert!(result.contains("<REDACTED>"));
assert!(!result.contains("supersecretvalue"));
}
#[test]
fn test_redact_env_tokens_multiline() {
let input = "NORMAL_VAR=hello\nAPI_TOKEN=abc123\nANOTHER=value";
let result = redact_env_tokens(input);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("NORMAL_VAR=hello"));
assert!(lines[1].contains("<REDACTED>"));
assert!(lines[2].contains("ANOTHER=value"));
}
#[test]
fn test_redact_env_tokens_no_match() {
let input = "NORMAL_VARIABLE=just_a_value\nFOO=bar";
let result = redact_env_tokens(input);
assert!(!result.contains("<REDACTED>"));
assert!(result.contains("NORMAL_VARIABLE=just_a_value"));
assert!(result.contains("FOO=bar"));
}
#[test]
fn test_redact_paths_short_path_not_redacted() {
let input = "run /x binary";
let result = redact_paths(input);
assert!(result.contains("/x"), "short path should not be redacted");
}
#[test]
fn test_redact_paths_slash_followed_by_space_not_consumed() {
let input = "a / b";
let result = redact_paths(input);
assert!(result.contains('/'), "standalone slash should be preserved");
}
#[test]
fn test_redact_paths_slash_at_start_of_string() {
let input = "/short";
let result = redact_paths(input);
assert!(!result.is_empty());
}
}