use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
use crate::reliability::harness::binary;
fn scan_bounded(path: &Path, extra: &[&str], secs: u64) -> (Option<i32>, bool) {
let mut args: Vec<String> = vec!["scan".into(), "--no-daemon".into()];
for e in extra {
args.push((*e).into());
}
args.push(path.to_string_lossy().into_owned());
let mut child = Command::new(binary())
.args(&args)
.env("KEYHOG_RELEASE_API_BASE", "http://127.0.0.1:9")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn keyhog scan");
let start = Instant::now();
loop {
if let Some(status) = child.try_wait().expect("try_wait") {
return (status.code(), false);
}
if start.elapsed() > Duration::from_secs(secs) {
let _ = child.kill();
let _ = child.wait();
return (None, true);
}
std::thread::sleep(Duration::from_millis(25));
}
}
fn assert_handled(path: &Path, extra: &[&str], secs: u64, what: &str) {
let (code, timed_out) = scan_bounded(path, extra, secs);
assert!(!timed_out, "{what}: keyhog HUNG (> {secs}s) on this input");
assert!(code.is_some(), "{what}: keyhog crashed (killed by signal)");
let c = code.unwrap();
assert!(
[0, 1, 2, 3].contains(&c),
"{what}: undocumented/abnormal exit {c}"
);
}
#[test]
fn empty_file() {
let d = TempDir::new().unwrap();
let p = d.path().join("empty.txt");
std::fs::write(&p, b"").unwrap();
assert_handled(&p, &["--format", "json"], 30, "empty file");
}
#[test]
fn all_nul_bytes() {
let d = TempDir::new().unwrap();
let p = d.path().join("nul.bin");
std::fs::write(&p, vec![0u8; 64 * 1024]).unwrap();
assert_handled(&p, &["--format", "json"], 30, "all-NUL file");
}
#[test]
fn invalid_utf8_bytes() {
let d = TempDir::new().unwrap();
let p = d.path().join("bad.bin");
std::fs::write(&p, vec![0xC0, 0xC1, 0xF5, 0xFF, 0xFE, 0x80, 0x81]).unwrap();
assert_handled(&p, &["--format", "json"], 30, "invalid UTF-8");
}
#[test]
fn random_binary_blob() {
let d = TempDir::new().unwrap();
let p = d.path().join("blob.bin");
let bytes: Vec<u8> = (0..256u32)
.flat_map(|i| (i as u8).wrapping_mul(31).to_le_bytes())
.cycle()
.take(512 * 1024)
.collect();
std::fs::write(&p, bytes).unwrap();
assert_handled(&p, &["--format", "json"], 30, "random binary blob");
}
#[test]
fn one_huge_line() {
let d = TempDir::new().unwrap();
let p = d.path().join("huge.txt");
std::fs::write(&p, "A".repeat(4 * 1024 * 1024)).unwrap();
assert_handled(&p, &["--format", "json"], 60, "4MB single line");
}
#[test]
fn millions_of_short_lines() {
let d = TempDir::new().unwrap();
let p = d.path().join("lines.txt");
let body = "x\n".repeat(500_000);
std::fs::write(&p, body).unwrap();
assert_handled(&p, &["--format", "json"], 60, "500k short lines");
}
#[test]
fn crlf_line_endings() {
let d = TempDir::new().unwrap();
let p = d.path().join("crlf.txt");
std::fs::write(
&p,
"line one\r\nAWS_ACCESS_KEY_ID = \"AKIAQYLPMN5HFIQR7XYA\"\r\n",
)
.unwrap();
assert_handled(&p, &["--format", "json"], 30, "CRLF file");
}
#[test]
fn utf8_bom_prefix() {
let d = TempDir::new().unwrap();
let p = d.path().join("bom.txt");
let mut bytes = vec![0xEF, 0xBB, 0xBF];
bytes.extend_from_slice(b"AWS_ACCESS_KEY_ID = \"AKIAQYLPMN5HFIQR7XYA\"\n");
std::fs::write(&p, bytes).unwrap();
assert_handled(&p, &["--format", "json"], 30, "UTF-8 BOM file");
}
#[test]
fn emoji_and_unicode_filename() {
let d = TempDir::new().unwrap();
let p = d.path().join("secret-🔑-café-日本語.env");
std::fs::write(&p, "API=clean\n").unwrap();
assert_handled(&p, &["--format", "json"], 30, "emoji/unicode filename");
}
#[test]
fn very_long_filename() {
let d = TempDir::new().unwrap();
let name = format!("{}.txt", "a".repeat(200));
let p = d.path().join(name);
std::fs::write(&p, "clean\n").unwrap();
assert_handled(&p, &["--format", "json"], 30, "200-char filename");
}
#[test]
fn dev_null_as_path() {
assert_handled(
Path::new("/dev/null"),
&["--format", "json"],
30,
"/dev/null",
);
}
#[test]
fn deeply_nested_directories() {
let d = TempDir::new().unwrap();
let mut path = d.path().to_path_buf();
for i in 0..40 {
path = path.join(format!("dir{i}"));
}
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join("deep.txt"), "clean\n").unwrap();
assert_handled(
d.path(),
&["--format", "json"],
60,
"40-deep directory tree",
);
}
#[test]
fn symlink_loop_does_not_hang() {
let d = TempDir::new().unwrap();
let sub = d.path().join("a").join("b");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(d.path().join("a").join("file.txt"), "clean\n").unwrap();
let link = sub.join("loop");
if std::os::unix::fs::symlink(d.path().join("a"), &link).is_ok() {
assert_handled(d.path(), &["--format", "json"], 30, "symlink loop");
}
}
#[test]
fn filename_that_looks_like_a_flag_via_separator() {
let d = TempDir::new().unwrap();
let weird = d.path().join("--output");
std::fs::write(&weird, "clean\n").unwrap();
let (code, timed_out) = scan_bounded(&weird, &["--format", "json", "--"], 30);
let _ = (code, timed_out);
let out = Command::new(binary())
.args(["scan", "--no-daemon", "--format", "json", "--"])
.arg(&weird)
.env("KEYHOG_RELEASE_API_BASE", "http://127.0.0.1:9")
.output()
.unwrap();
assert!(
out.status.code().is_some(),
"scanning a file named --output crashed"
);
assert!(
[0, 1, 2, 3].contains(&out.status.code().unwrap()),
"scanning a flag-like filename gave abnormal exit {:?}",
out.status.code()
);
}
#[test]
fn unreadable_directory_is_skipped_not_fatal() {
use std::os::unix::fs::PermissionsExt;
let d = TempDir::new().unwrap();
let readable = d.path().join("readable.txt");
std::fs::write(&readable, "clean\n").unwrap();
let locked = d.path().join("locked");
std::fs::create_dir(&locked).unwrap();
std::fs::write(locked.join("inside.txt"), "clean\n").unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let (code, timed_out) = scan_bounded(d.path(), &["--format", "json"], 30);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(!timed_out, "scan hung on an unreadable subdir");
assert!(
code.is_some() && [0, 1, 2, 3].contains(&code.unwrap()),
"an unreadable subdir should warn-and-continue, got {code:?}"
);
}