fast-shlibdeps 0.2.1

A fast ELF shared library dependency analyzer for Debian-based systems
use anyhow::Result;
use std::collections::HashMap;
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";

/// Find which packages own files by reading dpkg database directly.
/// Handles usr-merge by querying both with and without /usr prefix.
/// Returns a HashMap of path to package name.
pub(crate) fn find_packages_for_paths<'a>(
    paths: impl IntoIterator<Item = impl Deref<Target = &'a str>>,
) -> Result<HashMap<String, String>> {
    let mut path_to_package = HashMap::new();
    let mut search_paths = Vec::new();

    // Collect all paths we need to search for
    for path in paths {
        let path_str = *path;
        search_paths.push(path_str.to_string());
        // Also add /usr-prefixed version
        search_paths.push(format!("/usr{}", path_str));
    }

    // Read all .list files in /var/lib/dpkg/info/
    let info_dir = Path::new(DPKG_INFO_DIR);
    if !info_dir.exists() {
        anyhow::bail!("dpkg info directory not found: {}", DPKG_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();

        // Only process .list files
        if !file_name_str.ends_with(".list") {
            continue;
        }

        // Extract package name from filename (remove .list extension)
        let package_full = &file_name_str[..file_name_str.len() - 5];
        
        // Remove architecture suffix (e.g., :amd64, :i386)
        let package_name = if let Some(arch_pos) = package_full.rfind(':') {
            &package_full[..arch_pos]
        } else {
            package_full
        };

        // Read the list file and check if it contains any of our paths
        let file = File::open(entry.path())?;
        let reader = BufReader::new(file);

        for line in reader.lines() {
            let line = line?;
            let trimmed = line.trim();
            
            // Check if this line matches any of our search paths
            for search_path in &search_paths {
                if trimmed == search_path {
                    // Add the path as-is
                    path_to_package.insert(trimmed.to_string(), package_name.to_string());
                    
                    // If it starts with /usr, also add the version without /usr
                    if let Some(stripped) = trimmed.strip_prefix("/usr") {
                        path_to_package.insert(stripped.to_string(), package_name.to_string());
                    }
                    
                    // If it doesn't start with /usr, also check if we found the /usr version
                    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)
}

/// Get the currently installed versions of packages by reading dpkg status file.
/// Returns a HashMap of package name to version for successfully queried packages.
pub(crate) fn get_package_versions<'a>(
    package_names: impl Iterator<Item = impl Deref<Target = &'a str>>,
) -> Result<HashMap<String, String>> {
    let mut versions = HashMap::new();
    let mut target_packages: HashMap<String, bool> = package_names
        .map(|name| ((*name).to_string(), false))
        .collect();

    if target_packages.is_empty() {
        return Ok(versions);
    }

    // Read the dpkg status file
    let file = File::open(DPKG_STATUS_FILE)?;
    let reader = BufReader::new(file);

    let mut current_package = String::new();
    let mut current_status = String::new();
    let mut in_package = false;

    for line in reader.lines() {
        let line = line?;

        if line.is_empty() {
            // End of package entry
            if in_package && current_status.contains("install ok installed") {
                if let Some(found) = target_packages.get_mut(&current_package) {
                    *found = true;
                }
            }
            current_package.clear();
            current_status.clear();
            in_package = false;
            continue;
        }

        if let Some(package) = line.strip_prefix("Package: ") {
            current_package = package.to_string();
            in_package = target_packages.contains_key(&current_package);
        } else if in_package {
            if let Some(version) = line.strip_prefix("Version: ") {
                versions.insert(current_package.clone(), version.to_string());
            } else if let Some(status) = line.strip_prefix("Status: ") {
                current_status = status.to_string();
            }
        }
    }

    // Final package check (in case file doesn't end with empty line)
    if in_package && current_status.contains("install ok installed") {
        if target_packages.contains_key(&current_package) {
            // Version should already be inserted
        }
    }

    Ok(versions)
}