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());
}
}