fast-shlibdeps 0.2.2

A fast ELF shared library dependency analyzer for Debian-based systems
use anyhow::Result;
use elf::{ElfBytes, endian::AnyEndian};
use std::fs;
use std::path::Path;

/// Get the shared libraries that the file at path depends on. If path isn't a valid ELF file, returns an empty vec.
pub(crate) fn get_file_dependencies(path: &Path) -> Result<Vec<String>> {
    let file_data = match fs::read(path) {
        Ok(data) => data,
        Err(_) => return Ok(Vec::new()),
    };

    let file = match ElfBytes::<AnyEndian>::minimal_parse(&file_data) {
        Ok(elf) => elf,
        Err(_) => return Ok(Vec::new()),
    };

    let mut dependencies = Vec::new();

    if let Ok(Some(dynamic)) = file.dynamic() {
        if let Ok(Some(strtab)) = file.section_header_by_name(".dynstr") {
            if let Ok(string_table) = file.section_data(&strtab) {
                for entry in dynamic.iter() {
                    if entry.d_tag == elf::abi::DT_NEEDED {
                        let offset = entry.d_val() as usize;
                        if let Some(null_pos) =
                            string_table.0[offset..].iter().position(|&b| b == 0)
                        {
                            if let Ok(lib_name) =
                                std::str::from_utf8(&string_table.0[offset..offset + null_pos])
                            {
                                dependencies.push(lib_name.to_string());
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(dependencies)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    use std::fs;

    #[test]
    fn test_get_file_dependencies_nonexistent_file() {
        let result = get_file_dependencies(Path::new("/nonexistent/file/that/should/not/exist")).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_non_elf_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        fs::write(&file_path, "This is not an ELF file").unwrap();
        
        let result = get_file_dependencies(&file_path).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_empty_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("empty");
        fs::write(&file_path, "").unwrap();
        
        let result = get_file_dependencies(&file_path).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_binary_garbage() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("garbage");
        fs::write(&file_path, vec![0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xFF]).unwrap();
        
        let result = get_file_dependencies(&file_path).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_invalid_elf_magic() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("fake_elf");
        // Write something that starts like ELF but isn't valid
        let mut fake_elf = vec![0x7f, 0x45, 0x4c, 0x46]; // ELF magic
        fake_elf.extend_from_slice(&[0x01, 0x01, 0x01, 0x00]); // Some header bytes
        fake_elf.extend_from_slice(&vec![0x00; 100]); // Padding with zeros
        fs::write(&file_path, fake_elf).unwrap();
        
        let result = get_file_dependencies(&file_path).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_permission_denied() {
        let temp_dir = TempDir::new().unwrap();
        let dir_path = temp_dir.path().join("directory");
        fs::create_dir(&dir_path).unwrap();
        
        // Try to read a directory as if it were a file
        let result = get_file_dependencies(&dir_path).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_file_dependencies_real_elf_binary() {
        // Use our compiled hello world binary as a test fixture
        let hello_binary = Path::new("tests/fixtures/hello");
        
        let result = get_file_dependencies(hello_binary).unwrap();
        
        // Snapshot test: expect exactly the dependencies we know this binary has
        // This hello world C program compiled with gcc should only depend on libc
        let expected = vec!["libc.so.6"];
        assert_eq!(result, expected, 
                   "Expected exact dependency match for hello binary. Got: {:?}, Expected: {:?}", 
                   result, expected);
    }
}