use std::{
env,
ffi::{CStr, CString},
fs,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, Context, Result};
use log::info;
pub enum Outcome {
Installed,
AlreadyCurrent,
}
pub fn install_to(src: &Path, dest: &Path) -> Result<Outcome> {
let src = canonical(src)?;
if dest_matches_running(&src, dest) {
return Ok(Outcome::AlreadyCurrent);
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating `{}`", parent.display()))?;
}
let tmp = dest.with_extension("linprov-new");
fs::copy(&src, &tmp)
.with_context(|| format!("copying `{}` -> `{}`", src.display(), tmp.display()))?;
fs::set_permissions(&tmp, fs::Permissions::from_mode(0o755))
.with_context(|| format!("chmod 0755 `{}`", tmp.display()))?;
fs::rename(&tmp, dest)
.with_context(|| format!("renaming `{}` -> `{}`", tmp.display(), dest.display()))?;
info!("installed {} -> {}", src.display(), dest.display());
Ok(Outcome::Installed)
}
fn canonical(p: &Path) -> Result<PathBuf> {
p.canonicalize()
.with_context(|| format!("resolving `{}`", p.display()))
}
fn dest_matches_running(src: &Path, dest: &Path) -> bool {
let Ok(src_meta) = fs::metadata(src) else {
return false;
};
let Ok(dest_meta) = fs::metadata(dest) else {
return false;
};
src_meta.len() == dest_meta.len() && fs::read(src).ok() == fs::read(dest).ok()
}
pub fn current_exe() -> Result<PathBuf> {
std::env::current_exe().context("locating the running linprov binary")
}
pub fn cargo_install_source() -> Option<PathBuf> {
for user in candidate_users() {
if let Some(p) = cargo_bin_for_user(&user) {
return Some(p);
}
}
let euid = unsafe { libc::geteuid() };
if let Some(home) = home_dir_for_uid(euid) {
let p = home.join(".cargo").join("bin").join("linprov");
if p.exists() {
return Some(p);
}
}
scan_human_homes_for_cargo_bin()
}
fn candidate_users() -> Vec<String> {
let mut v = Vec::new();
let mut push = |s: String| {
if !s.is_empty() && s != "root" && !v.contains(&s) {
v.push(s);
}
};
if let Ok(u) = env::var("SUDO_USER") {
push(u);
}
if let Ok(u) = env::var("DOAS_USER") {
push(u);
}
if let Ok(uid_str) = env::var("PKEXEC_UID") {
if let Ok(uid) = uid_str.parse::<u32>() {
if let Some(u) = username_for_uid(uid) {
push(u);
}
}
}
if let Some(u) = run_logname() {
push(u);
}
v
}
fn cargo_bin_for_user(user: &str) -> Option<PathBuf> {
let home = home_dir_for(user)?;
let p = home.join(".cargo").join("bin").join("linprov");
p.exists().then_some(p)
}
fn run_logname() -> Option<String> {
let out = Command::new("/usr/bin/logname").output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
let t = s.trim();
if t.is_empty() {
None
} else {
Some(t.to_string())
}
}
fn scan_human_homes_for_cargo_bin() -> Option<PathBuf> {
let content = fs::read_to_string("/etc/passwd").ok()?;
let candidates: Vec<PathBuf> = content
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() < 6 {
return None;
}
let uid: u32 = parts[2].parse().ok()?;
if !(1000..65534).contains(&uid) {
return None;
}
let p = PathBuf::from(parts[5]).join(".cargo/bin/linprov");
p.exists().then_some(p)
})
.collect();
if candidates.len() == 1 {
candidates.into_iter().next()
} else {
None
}
}
fn home_dir_for(user: &str) -> Option<PathBuf> {
let cname = CString::new(user).ok()?;
let pw = unsafe { libc::getpwnam(cname.as_ptr()) };
pw_home(pw)
}
fn home_dir_for_uid(uid: libc::uid_t) -> Option<PathBuf> {
let pw = unsafe { libc::getpwuid(uid) };
pw_home(pw)
}
fn username_for_uid(uid: u32) -> Option<String> {
let pw = unsafe { libc::getpwuid(uid as libc::uid_t) };
if pw.is_null() {
return None;
}
let n = unsafe { (*pw).pw_name };
if n.is_null() {
return None;
}
let cstr = unsafe { CStr::from_ptr(n) };
cstr.to_str().ok().map(String::from)
}
fn pw_home(pw: *mut libc::passwd) -> Option<PathBuf> {
if pw.is_null() {
return None;
}
let dir = unsafe { (*pw).pw_dir };
if dir.is_null() {
return None;
}
let cstr = unsafe { CStr::from_ptr(dir) };
Some(PathBuf::from(cstr.to_str().ok()?))
}
pub fn refuse_distro_owned(dest: &Path) -> Result<()> {
if !dest.exists() {
return Ok(());
}
if let Ok(out) = Command::new("/usr/bin/dpkg").arg("-S").arg(dest).output() {
if out.status.success() {
let pkg = String::from_utf8_lossy(&out.stdout).trim().to_string();
return Err(anyhow!(
"{} is owned by a dpkg package ({pkg}); refusing to overwrite. \
Uninstall that package first.",
dest.display()
));
}
}
Ok(())
}