ld-so-cache 0.1.0

A parser for glibc ld.so.cache files
Documentation
use clap::{Arg, Command};
use ld_so_cache::parsers::parse_ld_cache;
use std::fs;
use std::path::Path;

fn main() {
    let matches = Command::new("ld-cache-parser")
        .version("0.1.0")
        .author("Generated CLI for ld.so.cache parsing")
        .about("Parses ld.so.cache files and displays library information")
        .arg(
            Arg::new("file")
                .short('f')
                .long("file")
                .value_name("FILE")
                .help("Path to ld.so.cache file")
                .default_value("/etc/ld.so.cache")
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(clap::ArgAction::SetTrue)
                .help("Show detailed information including hardware capabilities")
        )
        .arg(
            Arg::new("format")
                .short('F')
                .long("format")
                .value_name("FORMAT")
                .help("Output format")
                .value_parser(["human", "json", "csv"])
                .default_value("human")
        )
        .arg(
            Arg::new("filter")
                .long("filter")
                .value_name("PATTERN")
                .help("Filter libraries by name pattern")
        )
        .arg(
            Arg::new("stats")
                .long("stats")
                .action(clap::ArgAction::SetTrue)
                .help("Show cache statistics only")
        )
        .get_matches();

    let file_path = matches.get_one::<String>("file").unwrap();
    let verbose = matches.get_flag("verbose");
    let format = matches.get_one::<String>("format").unwrap();
    let filter_pattern = matches.get_one::<String>("filter");
    let show_stats = matches.get_flag("stats");

    if let Err(e) = run_parser(file_path, verbose, format, filter_pattern, show_stats) {
        eprintln!("Error: {e}");
        std::process::exit(1);
    }
}

fn run_parser(
    file_path: &str,
    verbose: bool,
    format: &str,
    filter_pattern: Option<&String>,
    show_stats: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    if !Path::new(file_path).exists() {
        return Err(format!("File not found: {file_path}").into());
    }

    let data = fs::read(file_path)?;
    let cache = parse_ld_cache(&data)?;

    if show_stats {
        print_cache_stats(&cache);
        return Ok(());
    }

    let entries = cache.get_entries()?;
    
    let filtered_entries: Vec<_> = if let Some(pattern) = filter_pattern {
        entries
            .into_iter()
            .filter(|entry| entry.library_name.contains(pattern))
            .collect()
    } else {
        entries
    };

    match format {
        "json" => print_json(&filtered_entries, verbose)?,
        "csv" => print_csv(&filtered_entries, verbose),
        _ => print_human(&filtered_entries, verbose),
    }

    Ok(())
}

fn print_cache_stats(cache: &ld_so_cache::LdCache) {
    println!("=== ld.so.cache Statistics ===");
    
    if let Some(old_cache) = &cache.old_format {
        println!("Old format present: {} entries", old_cache.nlibs);
    } else {
        println!("Old format: Not present");
    }
    
    if let Some(new_cache) = &cache.new_format {
        println!("New format present: {} entries", new_cache.nlibs);
        println!("String table size: {} bytes", new_cache.len_strings);
        println!("Endianness flag: {}", new_cache.flags);
        
        if new_cache.extension_offset > 0 {
            println!("Extensions: Present at offset {}", new_cache.extension_offset);
            if let Some(ext) = &new_cache.extensions {
                println!("Extension sections: {}", ext.count);
            }
        } else {
            println!("Extensions: Not present");
        }
    } else {
        println!("New format: Not present");
    }
    
    println!("String table size: {} bytes", cache.string_table.len());
    
    if let Ok(entries) = cache.get_entries() {
        println!("Total library entries: {}", entries.len());
        
        let with_hwcap = entries.iter().filter(|e| e.hwcap.is_some()).count();
        println!("Entries with hardware capabilities: {with_hwcap}");
        
        let elf_entries = entries.iter().filter(|e| e.flags & 1 != 0).count();
        println!("ELF library entries: {elf_entries}");
    }
}

fn print_human(entries: &[ld_so_cache::CacheEntry], verbose: bool) {
    println!("=== Library Cache Entries ({}) ===", entries.len());
    
    for (i, entry) in entries.iter().enumerate() {
        println!("{:4}: {} -> {}", i + 1, entry.library_name, entry.library_path);
        
        if verbose {
            println!("      Flags: 0x{:x}", entry.flags);
            if entry.flags & 1 != 0 {
                print!("      Type: ELF");
            } else {
                print!("      Type: Unknown");
            }
            
            if let Some(hwcap) = entry.hwcap {
                println!(" | HWCap: 0x{hwcap:016x}");
                if hwcap & (1u64 << 62) != 0 {
                    println!("            Extension flag set");
                }
                let isa_level = (hwcap >> 52) & 0x3ff;
                if isa_level > 0 {
                    println!("            ISA Level: {isa_level}");
                }
            } else {
                println!();
            }
        }
    }
}

fn print_csv(entries: &[ld_so_cache::CacheEntry], verbose: bool) {
    if verbose {
        println!("library_name,library_path,flags,hwcap");
        for entry in entries {
            println!(
                "{},{},0x{:x},{}",
                entry.library_name,
                entry.library_path,
                entry.flags,
                entry.hwcap.map_or_else(String::new, |h| format!("0x{h:016x}"))
            );
        }
    } else {
        println!("library_name,library_path");
        for entry in entries {
            println!("{},{}", entry.library_name, entry.library_path);
        }
    }
}

fn print_json(entries: &[ld_so_cache::CacheEntry], verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
    if verbose {
        println!("{}", serde_json::to_string_pretty(entries)?);
    } else {
        let simple_entries: Vec<_> = entries.iter()
            .map(|entry| SimpleEntry {
                library_name: &entry.library_name,
                library_path: &entry.library_path,
            })
            .collect();
        println!("{}", serde_json::to_string_pretty(&simple_entries)?);
    }
    
    Ok(())
}

#[derive(serde::Serialize)]
struct SimpleEntry<'a> {
    library_name: &'a str,
    library_path: &'a str,
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::NamedTempFile;
    use std::io::Write;

    #[test]
    fn test_run_parser_with_nonexistent_file() {
        let result = run_parser("/nonexistent/file", false, "human", None, false);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("File not found"));
    }

    #[test]
    fn test_run_parser_with_valid_cache() {
        let mut data = Vec::new();
        data.extend_from_slice(b"ld.so-1.7.0");
        data.extend_from_slice(&1u32.to_le_bytes());
        data.extend_from_slice(&1i32.to_le_bytes());
        data.extend_from_slice(&0u32.to_le_bytes());
        data.extend_from_slice(&10u32.to_le_bytes());
        data.extend_from_slice(b"libc.so.6\0/lib/libc.so.6\0");

        let mut temp_file = NamedTempFile::new().unwrap();
        temp_file.write_all(&data).unwrap();
        let temp_path = temp_file.path().to_str().unwrap();

        let result = run_parser(temp_path, false, "human", None, false);
        assert!(result.is_ok());
    }
}