use std::ffi::OsStr;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use crate::home::{self, Home};
use crate::install::other_name;
use crate::integrity::Lock;
const NAMES: [&str; 2] = ["openmoat", "moat"];
pub(super) fn refuse_copy(command: &str) -> Result<()> {
let mut locks: Vec<PathBuf> = Vec::new();
if let Ok(home) = Home::locate() {
locks.push(home.lock_path());
}
if let Ok(user) = home::user_home() {
let default = Home::under(&user).lock_path();
if !locks.contains(&default) {
locks.push(default);
}
}
let exe = std::env::current_exe().ok();
let invoked = std::env::args_os().next().map(PathBuf::from);
check(command, &locks, exe.as_deref(), invoked.as_deref(), |p| {
std::fs::canonicalize(p).ok()
})
}
fn check(
command: &str,
locks: &[PathBuf],
exe: Option<&Path>,
invoked: Option<&Path>,
canonical: impl Fn(&Path) -> Option<PathBuf>,
) -> Result<()> {
let mut recorded = Vec::new();
for path in locks {
match std::fs::read(path) {
Ok(bytes) => match Lock::parse(&bytes, path) {
Ok(lock) => recorded.push(PathBuf::from(&lock.binary)),
Err(error) => bail!(
"`{command}` cannot check which binary the policy lock records: {error:#}. \
Run `openmoat doctor` from the installed openmoat"
),
},
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => bail!(
"`{command}` cannot read the policy lock {}: {error}. Run it from the \
installed openmoat once the lock can be read",
path.display()
),
}
}
if recorded.is_empty() {
return Ok(());
}
let Some(exe) = exe else {
bail!("`{command}` cannot check which binary is running; run the installed openmoat");
};
if !invoked.is_some_and(started_by_name) {
bail!(
"`{command}` must be started as `openmoat` (or `moat`), the installed binary's own \
name; this was started as {}",
invoked.map_or_else(|| "nothing".into(), |p| p.display().to_string())
);
}
for binary in &recorded {
if !is_recorded(exe, binary, &canonical) {
bail!(
"`{command}` must be run by the installed openmoat, {}; this is {}. Run it from \
there, or run `openmoat init` from this binary to point the hooks at it",
binary.display(),
exe.display()
);
}
}
Ok(())
}
fn started_by_name(invoked: &Path) -> bool {
let named = |part: Option<&OsStr>| {
part.and_then(OsStr::to_str)
.is_some_and(|n| NAMES.contains(&n))
};
let exe = cfg!(windows)
&& invoked
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("exe"))
&& named(invoked.file_stem());
named(invoked.file_name()) || exe
}
fn is_recorded(exe: &Path, recorded: &Path, canonical: impl Fn(&Path) -> Option<PathBuf>) -> bool {
let Some(running) = canonical(exe) else {
return false;
};
std::iter::once(recorded.to_path_buf())
.chain(other_name(recorded))
.any(|path| canonical(&path).as_ref() == Some(&running))
}
#[cfg(test)]
mod tests;