#[derive(Debug, Clone, PartialEq)]
pub struct LdCache {
pub old_format: Option<OldCache>,
pub new_format: Option<NewCache>,
pub string_table: Vec<u8>,
pub string_table_offset: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OldCache {
pub nlibs: u32,
pub entries: Vec<OldFileEntry>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OldFileEntry {
pub flags: i32,
pub key: u32,
pub value: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NewCache {
pub nlibs: u32,
pub len_strings: u32,
pub flags: u8,
pub extension_offset: u32,
pub entries: Vec<NewFileEntry>,
pub extensions: Option<ExtensionDirectory>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NewFileEntry {
pub flags: i32,
pub key: u32,
pub value: u32,
pub osversion_unused: u32,
pub hwcap: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtensionDirectory {
pub count: u32,
pub sections: Vec<ExtensionSection>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtensionSection {
pub tag: u32,
pub flags: u32,
pub offset: u32,
pub size: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CacheFormat {
OldOnly(OldCache),
NewOnly(NewCache),
Both { old: OldCache, new: NewCache },
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct CacheEntry {
pub library_name: String,
pub library_path: String,
pub flags: i32,
#[serde(with = "hwcap_format", skip_serializing_if = "Option::is_none")]
pub hwcap: Option<u64>,
}
mod hwcap_format {
use serde::{Serializer, Serialize};
pub fn serialize<S>(hwcap: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match hwcap {
Some(value) => format!("0x{value:016x}").serialize(serializer),
None => serializer.serialize_none(),
}
}
}
impl LdCache {
pub fn get_entries(&self) -> Result<Vec<CacheEntry>, CacheError> {
let string_table = &self.string_table;
let mut entries = Vec::new();
if let Some(new_cache) = &self.new_format {
for entry in &new_cache.entries {
let key_offset = if self.string_table_offset > 0 && entry.key as usize >= self.string_table_offset {
entry.key as usize - self.string_table_offset
} else {
entry.key as usize
};
let value_offset = if self.string_table_offset > 0 && entry.value as usize >= self.string_table_offset {
entry.value as usize - self.string_table_offset
} else {
entry.value as usize
};
if key_offset >= string_table.len() || value_offset >= string_table.len() {
continue; }
let name = extract_string(string_table, key_offset)?;
let path = extract_string(string_table, value_offset)?;
entries.push(CacheEntry {
library_name: name,
library_path: path,
flags: entry.flags,
hwcap: Some(entry.hwcap),
});
}
} else if let Some(old_cache) = &self.old_format {
for entry in &old_cache.entries {
if entry.key as usize >= string_table.len() || entry.value as usize >= string_table.len() {
continue; }
let name = extract_string(string_table, entry.key as usize)?;
let path = extract_string(string_table, entry.value as usize)?;
entries.push(CacheEntry {
library_name: name,
library_path: path,
flags: entry.flags,
hwcap: None,
});
}
}
Ok(entries)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CacheError {
ParseError(String),
InvalidStringOffset(usize),
InvalidMagic,
TruncatedFile,
InvalidEndianness,
}
impl std::fmt::Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CacheError::ParseError(msg) => write!(f, "Parse error: {msg}"),
CacheError::InvalidStringOffset(offset) => write!(f, "Invalid string offset: {offset}"),
CacheError::InvalidMagic => write!(f, "Invalid magic number"),
CacheError::TruncatedFile => write!(f, "Truncated file"),
CacheError::InvalidEndianness => write!(f, "Invalid endianness"),
}
}
}
impl std::error::Error for CacheError {}
fn extract_string(string_table: &[u8], offset: usize) -> Result<String, CacheError> {
if offset >= string_table.len() {
return Err(CacheError::InvalidStringOffset(offset));
}
let slice = &string_table[offset..];
let null_pos = slice
.iter()
.position(|&b| b == 0)
.ok_or(CacheError::InvalidStringOffset(offset))?;
String::from_utf8(slice[..null_pos].to_vec())
.map_err(|_| CacheError::ParseError("Invalid UTF-8 in string".to_string()))
}
pub mod parsers;
#[cfg(test)]
mod tests {
use super::*;
use crate::parsers::parse_ld_cache;
#[test]
fn test_extract_string() {
let string_table = b"hello\0world\0test\0";
assert_eq!(extract_string(string_table, 0).unwrap(), "hello");
assert_eq!(extract_string(string_table, 6).unwrap(), "world");
assert_eq!(extract_string(string_table, 12).unwrap(), "test");
assert!(extract_string(string_table, 100).is_err());
}
#[test]
fn test_extract_string_edge_cases() {
let string_table = b"a\0";
assert_eq!(extract_string(string_table, 0).unwrap(), "a");
let empty_string_table = b"\0";
assert_eq!(extract_string(empty_string_table, 0).unwrap(), "");
let no_null_terminator = b"hello";
assert!(extract_string(no_null_terminator, 0).is_err());
let string_table = b"hello\0";
assert!(extract_string(string_table, 10).is_err());
}
#[test]
fn test_cache_entry_creation() {
let entry = CacheEntry {
library_name: "libc.so.6".to_string(),
library_path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
flags: 1,
hwcap: Some(0x1234_5678),
};
assert_eq!(entry.library_name, "libc.so.6");
assert_eq!(entry.flags, 1);
assert_eq!(entry.hwcap, Some(0x1234_5678));
}
#[test]
fn test_parse_old_format_cache() {
let mut data = Vec::new();
data.extend_from_slice(b"ld.so-1.7.0");
data.extend_from_slice(&2u32.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(&1i32.to_le_bytes());
data.extend_from_slice(&20u32.to_le_bytes());
data.extend_from_slice(&40u32.to_le_bytes());
data.extend_from_slice(b"libc.so.6\0/lib/libc.so.6\0libm.so.6\0/lib/libm.so.6\0");
let cache = parse_ld_cache(&data).unwrap();
assert!(cache.old_format.is_some());
assert!(cache.new_format.is_none());
let old_cache = cache.old_format.as_ref().unwrap();
assert_eq!(old_cache.nlibs, 2);
assert_eq!(old_cache.entries.len(), 2);
assert_eq!(old_cache.entries[0].flags, 1);
assert_eq!(old_cache.entries[0].key, 0);
assert_eq!(old_cache.entries[0].value, 10);
let entries = cache.get_entries().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].library_name, "libc.so.6");
assert_eq!(entries[0].library_path, "/lib/libc.so.6");
assert!(entries[0].hwcap.is_none());
}
#[test]
fn test_parse_new_format_cache() {
let mut data = Vec::new();
data.extend_from_slice(b"glibc-ld.so.cache");
data.extend_from_slice(b"1.1");
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&30u32.to_le_bytes());
data.push(2);
data.extend_from_slice(&[0, 0, 0]);
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&[0; 12]);
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(&0u32.to_le_bytes());
data.extend_from_slice(&0x1234_5678_u64.to_le_bytes());
data.extend_from_slice(b"libc.so.6\0/lib/libc.so.6\0");
let cache = parse_ld_cache(&data).unwrap();
assert!(cache.new_format.is_some());
assert!(cache.old_format.is_none());
let new_cache = cache.new_format.as_ref().unwrap();
assert_eq!(new_cache.nlibs, 1);
assert_eq!(new_cache.len_strings, 30);
assert_eq!(new_cache.flags, 2);
assert_eq!(new_cache.entries.len(), 1);
assert_eq!(new_cache.entries[0].hwcap, 0x1234_5678);
let entries = cache.get_entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].library_name, "libc.so.6");
assert_eq!(entries[0].library_path, "/lib/libc.so.6");
assert_eq!(entries[0].hwcap, Some(0x1234_5678));
}
#[test]
fn test_parse_invalid_magic() {
let invalid_data = b"invalid-magic-that-is-long-enough";
let result = parse_ld_cache(invalid_data);
assert!(matches!(result, Err(CacheError::InvalidMagic)));
}
#[test]
fn test_parse_truncated_file() {
let truncated_data = b"ld.so-1.7.0";
let result = parse_ld_cache(truncated_data);
assert!(matches!(result, Err(CacheError::TruncatedFile)));
}
#[test]
fn test_cache_error_display() {
let error = CacheError::ParseError("test error".to_string());
assert_eq!(format!("{error}"), "Parse error: test error");
let error = CacheError::InvalidStringOffset(42);
assert_eq!(format!("{error}"), "Invalid string offset: 42");
let error = CacheError::InvalidMagic;
assert_eq!(format!("{error}"), "Invalid magic number");
let error = CacheError::TruncatedFile;
assert_eq!(format!("{error}"), "Truncated file");
let error = CacheError::InvalidEndianness;
assert_eq!(format!("{error}"), "Invalid endianness");
}
#[test]
fn test_empty_cache() {
let mut data = Vec::new();
data.extend_from_slice(b"ld.so-1.7.0");
data.extend_from_slice(&0u32.to_le_bytes());
let cache = parse_ld_cache(&data).unwrap();
let entries = cache.get_entries().unwrap();
assert_eq!(entries.len(), 0);
}
#[test]
fn test_ld_cache_get_entries_preference() {
let old_cache = OldCache {
nlibs: 1,
entries: vec![OldFileEntry {
flags: 1,
key: 0,
value: 10,
}],
};
let new_cache = NewCache {
nlibs: 1,
len_strings: 30,
flags: 2,
extension_offset: 0,
entries: vec![NewFileEntry {
flags: 1,
key: 0,
value: 10,
osversion_unused: 0,
hwcap: 0x1234_5678,
}],
extensions: None,
};
let string_table = b"libc.so.6\0/lib/libc.so.6\0".to_vec();
let cache_with_both = LdCache {
old_format: Some(old_cache),
new_format: Some(new_cache),
string_table: string_table.clone(),
string_table_offset: 0,
};
let entries = cache_with_both.get_entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].hwcap, Some(0x1234_5678));
}
#[test]
fn test_hwcap_bit_decoding() {
let hwcap_with_isa_v2: u64 = 2u64 << 52;
let isa_level = (hwcap_with_isa_v2 >> 52) & 0x3ff;
assert_eq!(isa_level, 2);
let hwcap_with_isa_v3: u64 = 3u64 << 52;
let isa_level = (hwcap_with_isa_v3 >> 52) & 0x3ff;
assert_eq!(isa_level, 3);
let hwcap_with_extension: u64 = 1u64 << 62;
assert!(hwcap_with_extension & (1u64 << 62) != 0);
let hwcap_without_extension: u64 = 0x0000_1234_5678_9ABC;
assert!(hwcap_without_extension & (1u64 << 62) == 0);
let hwcap_with_features: u64 = 0x000F_FFFF_FFFF_FFFF; let cpu_features = hwcap_with_features & ((1u64 << 52) - 1);
assert_eq!(cpu_features, 0x000F_FFFF_FFFF_FFFF);
let entry = NewFileEntry {
flags: 1,
key: 0,
value: 0,
osversion_unused: 0,
hwcap: 0x0020_0000_0000_1234, };
let isa_level = (entry.hwcap >> 52) & 0x3ff;
let features = entry.hwcap & ((1u64 << 52) - 1);
assert_eq!(isa_level, 2);
assert_eq!(features, 0x1234);
let entry_with_ext = NewFileEntry {
flags: 1,
key: 0,
value: 0,
osversion_unused: 0,
hwcap: 0x4000_0000_0000_0000, };
let has_extension = entry_with_ext.hwcap & (1u64 << 62) != 0;
assert!(has_extension);
}
#[test]
fn test_graceful_degradation_with_invalid_entries() {
let new_cache = NewCache {
nlibs: 4,
len_strings: 50,
flags: 2,
extension_offset: 0,
entries: vec![
NewFileEntry {
flags: 1,
key: 0,
value: 10,
osversion_unused: 0,
hwcap: 0x1234,
},
NewFileEntry {
flags: 1,
key: 1000, value: 2000, osversion_unused: 0,
hwcap: 0x5678,
},
NewFileEntry {
flags: 1,
key: 26,
value: 36,
osversion_unused: 0,
hwcap: 0x9ABC,
},
NewFileEntry {
flags: 1,
key: 500, value: 10,
osversion_unused: 0,
hwcap: 0xDEF0,
},
],
extensions: None,
};
let string_table = b"libc.so.6\0/lib/libc.so.6\0\0libm.so.6\0/lib/libm.so.6\0".to_vec();
let cache = LdCache {
old_format: None,
new_format: Some(new_cache),
string_table,
string_table_offset: 0,
};
let entries = cache.get_entries().unwrap();
assert_eq!(entries.len(), 2); assert_eq!(entries[0].library_name, "libc.so.6");
assert_eq!(entries[1].library_name, "libm.so.6");
}
#[test]
fn test_string_table_offset_adjustment() {
let new_cache = NewCache {
nlibs: 1,
len_strings: 30,
flags: 2,
extension_offset: 0,
entries: vec![NewFileEntry {
flags: 1,
key: 1000, value: 1010, osversion_unused: 0,
hwcap: 0x1234,
}],
extensions: None,
};
let string_table = b"libc.so.6\0/lib/libc.so.6\0".to_vec();
let cache = LdCache {
old_format: None,
new_format: Some(new_cache),
string_table,
string_table_offset: 1000, };
let entries = cache.get_entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].library_name, "libc.so.6");
assert_eq!(entries[0].library_path, "/lib/libc.so.6");
}
#[test]
fn test_flags_architecture_decoding() {
let i386_flags = 0x0001; let arch_bits = (i386_flags >> 8) & 0xf;
assert_eq!(arch_bits, 0);
let x86_64_flags = 0x0301; let arch_bits = (x86_64_flags >> 8) & 0xf;
assert_eq!(arch_bits, 3);
let libx32_flags = 0x0801; let arch_bits = (libx32_flags >> 8) & 0xf;
assert_eq!(arch_bits, 8);
}
#[test]
fn test_endianness_flag_values() {
let test_cases = vec![
(0u8, "Endianness unset (legacy)"),
(1u8, "Invalid cache"),
(2u8, "Little endian"),
(3u8, "Big endian"),
];
for (flag_value, expected_meaning) in test_cases {
let new_cache = NewCache {
nlibs: 0,
len_strings: 0,
flags: flag_value,
extension_offset: 0,
entries: vec![],
extensions: None,
};
assert_eq!(new_cache.flags, flag_value);
let meaning = match flag_value {
0 => "Endianness unset (legacy)",
1 => "Invalid cache",
2 => "Little endian",
3 => "Big endian",
_ => "Unknown",
};
assert_eq!(meaning, expected_meaning);
}
}
#[test]
fn test_extension_tag_meanings() {
let ext_dir = ExtensionDirectory {
count: 2,
sections: vec![
ExtensionSection {
tag: 1,
flags: 0,
offset: 100,
size: 50,
},
ExtensionSection {
tag: 2,
flags: 0,
offset: 150,
size: 30,
},
],
};
assert_eq!(ext_dir.sections[0].tag, 1); assert_eq!(ext_dir.sections[1].tag, 2); }
#[test]
fn test_cache_entry_serialization_with_hwcap() {
let entry = CacheEntry {
library_name: "libc.so.6".to_string(),
library_path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
flags: 0x0301, hwcap: Some(0x0000_0000_0000_1000),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"hwcap\":\"0x0000000000001000\""));
let entry_no_hwcap = CacheEntry {
library_name: "libm.so.6".to_string(),
library_path: "/lib/libm.so.6".to_string(),
flags: 1,
hwcap: None,
};
let json_no_hwcap = serde_json::to_string(&entry_no_hwcap).unwrap();
assert!(!json_no_hwcap.contains("hwcap"));
}
}