404 lines
13 KiB
Rust
404 lines
13 KiB
Rust
//! Blitz harness for the bash installer (`install.sh`), the second client that
|
|
//! can brick a machine. Runs the REAL shipped `install.sh` against a fake
|
|
//! `curl` that can serve the good artifact, truncate it, or serve a right-length
|
|
//! garbage body, and asserts the same invariant as the Rust blitz:
|
|
//!
|
|
//! > After any install attempt, `$BIN_DIR/grok` resolves to a binary that runs,
|
|
//! > OR is still the previous-good binary — never a partial/garbage binary.
|
|
//!
|
|
//! Also covers shell-rc rewrite: stowed/symlinked `~/.bashrc` etc. must survive
|
|
//! reinstall without being replaced by a plain file.
|
|
//!
|
|
//! The installer lives in the sibling `xai-grok-pager` crate; it is resolved by
|
|
//! relative path. If it cannot be found (e.g. a sandbox that does not vendor it)
|
|
//! the test skips rather than fail — under the repo's `cargo nextest` workflow
|
|
//! the path resolves and the installer is exercised end to end.
|
|
|
|
#![cfg(unix)]
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn script_path(name: &str) -> Option<PathBuf> {
|
|
dunce::canonicalize(
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../xai-grok-pager/scripts/{name}")),
|
|
)
|
|
.ok()
|
|
.filter(|p| p.exists())
|
|
}
|
|
|
|
fn install_sh_path() -> Option<PathBuf> {
|
|
script_path("install.sh")
|
|
}
|
|
|
|
fn host_platform() -> String {
|
|
let os = if cfg!(target_os = "macos") {
|
|
"macos"
|
|
} else {
|
|
"linux"
|
|
};
|
|
let arch = if cfg!(target_arch = "x86_64") {
|
|
"x86_64"
|
|
} else {
|
|
"aarch64"
|
|
};
|
|
format!("{os}-{arch}")
|
|
}
|
|
|
|
const GOOD_SCRIPT: &str = "#!/bin/sh\nexit 0\n";
|
|
const INSTALLER_BLOCK_START: &str = "# >>> grok installer >>>";
|
|
|
|
/// Write a fake `curl` that intercepts every download `install.sh` performs.
|
|
/// `$FAKE_MODE` (full|truncate|garbage) selects the corruption.
|
|
fn write_fake_curl(dir: &Path) {
|
|
let body = format!(
|
|
r#"#!/bin/bash
|
|
mode="${{FAKE_MODE:-full}}"
|
|
fullsize={fullsize}
|
|
head=0; out=""; want_code=0; url=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--head) head=1 ;;
|
|
-o) shift; out="$1" ;;
|
|
-w) shift; [ "$1" = '%{{http_code}}' ] && want_code=1 ;;
|
|
-*) : ;;
|
|
*) url="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
if [ "$head" = 1 ]; then
|
|
if [ "$want_code" = 1 ]; then printf '200'; else printf 'HTTP/1.1 200 OK\r\nContent-Length: %s\r\n\r\n' "$fullsize"; fi
|
|
exit 0
|
|
fi
|
|
if [ -n "$out" ]; then
|
|
case "$mode" in
|
|
full) printf '%s' '{good}' > "$out" ;;
|
|
truncate) printf '\0\0\0\0' > "$out" ;;
|
|
garbage) head -c "$fullsize" /dev/zero | tr '\0' 'X' > "$out" ;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
printf '0.1.181'
|
|
exit 0
|
|
"#,
|
|
fullsize = GOOD_SCRIPT.len(),
|
|
good = GOOD_SCRIPT,
|
|
);
|
|
let path = dir.join("curl");
|
|
std::fs::write(&path, body).unwrap();
|
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
|
|
/// Seed a valid previous-good binary + symlink in the isolated home.
|
|
fn seed_previous_good(home: &Path, platform: &str) -> PathBuf {
|
|
let downloads = home.join(".grok").join("downloads");
|
|
let bin = home.join(".grok").join("bin");
|
|
std::fs::create_dir_all(&downloads).unwrap();
|
|
std::fs::create_dir_all(&bin).unwrap();
|
|
let prev = downloads.join(format!("grok-{platform}"));
|
|
std::fs::write(&prev, GOOD_SCRIPT).unwrap();
|
|
std::fs::set_permissions(&prev, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
let link = bin.join("grok");
|
|
let _ = std::fs::remove_file(&link);
|
|
std::os::unix::fs::symlink(format!("../downloads/grok-{platform}"), &link).unwrap();
|
|
dunce::canonicalize(&prev).unwrap()
|
|
}
|
|
|
|
/// Re-resolve `$BIN_DIR/grok` from disk and re-run it: the active grok must
|
|
/// always execute, and never be a `.tmp`/partial file.
|
|
fn assert_active_grok_runs(home: &Path) {
|
|
let link = home.join(".grok").join("bin").join("grok");
|
|
assert!(link.is_symlink(), "grok must remain a symlink");
|
|
let resolved =
|
|
dunce::canonicalize(&link).unwrap_or_else(|e| panic!("grok symlink dangles: {e}"));
|
|
let name = resolved.file_name().unwrap().to_string_lossy().to_string();
|
|
assert!(
|
|
!name.contains(".tmp"),
|
|
"active grok must not be a temp file: {name}"
|
|
);
|
|
let ok = Command::new(&resolved)
|
|
.arg("--version")
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false);
|
|
assert!(ok, "active grok must run: {}", resolved.display());
|
|
}
|
|
|
|
fn run_installer(install_sh: &Path, home: &Path, fakebin: &Path, mode: &str, shell: &str) -> bool {
|
|
let path_env = format!("{}:/usr/bin:/bin", fakebin.display());
|
|
let status = Command::new("/bin/bash")
|
|
.arg(install_sh)
|
|
.arg("0.1.181")
|
|
.env_clear()
|
|
.env("HOME", home)
|
|
.env("PATH", path_env)
|
|
.env("SHELL", shell)
|
|
.env("GROK_BIN_DIR", home.join(".grok").join("bin"))
|
|
.env("GROK_CHANNEL", "stable")
|
|
.env("FAKE_MODE", mode)
|
|
.status()
|
|
.expect("spawn bash install.sh");
|
|
status.success()
|
|
}
|
|
|
|
fn installer_block_count(body: &str) -> usize {
|
|
body.matches(INSTALLER_BLOCK_START).count()
|
|
}
|
|
|
|
fn assert_single_installer_block(path: &Path, preserved: Option<&str>) {
|
|
let body = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
|
panic!("read {}: {e}", path.display());
|
|
});
|
|
let n = installer_block_count(&body);
|
|
assert_eq!(
|
|
n,
|
|
1,
|
|
"{} must contain exactly one grok installer block, got {n}:\n{body}",
|
|
path.display()
|
|
);
|
|
if let Some(marker) = preserved {
|
|
assert!(
|
|
body.contains(marker),
|
|
"{} must keep pre-existing content ({marker:?}):\n{body}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum RcLayout {
|
|
Missing,
|
|
Plain,
|
|
StowAbsolute,
|
|
StowRelative,
|
|
/// `$root/user/.bashrc` → `../packages/bash/bashrc` (physical relative arm).
|
|
StowRelativeDotDot,
|
|
}
|
|
|
|
struct ShellRcCase {
|
|
name: &'static str,
|
|
script: &'static str,
|
|
shell: &'static str,
|
|
rc_name: &'static str,
|
|
stow_name: &'static str,
|
|
layout: RcLayout,
|
|
reinstall: bool,
|
|
}
|
|
|
|
/// Returns `(installer_home, rc_path, stow_target, expected_link_value)`.
|
|
fn setup_rc(
|
|
root: &Path,
|
|
case: &ShellRcCase,
|
|
) -> (PathBuf, PathBuf, Option<PathBuf>, Option<PathBuf>) {
|
|
let marker = "# user shell rc\n";
|
|
match case.layout {
|
|
RcLayout::Missing => {
|
|
let home = root.to_path_buf();
|
|
(home.clone(), home.join(case.rc_name), None, None)
|
|
}
|
|
RcLayout::Plain => {
|
|
let home = root.to_path_buf();
|
|
let rc_link = home.join(case.rc_name);
|
|
std::fs::write(&rc_link, marker).unwrap();
|
|
(home, rc_link, None, None)
|
|
}
|
|
RcLayout::StowAbsolute | RcLayout::StowRelative => {
|
|
let home = root.to_path_buf();
|
|
let stow_dir = home.join("dotfiles");
|
|
std::fs::create_dir_all(&stow_dir).unwrap();
|
|
let target = stow_dir.join(case.stow_name);
|
|
std::fs::write(&target, marker).unwrap();
|
|
let link_value = if matches!(case.layout, RcLayout::StowAbsolute) {
|
|
target.clone()
|
|
} else {
|
|
PathBuf::from(format!("dotfiles/{}", case.stow_name))
|
|
};
|
|
let rc_link = home.join(case.rc_name);
|
|
std::os::unix::fs::symlink(&link_value, &rc_link).unwrap();
|
|
(home, rc_link, Some(target), Some(link_value))
|
|
}
|
|
RcLayout::StowRelativeDotDot => {
|
|
// $HOME = root/user; package is a sibling of user (relative needs `..`).
|
|
let home = root.join("user");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
let target = root.join("packages/bash/bashrc");
|
|
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
|
|
std::fs::write(&target, marker).unwrap();
|
|
let link_value = PathBuf::from("../packages/bash/bashrc");
|
|
let rc_link = home.join(case.rc_name);
|
|
std::os::unix::fs::symlink(&link_value, &rc_link).unwrap();
|
|
(home, rc_link, Some(target), Some(link_value))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_shell_rc_case(case: &ShellRcCase) {
|
|
let Some(script) = script_path(case.script) else {
|
|
eprintln!(
|
|
"skipping {}: {} not found relative to crate",
|
|
case.name, case.script
|
|
);
|
|
return;
|
|
};
|
|
let platform = host_platform();
|
|
let fakedir = tempfile::tempdir().unwrap();
|
|
write_fake_curl(fakedir.path());
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
let (home_path, rc_path, stow_target, expected_link) = setup_rc(root.path(), case);
|
|
seed_previous_good(&home_path, &platform);
|
|
|
|
assert!(
|
|
run_installer(&script, &home_path, fakedir.path(), "full", case.shell),
|
|
"{}: first install should succeed",
|
|
case.name
|
|
);
|
|
|
|
if case.reinstall {
|
|
assert!(
|
|
run_installer(&script, &home_path, fakedir.path(), "full", case.shell),
|
|
"{}: reinstall should succeed",
|
|
case.name
|
|
);
|
|
}
|
|
|
|
match case.layout {
|
|
RcLayout::Missing | RcLayout::Plain => {
|
|
assert!(
|
|
rc_path.is_file() && !rc_path.is_symlink(),
|
|
"{}: {} must be a regular file",
|
|
case.name,
|
|
case.rc_name
|
|
);
|
|
let preserved = match case.layout {
|
|
RcLayout::Plain => Some("# user shell rc"),
|
|
_ => None,
|
|
};
|
|
assert_single_installer_block(&rc_path, preserved);
|
|
}
|
|
RcLayout::StowAbsolute | RcLayout::StowRelative | RcLayout::StowRelativeDotDot => {
|
|
assert!(
|
|
rc_path.is_symlink(),
|
|
"{}: {} must remain a symlink after install",
|
|
case.name,
|
|
case.rc_name
|
|
);
|
|
let link = std::fs::read_link(&rc_path).unwrap();
|
|
assert_eq!(
|
|
link,
|
|
*expected_link.as_ref().unwrap(),
|
|
"{}: symlink target must be unchanged",
|
|
case.name
|
|
);
|
|
let target = stow_target.as_ref().unwrap();
|
|
assert_single_installer_block(target, Some("# user shell rc"));
|
|
}
|
|
}
|
|
|
|
assert_active_grok_runs(&home_path);
|
|
}
|
|
|
|
#[test]
|
|
fn install_sh_blitz_keeps_grok_runnable_under_corruption() {
|
|
let Some(install_sh) = install_sh_path() else {
|
|
eprintln!("skipping: install.sh not found relative to crate; run under cargo");
|
|
return;
|
|
};
|
|
let platform = host_platform();
|
|
let fakedir = tempfile::tempdir().unwrap();
|
|
write_fake_curl(fakedir.path());
|
|
|
|
// Each entry: (mode, should the installer succeed?). Loop a few rounds so a
|
|
// re-install over an existing good install is also exercised.
|
|
let cases = [
|
|
("full", true),
|
|
("truncate", false),
|
|
("garbage", false),
|
|
("full", true),
|
|
("truncate", false),
|
|
("garbage", false),
|
|
("full", true),
|
|
];
|
|
|
|
for (mode, expect_ok) in cases {
|
|
let home = tempfile::tempdir().unwrap();
|
|
seed_previous_good(home.path(), &platform);
|
|
|
|
let ok = run_installer(&install_sh, home.path(), fakedir.path(), mode, "/bin/bash");
|
|
assert_eq!(
|
|
ok, expect_ok,
|
|
"install.sh mode={mode} exit success mismatch"
|
|
);
|
|
|
|
// The invariant holds regardless of which path was taken: the active
|
|
// grok always runs (new good binary on success, previous-good on
|
|
// rejection).
|
|
assert_active_grok_runs(home.path());
|
|
}
|
|
}
|
|
|
|
/// Shell-rc rewrite matrix: stow absolute/relative/`..`, plain, first-create, enterprise.
|
|
#[test]
|
|
fn install_sh_shell_rc_rewrite_matrix() {
|
|
let cases = [
|
|
ShellRcCase {
|
|
name: "stow absolute bashrc reinstall",
|
|
script: "install.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::StowAbsolute,
|
|
reinstall: true,
|
|
},
|
|
ShellRcCase {
|
|
name: "stow relative bashrc reinstall",
|
|
script: "install.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::StowRelative,
|
|
reinstall: true,
|
|
},
|
|
ShellRcCase {
|
|
name: "stow relative ../ bashrc reinstall",
|
|
script: "install.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::StowRelativeDotDot,
|
|
reinstall: true,
|
|
},
|
|
ShellRcCase {
|
|
name: "plain bashrc reinstall",
|
|
script: "install.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::Plain,
|
|
reinstall: true,
|
|
},
|
|
ShellRcCase {
|
|
name: "missing bashrc first install",
|
|
script: "install.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::Missing,
|
|
reinstall: false,
|
|
},
|
|
ShellRcCase {
|
|
name: "enterprise stow absolute bashrc reinstall",
|
|
script: "install-enterprise.sh",
|
|
shell: "/bin/bash",
|
|
rc_name: ".bashrc",
|
|
stow_name: "bashrc",
|
|
layout: RcLayout::StowAbsolute,
|
|
reinstall: true,
|
|
},
|
|
];
|
|
|
|
for case in &cases {
|
|
run_shell_rc_case(case);
|
|
}
|
|
}
|