use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::path::Path;
const DPKG_INFO_DIR: &str = "/var/lib/dpkg/info";
const DPKG_STATUS_FILE: &str = "/var/lib/dpkg/status";
pub(crate) fn find_packages_for_paths<'a>(
paths: impl IntoIterator<Item = impl Deref<Target = &'a str>>,
) -> Result<HashMap<String, String>> {
find_packages_for_paths_in_dir(paths, Path::new(DPKG_INFO_DIR))
}
fn find_packages_for_paths_in_dir<'a>(
paths: impl IntoIterator<Item = impl Deref<Target = &'a str>>,
info_dir: &Path,
) -> Result<HashMap<String, String>> {
let mut path_to_package = HashMap::new();
let mut search_paths = Vec::new();
for path in paths {
let path_str = *path;
search_paths.push(path_str.to_string());
search_paths.push(format!("/usr{}", path_str));
}
if !info_dir.exists() {
anyhow::bail!("dpkg info directory not found: {:?}", info_dir);
}
for entry in std::fs::read_dir(info_dir)? {
let entry = entry?;
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if !file_name_str.ends_with(".list") {
continue;
}
let package_full = &file_name_str[..file_name_str.len() - 5];
let package_name = if let Some(arch_pos) = package_full.rfind(':') {
&package_full[..arch_pos]
} else {
package_full
};
let file = File::open(entry.path())?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
for search_path in &search_paths {
if trimmed == search_path {
path_to_package.insert(trimmed.to_string(), package_name.to_string());
if let Some(stripped) = trimmed.strip_prefix("/usr") {
path_to_package.insert(stripped.to_string(), package_name.to_string());
}
if !trimmed.starts_with("/usr") {
let usr_version = format!("/usr{}", trimmed);
if search_paths.contains(&usr_version) {
path_to_package.insert(trimmed.to_string(), package_name.to_string());
}
}
}
}
}
}
Ok(path_to_package)
}
pub(crate) fn get_package_versions<'a>(
package_names: impl Iterator<Item = impl Deref<Target = &'a str>>,
) -> Result<HashMap<String, String>> {
get_package_versions_from_file(package_names, Path::new(DPKG_STATUS_FILE))
}
fn get_package_versions_from_file<'a>(
package_names: impl Iterator<Item = impl Deref<Target = &'a str>>,
status_file: &Path,
) -> Result<HashMap<String, String>> {
let mut versions = HashMap::new();
let target_packages: HashSet<String> = package_names
.map(|name| (*name).to_string())
.collect();
if target_packages.is_empty() {
return Ok(versions);
}
let file = File::open(status_file)?;
let reader = BufReader::new(file);
let mut current_package = String::new();
let mut current_status = String::new();
let mut current_version = String::new();
let mut in_package = false;
for line in reader.lines() {
let line = line?;
if line.is_empty() {
if in_package && current_status.contains("install ok installed") && !current_version.is_empty() {
versions.insert(current_package.clone(), current_version.clone());
}
current_package.clear();
current_status.clear();
current_version.clear();
in_package = false;
continue;
}
if let Some(package) = line.strip_prefix("Package: ") {
current_package = package.to_string();
in_package = target_packages.contains(¤t_package);
} else if in_package {
if let Some(version) = line.strip_prefix("Version: ") {
current_version = version.to_string();
} else if let Some(status) = line.strip_prefix("Status: ") {
current_status = status.to_string();
}
}
}
if in_package && current_status.contains("install ok installed") && !current_version.is_empty() {
versions.insert(current_package, current_version);
}
Ok(versions)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_dpkg_info() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let info_dir = temp_dir.path().join("info");
fs::create_dir(&info_dir).unwrap();
fs::write(
info_dir.join("test-package.list"),
"/usr/bin/test-binary\n/usr/lib/libtest.so.1\n/lib/x86_64-linux-gnu/libtest.so.1\n",
)
.unwrap();
fs::write(
info_dir.join("another-package:amd64.list"),
"/usr/bin/another\n/usr/share/doc/another-package\n",
)
.unwrap();
fs::write(
info_dir.join("libc6:amd64.list"),
"/lib/x86_64-linux-gnu/libc.so.6\n/usr/lib/x86_64-linux-gnu/libc.so.6\n",
)
.unwrap();
fs::write(info_dir.join("test.txt"), "should be ignored").unwrap();
temp_dir
}
fn setup_test_dpkg_status() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let status_content = r#"Package: libc6
Status: install ok installed
Version: 2.35-0ubuntu3.1
Architecture: amd64
Package: libssl3
Status: install ok installed
Version: 3.0.2-0ubuntu1.6
Architecture: amd64
Package: test-package
Status: install ok installed
Version: 1.2.3-4ubuntu5
Architecture: amd64
Package: not-installed
Status: deinstall ok config-files
Version: 0.0.1
Architecture: amd64
Package: another-package
Status: install ok installed
Version: 2.0.0
Architecture: amd64
"#;
fs::write(temp_dir.path().join("status"), status_content).unwrap();
temp_dir
}
#[test]
fn test_find_packages_for_paths_basic() {
let temp_dir = setup_test_dpkg_info();
let info_dir = temp_dir.path().join("info");
let paths = vec!["/usr/bin/test-binary", "/lib/x86_64-linux-gnu/libc.so.6"];
let result = find_packages_for_paths_in_dir(paths.iter(), &info_dir).unwrap();
assert_eq!(result.get("/usr/bin/test-binary").unwrap(), "test-package");
assert_eq!(result.get("/lib/x86_64-linux-gnu/libc.so.6").unwrap(), "libc6");
}
#[test]
fn test_find_packages_handles_usr_merge() {
let temp_dir = setup_test_dpkg_info();
let info_dir = temp_dir.path().join("info");
let paths = vec!["/lib/x86_64-linux-gnu/libtest.so.1"];
let result = find_packages_for_paths_in_dir(paths.iter(), &info_dir).unwrap();
assert_eq!(result.get("/lib/x86_64-linux-gnu/libtest.so.1").unwrap(), "test-package");
assert_eq!(result.get("lib/x86_64-linux-gnu/libtest.so.1"), None);
}
#[test]
fn test_find_packages_strips_architecture() {
let temp_dir = setup_test_dpkg_info();
let info_dir = temp_dir.path().join("info");
let paths = vec!["/usr/bin/another"];
let result = find_packages_for_paths_in_dir(paths.iter(), &info_dir).unwrap();
assert_eq!(result.get("/usr/bin/another").unwrap(), "another-package");
}
#[test]
fn test_find_packages_empty_paths() {
let temp_dir = setup_test_dpkg_info();
let info_dir = temp_dir.path().join("info");
let paths: Vec<&str> = vec![];
let result = find_packages_for_paths_in_dir(paths.iter(), &info_dir).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_find_packages_nonexistent_paths() {
let temp_dir = setup_test_dpkg_info();
let info_dir = temp_dir.path().join("info");
let paths = vec!["/nonexistent/path", "/another/missing/file"];
let result = find_packages_for_paths_in_dir(paths.iter(), &info_dir).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_get_package_versions_basic() {
let temp_dir = setup_test_dpkg_status();
let status_file = temp_dir.path().join("status");
let packages = vec!["libc6", "libssl3", "test-package"];
let result = get_package_versions_from_file(packages.iter(), &status_file).unwrap();
assert_eq!(result.get("libc6").unwrap(), "2.35-0ubuntu3.1");
assert_eq!(result.get("libssl3").unwrap(), "3.0.2-0ubuntu1.6");
assert_eq!(result.get("test-package").unwrap(), "1.2.3-4ubuntu5");
}
#[test]
fn test_get_package_versions_not_installed() {
let temp_dir = setup_test_dpkg_status();
let status_file = temp_dir.path().join("status");
let packages = vec!["not-installed"];
let result = get_package_versions_from_file(packages.iter(), &status_file).unwrap();
assert!(!result.contains_key("not-installed"));
}
#[test]
fn test_get_package_versions_mixed() {
let temp_dir = setup_test_dpkg_status();
let status_file = temp_dir.path().join("status");
let packages = vec!["libc6", "not-installed", "nonexistent", "another-package"];
let result = get_package_versions_from_file(packages.iter(), &status_file).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result.get("libc6").unwrap(), "2.35-0ubuntu3.1");
assert_eq!(result.get("another-package").unwrap(), "2.0.0");
assert!(!result.contains_key("not-installed"));
assert!(!result.contains_key("nonexistent"));
}
#[test]
fn test_get_package_versions_empty() {
let temp_dir = setup_test_dpkg_status();
let status_file = temp_dir.path().join("status");
let packages: Vec<&str> = vec![];
let result = get_package_versions_from_file(packages.iter(), &status_file).unwrap();
assert!(result.is_empty());
}
}