use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant, SystemTime};
const ROW_TIMEOUT: Duration = Duration::from_secs(10);
fn frost_bin() -> &'static str {
env!("CARGO_BIN_EXE_frost")
}
struct Sandbox {
root: PathBuf,
bin: PathBuf,
}
impl Sandbox {
fn new(tag: &str) -> Self {
use std::os::unix::fs::PermissionsExt;
let root = std::env::temp_dir().join(format!(
"frost-path-enum-{tag}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0)
));
let bin = root.join("bin");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&bin).unwrap();
let exe = bin.join("sentinelcmd");
std::fs::write(&exe, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
Self { root, bin }
}
fn run_frost(&self, script: &str) -> (i32, String) {
let mut child = Command::new(frost_bin())
.arg("-c")
.arg(script)
.env_clear()
.env("PATH", &self.bin)
.env("HOME", &self.root)
.env("FROSTRC", "/dev/null")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn frost");
let started = Instant::now();
loop {
match child.try_wait().expect("try_wait") {
Some(_) => break,
None if started.elapsed() > ROW_TIMEOUT => {
let _ = child.kill();
let _ = child.wait();
panic!(
"frost -c {script:?} did not finish within {ROW_TIMEOUT:?} — \
a hang is a hard failure, not a slow test"
);
}
None => std::thread::sleep(Duration::from_millis(5)),
}
}
let out = child.wait_with_output().expect("wait_with_output");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn atime(dir: &Path) -> SystemTime {
std::fs::metadata(dir)
.expect("stat sandbox bin dir")
.accessed()
.expect("filesystem must report an access time")
}
fn arm_probe(dir: &Path) {
let marker = dir.join(".atime-probe-arm");
std::fs::write(&marker, b"").expect("write probe marker");
std::fs::remove_file(&marker).expect("remove probe marker");
std::thread::sleep(Duration::from_millis(20));
}
fn atime_tracks_readdir(dir: &Path) -> bool {
arm_probe(dir);
let before = atime(dir);
for e in std::fs::read_dir(dir).expect("read sandbox bin dir") {
let _ = e;
}
atime(dir) != before
}
#[test]
fn successful_command_does_not_enumerate_path() {
let sb = Sandbox::new("success");
if !atime_tracks_readdir(&sb.bin) {
eprintln!(
"SKIPPED: {} does not update directory atime on readdir \
(noatime/relatime mount) — the probe cannot see enumeration here",
sb.bin.display()
);
return;
}
arm_probe(&sb.bin);
let before = atime(&sb.bin);
let (code, stderr) = sb.run_frost(":");
assert_eq!(code, 0, "`:` must succeed; stderr={stderr}");
assert_eq!(
atime(&sb.bin),
before,
"a SUCCESSFUL command read the $PATH directory — the did-you-mean \
corpus is being built speculatively again"
);
arm_probe(&sb.bin);
let before = atime(&sb.bin);
let (code, stderr) = sb.run_frost("sentinelcm");
assert_eq!(
code, 127,
"an unknown command must exit 127; stderr={stderr}"
);
assert!(
stderr.contains("sentinelcmd"),
"the control must actually reach the suggestion path; stderr={stderr}"
);
assert_ne!(
atime(&sb.bin),
before,
"the FAILING command did not read the $PATH directory either — the \
atime probe is blind, so the assertion above proved nothing"
);
}
#[test]
fn command_not_found_suggests_a_symlinked_path_binary() {
let sb = Sandbox::new("symlink");
std::os::unix::fs::symlink(sb.bin.join("sentinelcmd"), sb.bin.join("linkedcmd")).unwrap();
let (code, stderr) = sb.run_frost("linkedcm");
assert_eq!(code, 127, "unknown command must exit 127; stderr={stderr}");
assert!(
stderr.contains("linkedcmd"),
"a symlinked PATH binary must be suggested; stderr={stderr}"
);
}
#[test]
fn no_row_hangs() {
let sb = Sandbox::new("nohang");
for script in [
":",
"true",
"definitely-not-a-command",
"eval \"$(printf %s \"export D=1\")\"",
] {
let (_code, _stderr) = sb.run_frost(script);
}
}