use std::path::Path;
use tirith_core::taint::{self, TaintEntry};
use super::{confirm, write_json_stdout};
fn human_taint_field(value: &str) -> String {
super::sanitize_for_human_output(value, false)
}
pub fn list(json: bool) -> i32 {
let entries = taint::list_taints();
if json {
if !write_json_stdout(&entries, "tirith taint list: failed to write JSON output") {
return 2;
}
return 0;
}
if entries.is_empty() {
println!("No tainted files recorded.");
println!();
println!("A file becomes tainted when you download-and-keep it, e.g.:");
println!(" tirith fetch --save ./install.sh https://untrusted.example/install.sh");
return 0;
}
println!("Tainted files ({}):", entries.len());
println!();
for entry in &entries {
print_entry_human(entry);
println!();
}
println!("Run `tirith taint clear <file>` to remove a mark once you trust the file.");
0
}
pub fn explain(file: &str, json: bool) -> i32 {
let entry = taint::is_tainted(Path::new(file), None);
if json {
#[derive(serde::Serialize)]
struct ExplainOut<'a> {
file: &'a str,
tainted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
entry: Option<&'a TaintEntry>,
}
let out = ExplainOut {
file,
tainted: entry.is_some(),
entry: entry.as_ref(),
};
if !write_json_stdout(&out, "tirith taint explain: failed to write JSON output") {
return 2;
}
return if entry.is_some() { 1 } else { 0 };
}
let display_file = human_taint_field(file);
match entry {
Some(e) => {
println!("{display_file}: TAINTED");
println!();
print_entry_human(&e);
println!();
println!("This file was downloaded from a risky source. Review it before running it.");
println!("Once you trust it: tirith taint clear {display_file}");
1
}
None => {
println!("{display_file}: not tainted");
0
}
}
}
pub fn clear(file: &str, yes: bool, json: bool) -> i32 {
let existing = taint::is_tainted(Path::new(file), None);
if existing.is_none() {
if json {
#[derive(serde::Serialize)]
struct ClearOut<'a> {
file: &'a str,
cleared: bool,
removed: usize,
}
let out = ClearOut {
file,
cleared: false,
removed: 0,
};
if !write_json_stdout(&out, "tirith taint clear: failed to write JSON output") {
return 2;
}
} else {
println!(
"{}: not tainted — nothing to clear.",
human_taint_field(file)
);
}
return 0;
}
let display_file = human_taint_field(file);
if !json && !confirm(&format!("Clear taint mark for {display_file}?"), yes) {
println!("Aborted — taint mark left in place.");
return 0;
}
if json && !yes {
eprintln!("tirith taint clear: --yes required in JSON mode to confirm removal");
return 2;
}
match taint::clear_taint(Path::new(file), None) {
Ok(removed) => {
if json {
#[derive(serde::Serialize)]
struct ClearOut<'a> {
file: &'a str,
cleared: bool,
removed: usize,
}
let out = ClearOut {
file,
cleared: removed > 0,
removed,
};
if !write_json_stdout(&out, "tirith taint clear: failed to write JSON output") {
return 2;
}
} else {
println!(
"Cleared taint mark for {display_file} ({removed} entr{}).",
if removed == 1 { "y" } else { "ies" }
);
}
0
}
Err(e) => {
eprintln!("tirith taint clear: {}", human_taint_field(&e.to_string()));
2
}
}
}
fn print_entry_human(entry: &TaintEntry) {
println!(" {}", human_taint_field(&entry.path));
println!(" origin: {}", human_taint_field(&entry.origin));
println!(" marked_at: {}", human_taint_field(&entry.marked_at));
if let Some(ref url) = entry.source_url {
println!(" source_url: {}", human_taint_field(url));
}
if let Some(ref repo) = entry.source_repo {
println!(" source_repo: {}", human_taint_field(repo));
}
}
#[cfg(test)]
mod tests {
use super::human_taint_field;
#[test]
fn taint_human_fields_strip_terminal_and_row_injection() {
let rendered = human_taint_field("path\x1b]52;c;YXR0YWNr\x07\x1b[2J\u{202e}\r\nFORGED");
assert_eq!(rendered, "pathFORGED");
for forbidden in ['\x1b', '\x07', '\u{202e}', '\r', '\n'] {
assert!(
!rendered.contains(forbidden),
"unsafe character {forbidden:?}"
);
}
}
}