use std::fs;
use std::path::Path;
use std::process::Command;
#[must_use]
pub fn owner(path: &Path) -> Option<String> {
let program = std::env::var_os("CABOODLE_DPKG_QUERY").unwrap_or_else(|| "dpkg-query".into());
owner_with(Path::new(&program), path)
}
fn owner_with(program: &Path, path: &Path) -> Option<String> {
let resolved = fs::canonicalize(path).ok()?;
let out = Command::new(program)
.arg("-S")
.arg(&resolved)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let wanted = resolved.to_string_lossy();
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|line| {
let (packages, file) = line.split_once(": ")?;
if line.starts_with("diversion by") || file != wanted {
return None;
}
let first = packages.split(", ").next()?;
Some(first.split(':').next()?.to_string())
})
}
pub fn refuse_replacement(tool: &str, path: &Path) -> anyhow::Result<()> {
refuse_if_owned(tool, path, owner(path))
}
fn refuse_if_owned(tool: &str, path: &Path, owner: Option<String>) -> anyhow::Result<()> {
if let Some(package) = owner {
anyhow::bail!(
"{tool}: {} is managed by apt (package {package}); refusing to replace it. \
Update it with `sudo apt install --only-upgrade {package}`, or roll back with \
`sudo apt install {package}=<version>`",
path.display()
);
}
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::{symlink, PermissionsExt};
fn fake_dpkg_query(dir: &Path, body: &str) -> std::path::PathBuf {
let path = dir.join("dpkg-query");
fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[test]
fn a_symlink_to_a_package_file_is_owned_by_that_package() {
let dir = tempfile::tempdir().unwrap();
let packaged = dir.path().join("usr-bin-sd");
fs::write(&packaged, "binary").unwrap();
let link = dir.path().join("sd");
symlink(&packaged, &link).unwrap();
let resolved = fs::canonicalize(&packaged).unwrap();
let query = fake_dpkg_query(
dir.path(),
&format!(
"[ \"$2\" = '{}' ] && echo 'seeds:amd64: {}' && exit 0; exit 1",
resolved.display(),
resolved.display()
),
);
assert_eq!(owner_with(&query, &link).as_deref(), Some("seeds"));
let error = refuse_if_owned("seeds", &link, owner_with(&query, &link))
.unwrap_err()
.to_string();
assert!(error.contains("managed by apt (package seeds)"), "{error}");
let plain = dir.path().join("plain");
fs::write(&plain, "binary").unwrap();
assert_eq!(owner_with(&query, &plain), None);
assert!(refuse_if_owned("plain", &plain, owner_with(&query, &plain)).is_ok());
assert_eq!(owner_with(&dir.path().join("absent"), &link), None);
}
#[test]
fn diversions_and_other_paths_are_not_ownership() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("x");
fs::write(&file, "x").unwrap();
let resolved = fs::canonicalize(&file).unwrap();
let query = fake_dpkg_query(
dir.path(),
&format!(
"echo 'diversion by foo from: {0}'; echo 'other: {0}.bak'; exit 0",
resolved.display()
),
);
assert_eq!(owner_with(&query, &file), None);
}
}