#![cfg(unix)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
const CLEAN_PATH: &str = "/usr/bin:/bin:/usr/sbin:/sbin";
fn copy_binary_to(dest: &Path) -> PathBuf {
let src = env!("CARGO_BIN_EXE_git-prism");
fs::create_dir_all(dest.parent().unwrap()).unwrap();
fs::copy(src, dest).unwrap();
let mut perms = fs::metadata(dest).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(dest, perms).unwrap();
dest.to_path_buf()
}
fn install_and_read_target(exe: &Path, home: &Path) -> PathBuf {
let out = Command::new(exe)
.env("HOME", home)
.env("PATH", CLEAN_PATH)
.env("SHELL", "/bin/zsh")
.args(["shim", "install"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(b"n\n").unwrap();
child.wait_with_output()
})
.unwrap();
assert!(
out.status.success(),
"shim install failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let link = home.join(".local/share/git-prism/bin/git");
fs::read_link(&link).unwrap()
}
#[test]
fn cellar_dir_in_non_homebrew_path_must_not_retarget_to_unrelated_bin() {
let root = TempDir::new().unwrap();
let r = root.path();
let real_exe = copy_binary_to(&r.join("Cellar/work/git-prism"));
let unrelated = r.join("bin/git-prism");
fs::create_dir_all(unrelated.parent().unwrap()).unwrap();
fs::write(&unrelated, b"#!/bin/sh\necho WRONG BINARY\n").unwrap();
fs::set_permissions(&unrelated, fs::Permissions::from_mode(0o755)).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&real_exe, home.path());
let canon_target = fs::canonicalize(&target).unwrap_or(target.clone());
let canon_real = fs::canonicalize(&real_exe).unwrap();
let canon_unrelated = fs::canonicalize(&unrelated).unwrap();
assert_ne!(
canon_target,
canon_unrelated,
"shim was retargeted to an UNRELATED binary because of a stray `Cellar` \
directory in a non-Homebrew path; target={}",
target.display()
);
assert_eq!(
canon_target,
canon_real,
"shim must point at the binary the user actually ran; target={}",
target.display()
);
}
#[test]
fn cellar_marker_without_formula_version_bin_shape_must_not_rewrite() {
let root = TempDir::new().unwrap();
let r = root.path();
let real_exe = copy_binary_to(&r.join("Cellar/git-prism"));
let unrelated = r.join("bin/git-prism");
fs::create_dir_all(unrelated.parent().unwrap()).unwrap();
fs::write(&unrelated, b"#!/bin/sh\necho WRONG\n").unwrap();
fs::set_permissions(&unrelated, fs::Permissions::from_mode(0o755)).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&real_exe, home.path());
let canon_target = fs::canonicalize(&target).unwrap_or(target.clone());
let canon_real = fs::canonicalize(&real_exe).unwrap();
let canon_unrelated = fs::canonicalize(&unrelated).unwrap();
assert_ne!(
canon_target,
canon_unrelated,
"a path that does not match Cellar/<formula>/<version>/bin/<bin> was \
still rewritten to <prefix>/bin/<bin>; target={}",
target.display()
);
assert_eq!(
canon_target,
canon_real,
"degenerate Cellar path must point at the binary the user actually ran; target={}",
target.display()
);
}
fn run_status(exe: &Path, home: &Path) -> String {
let out = Command::new(exe)
.env("HOME", home)
.env("PATH", CLEAN_PATH)
.args(["shim", "status"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(out.status.success(), "shim status exited non-zero");
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn dangling_cellar_target_status_must_advise_reinstall() {
let bin = PathBuf::from(env!("CARGO_BIN_EXE_git-prism"));
let home = TempDir::new().unwrap();
let h = home.path();
let shim_dir = h.join(".local/share/git-prism/bin");
fs::create_dir_all(&shim_dir).unwrap();
let link = shim_dir.join("git");
let dangling_cellar = h.join("opt/homebrew/Cellar/git-prism/0.9.0/bin/git-prism");
std::os::unix::fs::symlink(&dangling_cellar, &link).unwrap();
let stdout = run_status(&bin, h);
assert!(
stdout.contains("git-prism shim install"),
"status on a dangling Cellar target must mention `git-prism shim install`; got: {stdout:?}"
);
assert!(
stdout.to_lowercase().contains("brew upgrade"),
"status on a dangling Cellar target must mention `brew upgrade`; got: {stdout:?}"
);
assert!(
stdout.to_lowercase().contains("broken link"),
"status on a dangling Cellar target must report as broken link; got: {stdout:?}"
);
}
#[test]
fn genuine_homebrew_cellar_layout_rewrites_to_stable_bin() {
let root = TempDir::new().unwrap();
let r = root.path();
let cellar_exe = copy_binary_to(&r.join("Cellar/git-prism/1.0.0/bin/git-prism"));
let stable = r.join("bin/git-prism");
fs::create_dir_all(stable.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&cellar_exe, &stable).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&cellar_exe, home.path());
assert_eq!(
fs::canonicalize(&target).unwrap(),
fs::canonicalize(&stable).unwrap(),
"genuine Homebrew Cellar exe should be rewritten to <prefix>/bin/<bin>; \
target={}",
target.display()
);
}
#[test]
fn cellar_with_extra_libexec_segment_must_not_rewrite() {
let root = TempDir::new().unwrap();
let r = root.path();
let real_exe = copy_binary_to(&r.join("Cellar/git-prism/1.0.0/libexec/bin/git-prism"));
let unrelated = r.join("bin/git-prism");
fs::create_dir_all(unrelated.parent().unwrap()).unwrap();
fs::write(&unrelated, b"#!/bin/sh\necho WRONG\n").unwrap();
fs::set_permissions(&unrelated, fs::Permissions::from_mode(0o755)).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&real_exe, home.path());
let canon_target = fs::canonicalize(&target).unwrap_or(target.clone());
let canon_real = fs::canonicalize(&real_exe).unwrap();
let canon_unrelated = fs::canonicalize(&unrelated).unwrap();
assert_ne!(
canon_target,
canon_unrelated,
"a libexec/bin Cellar layout (5 trailing components) was rewritten to \
<prefix>/bin/<bin>; target={}",
target.display()
);
assert_eq!(
canon_target,
canon_real,
"libexec/bin layout must be left unchanged; target={}",
target.display()
);
}
#[test]
fn cellar_at_n_minus_5_but_not_bin_at_n_minus_2_must_not_rewrite() {
let root = TempDir::new().unwrap();
let r = root.path();
let real_exe = copy_binary_to(&r.join("Cellar/git-prism/1.0.0/sbin/git-prism"));
let unrelated = r.join("bin/git-prism");
fs::create_dir_all(unrelated.parent().unwrap()).unwrap();
fs::write(&unrelated, b"#!/bin/sh\necho WRONG\n").unwrap();
fs::set_permissions(&unrelated, fs::Permissions::from_mode(0o755)).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&real_exe, home.path());
let canon_target = fs::canonicalize(&target).unwrap_or(target.clone());
let canon_real = fs::canonicalize(&real_exe).unwrap();
let canon_unrelated = fs::canonicalize(&unrelated).unwrap();
assert_ne!(
canon_target,
canon_unrelated,
"a Cellar layout with `sbin` (not `bin`) at n-2 was rewritten; target={}",
target.display()
);
assert_eq!(
canon_target,
canon_real,
"sbin layout must point at the binary the user actually ran; target={}",
target.display()
);
}
#[test]
fn rewrite_preserves_non_default_binary_filename() {
let root = TempDir::new().unwrap();
let r = root.path();
let cellar_exe = copy_binary_to(&r.join("Cellar/git-prism/1.0.0/bin/git-prism2"));
let stable = r.join("bin/git-prism2");
fs::create_dir_all(stable.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&cellar_exe, &stable).unwrap();
let decoy = r.join("bin/git-prism");
fs::write(&decoy, b"#!/bin/sh\necho DECOY\n").unwrap();
fs::set_permissions(&decoy, fs::Permissions::from_mode(0o755)).unwrap();
let home = TempDir::new().unwrap();
let target = install_and_read_target(&cellar_exe, home.path());
assert_eq!(
target.file_name().and_then(|s| s.to_str()),
Some("git-prism2"),
"rewritten target must keep the exe's own filename (git-prism2), not a \
hardcoded name; target={}",
target.display()
);
assert_eq!(
fs::canonicalize(&target).unwrap(),
fs::canonicalize(&stable).unwrap(),
"should rewrite to <prefix>/bin/git-prism2; target={}",
target.display()
);
}
#[test]
fn genuine_cellar_without_stable_bin_falls_back_to_canonical() {
let root = TempDir::new().unwrap();
let r = root.path();
let cellar_exe = copy_binary_to(&r.join("Cellar/git-prism/1.0.0/bin/git-prism"));
let home = TempDir::new().unwrap();
let target = install_and_read_target(&cellar_exe, home.path());
assert_eq!(
fs::canonicalize(&target).unwrap(),
fs::canonicalize(&cellar_exe).unwrap(),
"missing <prefix>/bin/<bin> must fall back to the canonical Cellar exe; \
target={}",
target.display()
);
}
#[test]
fn cargo_install_path_left_unchanged() {
let root = TempDir::new().unwrap();
let r = root.path();
let cargo_exe = copy_binary_to(&r.join(".cargo/bin/git-prism"));
let home = TempDir::new().unwrap();
let target = install_and_read_target(&cargo_exe, home.path());
assert_eq!(
fs::canonicalize(&target).unwrap(),
fs::canonicalize(&cargo_exe).unwrap(),
"cargo-install path (no Cellar) must be unchanged; target={}",
target.display()
);
}
#[test]
fn dangling_non_cellar_target_gets_no_cellar_advisory() {
let bin = PathBuf::from(env!("CARGO_BIN_EXE_git-prism"));
let home = TempDir::new().unwrap();
let h = home.path();
let shim_dir = h.join(".local/share/git-prism/bin");
fs::create_dir_all(&shim_dir).unwrap();
let link = shim_dir.join("git");
let dangling = h.join(".cargo/bin/git-prism");
std::os::unix::fs::symlink(&dangling, &link).unwrap();
let stdout = run_status(&bin, h);
assert!(
!stdout.to_lowercase().contains("brew upgrade"),
"a dangling non-Cellar target must not emit a Homebrew/Cellar advisory; \
got: {stdout:?}"
);
assert!(
stdout.to_lowercase().contains("broken link"),
"a dangling non-Cellar target must still report as broken link; got: {stdout:?}"
);
}