mod helpers;
use std::process::Command;
fn run(args: &[&str]) -> (Vec<u8>, Vec<u8>, Option<i32>) {
let o = Command::new(env!("CARGO_BIN_EXE_trg")).args(args).output().unwrap();
(o.stdout, o.stderr, o.status.code())
}
fn has(hay: &[u8], needle: &[u8]) -> bool {
hay.windows(needle.len()).any(|w| w == needle)
}
const EVIL: &[u8] = b"GET /x NEEDLE \x1b]0;PWNED\x07\x1b[2Jok\n";
#[test]
fn escape_always_makes_control_bytes_visible_in_a_line() {
let f = helpers::tgz("e.tgz", &[("logs/a.log", EVIL)]);
let (out, _, c) = run(&["--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
assert!(!out.contains(&0x1b), "a raw ESC reached the output: {out:?}");
assert!(!out.contains(&0x07), "a raw BEL reached the output: {out:?}");
assert!(has(&out, br"NEEDLE \x1b]0;PWNED\x07\x1b[2Jok"), "got: {}", String::from_utf8_lossy(&out));
assert_eq!(c, Some(0));
}
#[test]
fn escape_never_passes_the_bytes_through_unchanged() {
let f = helpers::tgz("e.tgz", &[("logs/a.log", EVIL)]);
let (out, _, _) = run(&["--escape", "never", "NEEDLE", f.path.to_str().unwrap()]);
assert!(has(&out, b"\x1b]0;PWNED\x07\x1b[2Jok"), "never must be byte-exact: {out:?}");
}
#[test]
fn piped_output_is_unescaped_by_default() {
let f = helpers::tgz("e.tgz", &[("logs/a.log", EVIL)]);
let (out, _, _) = run(&["NEEDLE", f.path.to_str().unwrap()]);
assert!(has(&out, b"\x1b]0;PWNED\x07"), "auto must be off on a pipe: {out:?}");
}
#[test]
fn a_member_name_is_escaped_too() {
let f = helpers::tgz("n.tgz", &[("logs/a\x1b[31m.log", b"NEEDLE\n")]);
let (out, _, _) = run(&["--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
assert!(!out.contains(&0x1b), "raw ESC from a member name: {out:?}");
assert!(has(&out, br"logs/a\x1b[31m.log:1:NEEDLE"), "got: {}", String::from_utf8_lossy(&out));
}
#[test]
fn list_members_escapes_member_names() {
let f = helpers::tgz("n.tgz", &[("logs/a\x1b[31m.log", b"x\n")]);
let (out, _, _) = run(&["--escape", "always", "-T", f.path.to_str().unwrap()]);
assert!(!out.contains(&0x1b), "raw ESC in -T output: {out:?}");
assert!(has(&out, br"logs/a\x1b[31m.log"), "got: {}", String::from_utf8_lossy(&out));
}
#[test]
fn our_own_colours_survive_escaping() {
let f = helpers::tgz("e.tgz", &[("logs/a.log", EVIL)]);
let (out, _, _) = run(&["--escape", "always", "--color", "always", "NEEDLE", f.path.to_str().unwrap()]);
assert!(out.contains(&0x1b), "colour escapes were lost: {out:?}");
assert!(has(&out, br"\x1b]0;PWNED"), "the log's escape was not neutralised: {out:?}");
assert!(!has(&out, b"\x1b]0;"), "a raw OSC from the log survived: {out:?}");
}
#[test]
fn a_lone_cr_is_escaped_but_crlf_is_kept() {
let f = helpers::tgz("r.tgz", &[("logs/a.log", b"secret\rNEEDLE cover\r\nNEEDLE dos\r\n")]);
let (out, _, _) = run(&["--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
let s = String::from_utf8_lossy(&out);
assert!(s.contains(r"secret\x0dNEEDLE cover"), "lone CR not escaped: {s:?}");
assert!(s.contains("NEEDLE dos\r\n"), "CRLF must stay a real line ending: {s:?}");
}
#[test]
fn utf8_c1_controls_are_escaped() {
let f = helpers::tgz("c.tgz", &[("logs/a.log", "NEEDLE \u{9b}2J é\n".as_bytes())]);
let (out, _, _) = run(&["--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
let s = String::from_utf8_lossy(&out);
assert!(s.contains(r"NEEDLE \u{9b}2J é"), "C1 not escaped, or ordinary UTF-8 mangled: {s:?}");
}
#[test]
fn tabs_and_plain_text_are_untouched() {
let f = helpers::tgz("t.tgz", &[("logs/a.log", b"NEEDLE\tcol2\tcol3\n")]);
let (out, _, _) = run(&["--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
assert!(has(&out, b"NEEDLE\tcol2\tcol3\n"), "got: {out:?}");
}
#[test]
fn diagnostics_on_stderr_are_escaped() {
let (_, err, c) = run(&["--escape", "always", "x", "/nonexistent/a\x1b[31mb"]);
assert!(!err.contains(&0x1b), "raw ESC on stderr: {err:?}");
assert!(has(&err, br"a\x1b[31mb"), "got: {}", String::from_utf8_lossy(&err));
assert_eq!(c, Some(2));
}
#[test]
fn a_bad_escape_value_is_an_error() {
let (_, err, c) = run(&["--escape", "sometimes", "x", "f.tgz"]);
assert_eq!(c, Some(2));
assert!(has(&err, b"--escape expects auto|always|never"), "got: {}", String::from_utf8_lossy(&err));
}
#[test]
fn json_output_stays_valid_json_when_escaped() {
let f = helpers::tgz("j.tgz", &[("logs/a.log", "NEEDLE \u{9b}31m del\x7f esc\x1b[2J\n".as_bytes())]);
let (out, _, _) = run(&["--json", "--escape", "always", "NEEDLE", f.path.to_str().unwrap()]);
let s = String::from_utf8_lossy(&out);
assert!(s.contains(r"\u009b31m del\u007f esc\u001b[2J"), "got: {s}");
assert!(!s.contains(r"\x7f") && !s.contains(r"\u{"), "non-JSON escape in JSON output: {s}");
assert!(!out.contains(&0x7f) && !has(&out, b"\xc2\x9b"), "raw DEL/C1 reached the output: {s}");
}