fast-shlibdeps 0.2.2

A fast ELF shared library dependency analyzer for Debian-based systems
use anyhow::Result;
use std::collections::HashMap;
use std::process::Command;

/// Get the contents of the ld cache by running `/sbin/ldconfig -p`. Returns a mapping of library names to their paths.
pub(crate) fn get_ld_cache() -> Result<HashMap<String, String>> {
    let output = Command::new("/sbin/ldconfig").arg("-p").output()?;

    if !output.status.success() {
        anyhow::bail!("ldconfig -p failed with status: {}", output.status);
    }

    let stdout = String::from_utf8(output.stdout)?;
    parse_ldconfig_output(&stdout)
}

/// Parse ldconfig output into a HashMap
fn parse_ldconfig_output(output: &str) -> Result<HashMap<String, String>> {
    let mut cache = HashMap::new();

    for line in output.lines().skip(1) {
        // Skip the header line
        let line = line.trim();
        if let Some(arrow_pos) = line.find(" => ") {
            let lib_name = line[..arrow_pos].trim();
            let lib_path = line[arrow_pos + 4..].trim();

            // Extract just the library name (e.g., "libssl.so.3" from "libssl.so.3 (libc6,x86-64)")
            if let Some(paren_pos) = lib_name.find(" (") {
                let clean_name = &lib_name[..paren_pos];
                cache.insert(clean_name.to_string(), lib_path.to_string());
            } else {
                cache.insert(lib_name.to_string(), lib_path.to_string());
            }
        }
    }

    Ok(cache)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_ldconfig_output() {
        let sample_output = r#"42 libs found in cache `/etc/ld.so.cache'
	libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
	libz.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so
	libyaml-0.so.2 (libc6,x86-64) => /lib/x86_64-linux-gnu/libyaml-0.so.2
	libxtables.so.12 (libc6,x86-64) => /lib/x86_64-linux-gnu/libxtables.so.12
	libxslt.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libxslt.so.1
	libxml2.so.2 (libc6,x86-64, OS ABI: Linux 3.2.0) => /lib/x86_64-linux-gnu/libxml2.so.2
	libwrap.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libwrap.so.0
	libuuid.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libuuid.so.1
	libc.so.6 (libc6,x86-64, OS ABI: Linux 3.2.0) => /lib/x86_64-linux-gnu/libc.so.6
	libselinux.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libselinux.so.1
"#;

        let cache = parse_ldconfig_output(sample_output).unwrap();
        
        assert_eq!(cache.get("libz.so.1").unwrap(), "/lib/x86_64-linux-gnu/libz.so.1");
        assert_eq!(cache.get("libz.so").unwrap(), "/lib/x86_64-linux-gnu/libz.so");
        assert_eq!(cache.get("libyaml-0.so.2").unwrap(), "/lib/x86_64-linux-gnu/libyaml-0.so.2");
        assert_eq!(cache.get("libxml2.so.2").unwrap(), "/lib/x86_64-linux-gnu/libxml2.so.2");
        assert_eq!(cache.get("libc.so.6").unwrap(), "/lib/x86_64-linux-gnu/libc.so.6");
        assert_eq!(cache.get("libselinux.so.1").unwrap(), "/lib/x86_64-linux-gnu/libselinux.so.1");
        assert_eq!(cache.len(), 10);
    }

    #[test]
    fn test_parse_ldconfig_output_empty() {
        let sample_output = "0 libs found in cache `/etc/ld.so.cache'\n";
        let cache = parse_ldconfig_output(sample_output).unwrap();
        assert!(cache.is_empty());
    }

    #[test]
    fn test_parse_ldconfig_output_no_parentheses() {
        let sample_output = r#"1 libs found in cache `/etc/ld.so.cache'
	libweird.so => /lib/libweird.so
"#;
        let cache = parse_ldconfig_output(sample_output).unwrap();
        assert_eq!(cache.get("libweird.so").unwrap(), "/lib/libweird.so");
    }

    #[test]
    fn test_parse_ldconfig_output_skip_invalid_lines() {
        let sample_output = r#"3 libs found in cache `/etc/ld.so.cache'
	libvalid.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libvalid.so.1
	this line has no arrow
	libvalid2.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libvalid2.so.1
	malformed => 
	 => no lib name
"#;
        let cache = parse_ldconfig_output(sample_output).unwrap();
        assert_eq!(cache.len(), 2);
        assert_eq!(cache.get("libvalid.so.1").unwrap(), "/lib/x86_64-linux-gnu/libvalid.so.1");
        assert_eq!(cache.get("libvalid2.so.1").unwrap(), "/lib/x86_64-linux-gnu/libvalid2.so.1");
    }

    #[test]
    fn test_parse_ldconfig_output_multiarch() {
        let sample_output = r#"4 libs found in cache `/etc/ld.so.cache'
	libc.so.6 (libc6,x86-64, OS ABI: Linux 3.2.0) => /lib/x86_64-linux-gnu/libc.so.6
	libc.so.6 (libc6, OS ABI: Linux 3.2.0) => /lib/i386-linux-gnu/libc.so.6
	libm.so.6 (libc6,x86-64, OS ABI: Linux 3.2.0) => /lib/x86_64-linux-gnu/libm.so.6
	libm.so.6 (libc6, OS ABI: Linux 3.2.0) => /lib/i386-linux-gnu/libm.so.6
"#;
        let cache = parse_ldconfig_output(sample_output).unwrap();
        // The function will overwrite entries with the same name, so we'll get the last one
        assert_eq!(cache.len(), 2);
        assert!(cache.contains_key("libc.so.6"));
        assert!(cache.contains_key("libm.so.6"));
    }
}