ld_so_cache/lib.rs
1
2//! # ld.so.cache Parser Library
3//!
4//! This crate provides a comprehensive parser for `ld.so.cache` files used by the Linux dynamic linker.
5//! It supports both the legacy format (ld.so-1.7.0) and the modern glibc format with hardware capabilities.
6//!
7//! ## Quick Start
8//!
9//! ```rust
10//! use ld_so_cache::parsers::parse_ld_cache;
11//! use std::fs;
12//!
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! // Parse the system's ld.so.cache file
15//! let data = fs::read("/etc/ld.so.cache")?;
16//! let cache = parse_ld_cache(&data)?;
17//!
18//! // Extract all library entries
19//! let entries = cache.get_entries()?;
20//! for entry in entries {
21//! println!("{} -> {}", entry.library_name, entry.library_path);
22//! }
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! ## Cache Formats
28//!
29//! The library supports two cache formats:
30//!
31//! - **Old Format**: Used by ld.so-1.7.0, contains basic library mappings
32//! - **New Format**: Used by glibc, includes hardware capabilities and extensions
33//!
34//! ## Hardware Capabilities
35//!
36//! The new format includes hardware capability flags that indicate processor features
37//! required by libraries. This allows the dynamic linker to select the most optimized
38//! version of a library for the current processor.
39//!
40//! ### Hardware Capability Bits
41//!
42//! The 64-bit `hwcap` field encodes various processor features:
43//!
44//! ```rust
45//! # use ld_so_cache::NewFileEntry;
46//! let entry = NewFileEntry {
47//! flags: 1,
48//! key: 0,
49//! value: 10,
50//! osversion_unused: 0,
51//! hwcap: 0x0002000000000000, // Example capability
52//! };
53//!
54//! // Extract ISA level (bits 52-61)
55//! let isa_level = (entry.hwcap >> 52) & 0x3ff;
56//! if isa_level >= 2 {
57//! println!("Requires x86-64-v2 or higher");
58//! }
59//!
60//! // Check for extension flag (bit 62)
61//! let has_extensions = entry.hwcap & (1u64 << 62) != 0;
62//! if has_extensions {
63//! println!("Library uses hardware capability extensions");
64//! }
65//!
66//! // Check for specific CPU features (bits 0-51)
67//! // Note: Exact bit meanings are architecture-specific
68//! let cpu_features = entry.hwcap & ((1u64 << 52) - 1);
69//! ```
70//!
71//! ## Error Handling
72//!
73//! All parsing operations return `Result<T, CacheError>` for robust error handling.
74//! The library is designed to be resilient and will attempt to extract as much
75//! information as possible even from partially corrupted files.
76//!
77//! ### Common Error Patterns
78//!
79//! ```rust
80//! use ld_so_cache::{parsers::parse_ld_cache, CacheError};
81//! use std::fs;
82//!
83//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
84//! // Read and parse cache file
85//! let data = fs::read("/etc/ld.so.cache")?;
86//! match parse_ld_cache(&data) {
87//! Ok(cache) => {
88//! // Successfully parsed cache
89//! match cache.get_entries() {
90//! Ok(entries) => println!("Found {} libraries", entries.len()),
91//! Err(CacheError::InvalidStringOffset(offset)) => {
92//! eprintln!("Corrupted string table at offset {}", offset);
93//! }
94//! Err(e) => eprintln!("Error extracting entries: {}", e),
95//! }
96//! }
97//! Err(CacheError::InvalidMagic) => {
98//! eprintln!("File is not a valid ld.so.cache");
99//! }
100//! Err(CacheError::TruncatedFile) => {
101//! eprintln!("Cache file appears to be truncated");
102//! }
103//! Err(e) => eprintln!("Parse error: {}", e),
104//! }
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! ### Graceful Degradation
110//!
111//! When extracting entries, the library skips invalid entries rather than failing:
112//!
113//! ```rust
114//! # use ld_so_cache::parsers::parse_ld_cache;
115//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
116//! # let data = vec![]; // Would be actual cache data
117//! let cache = parse_ld_cache(&data)?;
118//! let entries = cache.get_entries()?; // Will skip invalid entries
119//!
120//! // Even if some entries are corrupted, we get the valid ones
121//! for entry in entries {
122//! println!("{} -> {}", entry.library_name, entry.library_path);
123//! }
124//! # Ok(())
125//! # }
126//! ```
127
128/// The main cache structure representing a parsed `ld.so.cache` file.
129///
130/// This structure can contain either the old format, new format, or both.
131/// When both formats are present, the new format takes precedence for library lookups.
132///
133/// # Fields
134///
135/// * `old_format` - Legacy cache format data (ld.so-1.7.0)
136/// * `new_format` - Modern glibc cache format with hardware capabilities
137/// * `string_table` - Raw bytes containing null-terminated library names and paths
138/// * `string_table_offset` - Absolute file offset where the string table begins (for new format)
139///
140/// # Examples
141///
142/// ```rust
143/// use ld_so_cache::parsers::parse_ld_cache;
144/// # use std::fs;
145/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
146/// let data = fs::read("/etc/ld.so.cache")?;
147/// let cache = parse_ld_cache(&data)?;
148///
149/// // Check which formats are present
150/// if cache.old_format.is_some() {
151/// println!("Old format present");
152/// }
153/// if cache.new_format.is_some() {
154/// println!("New format present");
155/// }
156/// # Ok(())
157/// # }
158/// ```
159#[derive(Debug, Clone, PartialEq)]
160pub struct LdCache {
161 pub old_format: Option<OldCache>,
162 pub new_format: Option<NewCache>,
163 pub string_table: Vec<u8>,
164 pub string_table_offset: usize,
165}
166
167/// Legacy cache format used by ld.so-1.7.0.
168///
169/// This format provides basic library name to path mappings without
170/// hardware capability information.
171///
172/// # Fields
173///
174/// * `nlibs` - Number of library entries in the cache
175/// * `entries` - Vector of old format file entries
176///
177/// # Example
178///
179/// ```rust
180/// # use ld_so_cache::OldCache;
181/// let old_cache = OldCache {
182/// nlibs: 10,
183/// entries: vec![], // Would contain actual entries
184/// };
185/// assert_eq!(old_cache.nlibs, 10);
186/// ```
187#[derive(Debug, Clone, PartialEq)]
188pub struct OldCache {
189 pub nlibs: u32,
190 pub entries: Vec<OldFileEntry>,
191}
192
193/// A single library entry in the old cache format.
194///
195/// # Fields
196///
197/// * `flags` - Library type flags (bit 0: ELF library)
198/// * `key` - Offset into string table for library name
199/// * `value` - Offset into string table for library path
200///
201/// # Flag Values
202///
203/// * `flags & 1 != 0` - ELF library
204/// * `flags & 1 == 0` - Other/unknown format
205///
206/// # Example
207///
208/// ```rust
209/// # use ld_so_cache::OldFileEntry;
210/// let entry = OldFileEntry {
211/// flags: 1, // ELF library
212/// key: 0, // Library name at offset 0 in string table
213/// value: 10, // Library path at offset 10 in string table
214/// };
215/// assert!(entry.flags & 1 != 0); // Is ELF library
216/// ```
217#[derive(Debug, Clone, PartialEq)]
218pub struct OldFileEntry {
219 pub flags: i32,
220 pub key: u32,
221 pub value: u32,
222}
223
224/// Modern glibc cache format with hardware capabilities and extensions.
225///
226/// This format extends the old format with hardware capability matching
227/// and optional extension directories for future enhancements.
228///
229/// # Fields
230///
231/// * `nlibs` - Number of library entries in the cache
232/// * `len_strings` - Length of the string table in bytes
233/// * `flags` - Endianness and format flags (2 = little endian)
234/// * `extension_offset` - File offset to extension directory (0 = none)
235/// * `entries` - Vector of new format file entries with hardware capabilities
236/// * `extensions` - Optional extension directory for additional metadata
237///
238/// # Example
239///
240/// ```rust
241/// # use ld_so_cache::NewCache;
242/// let new_cache = NewCache {
243/// nlibs: 100,
244/// len_strings: 5000,
245/// flags: 2, // Little endian
246/// extension_offset: 0, // No extensions
247/// entries: vec![],
248/// extensions: None,
249/// };
250/// assert_eq!(new_cache.flags, 2);
251/// ```
252#[derive(Debug, Clone, PartialEq)]
253pub struct NewCache {
254 pub nlibs: u32,
255 pub len_strings: u32,
256 pub flags: u8,
257 pub extension_offset: u32,
258 pub entries: Vec<NewFileEntry>,
259 pub extensions: Option<ExtensionDirectory>,
260}
261
262/// A single library entry in the new cache format with hardware capabilities.
263///
264/// # Fields
265///
266/// * `flags` - Library type flags (same as old format)
267/// * `key` - Offset into string table for library name
268/// * `value` - Offset into string table for library path
269/// * `osversion_unused` - Unused field (always 0)
270/// * `hwcap` - Hardware capability mask indicating required processor features
271///
272/// # Hardware Capabilities
273///
274/// The `hwcap` field encodes processor features required by the library:
275/// * Bits 0-51: Various CPU features (SSE, AVX, etc.)
276/// * Bits 52-61: ISA level (x86-64-v2, x86-64-v3, etc.)
277/// * Bit 62: Extension flag
278/// * Bit 63: Reserved
279///
280/// # Example
281///
282/// ```rust
283/// # use ld_so_cache::NewFileEntry;
284/// let entry = NewFileEntry {
285/// flags: 1,
286/// key: 0,
287/// value: 20,
288/// osversion_unused: 0,
289/// hwcap: 0x1000000000000000, // Some capability set
290/// };
291///
292/// // Check for extension flag
293/// let has_extension = entry.hwcap & (1u64 << 62) != 0;
294///
295/// // Extract ISA level
296/// let isa_level = (entry.hwcap >> 52) & 0x3ff;
297/// ```
298#[derive(Debug, Clone, PartialEq)]
299pub struct NewFileEntry {
300 pub flags: i32,
301 pub key: u32,
302 pub value: u32,
303 pub osversion_unused: u32,
304 pub hwcap: u64,
305}
306
307/// Directory of extension sections for future cache format enhancements.
308///
309/// This allows the cache format to be extended with additional metadata
310/// while maintaining backward compatibility.
311///
312/// # Fields
313///
314/// * `count` - Number of extension sections
315/// * `sections` - Vector of extension section descriptors
316///
317/// # Example
318///
319/// ```rust
320/// # use ld_so_cache::{ExtensionDirectory, ExtensionSection};
321/// let ext_dir = ExtensionDirectory {
322/// count: 2,
323/// sections: vec![
324/// ExtensionSection { tag: 1, flags: 0, offset: 100, size: 50 },
325/// ExtensionSection { tag: 2, flags: 0, offset: 150, size: 30 },
326/// ],
327/// };
328/// assert_eq!(ext_dir.count, 2);
329/// ```
330#[derive(Debug, Clone, PartialEq)]
331pub struct ExtensionDirectory {
332 pub count: u32,
333 pub sections: Vec<ExtensionSection>,
334}
335
336/// A single extension section descriptor.
337///
338/// Extension sections allow for future enhancements to the cache format
339/// without breaking existing parsers.
340///
341/// # Fields
342///
343/// * `tag` - Identifies the type of extension data
344/// * `flags` - Extension-specific flags
345/// * `offset` - File offset where extension data begins
346/// * `size` - Size of extension data in bytes
347///
348/// # Example
349///
350/// ```rust
351/// # use ld_so_cache::ExtensionSection;
352/// let section = ExtensionSection {
353/// tag: 1, // Extension type 1
354/// flags: 0, // No special flags
355/// offset: 1000, // Data starts at offset 1000
356/// size: 256, // 256 bytes of data
357/// };
358/// ```
359#[derive(Debug, Clone, PartialEq)]
360pub struct ExtensionSection {
361 pub tag: u32,
362 pub flags: u32,
363 pub offset: u32,
364 pub size: u32,
365}
366
367/// Represents the different cache format combinations that can be found.
368///
369/// Real-world cache files may contain only the old format, only the new format,
370/// or both formats for backward compatibility.
371///
372/// # Variants
373///
374/// * `OldOnly` - File contains only the legacy ld.so-1.7.0 format
375/// * `NewOnly` - File contains only the modern glibc format
376/// * `Both` - File contains both formats (common for compatibility)
377///
378/// # Example
379///
380/// ```rust
381/// # use ld_so_cache::{CacheFormat, OldCache, NewCache};
382/// # let old_cache = OldCache { nlibs: 0, entries: vec![] };
383/// # let new_cache = NewCache { nlibs: 0, len_strings: 0, flags: 2, extension_offset: 0, entries: vec![], extensions: None };
384/// let format = CacheFormat::Both {
385/// old: old_cache,
386/// new: new_cache,
387/// };
388///
389/// match format {
390/// CacheFormat::OldOnly(_) => println!("Legacy format only"),
391/// CacheFormat::NewOnly(_) => println!("Modern format only"),
392/// CacheFormat::Both { .. } => println!("Both formats present"),
393/// }
394/// ```
395#[derive(Debug, Clone, PartialEq)]
396pub enum CacheFormat {
397 OldOnly(OldCache),
398 NewOnly(NewCache),
399 Both { old: OldCache, new: NewCache },
400}
401
402/// A processed library entry extracted from the cache.
403///
404/// This represents a single library mapping with resolved strings and
405/// optional hardware capability information.
406///
407/// # Fields
408///
409/// * `library_name` - The library name (e.g., "libc.so.6")
410/// * `library_path` - Full path to the library file
411/// * `flags` - Library type flags (bit 0: ELF library)
412/// * `hwcap` - Hardware capabilities (None for old format entries)
413///
414/// # Serialization
415///
416/// When serialized to JSON, hardware capabilities are formatted as
417/// hexadecimal strings (e.g., "0x0000000000001000").
418///
419/// # Example
420///
421/// ```rust
422/// # use ld_so_cache::CacheEntry;
423/// let entry = CacheEntry {
424/// library_name: "libc.so.6".to_string(),
425/// library_path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
426/// flags: 1, // ELF library
427/// hwcap: Some(0x1000), // Some hardware capability
428/// };
429///
430/// // Check if it's an ELF library
431/// let is_elf = entry.flags & 1 != 0;
432/// assert!(is_elf);
433///
434/// // Check for hardware capabilities
435/// if let Some(hwcap) = entry.hwcap {
436/// println!("Hardware capabilities: 0x{:016x}", hwcap);
437/// }
438/// ```
439#[derive(Debug, Clone, PartialEq, serde::Serialize)]
440pub struct CacheEntry {
441 pub library_name: String,
442 pub library_path: String,
443 pub flags: i32,
444 #[serde(with = "hwcap_format", skip_serializing_if = "Option::is_none")]
445 pub hwcap: Option<u64>,
446}
447
448mod hwcap_format {
449 use serde::{Serializer, Serialize};
450
451 pub fn serialize<S>(hwcap: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
452 where
453 S: Serializer,
454 {
455 match hwcap {
456 Some(value) => format!("0x{value:016x}").serialize(serializer),
457 None => serializer.serialize_none(),
458 }
459 }
460}
461
462impl LdCache {
463 /// Extracts all library entries from the cache.
464 ///
465 /// This method processes both old and new format entries, converting raw
466 /// cache data into user-friendly `CacheEntry` structures. If both formats
467 /// are present, only the new format entries are returned as they include
468 /// hardware capability information.
469 ///
470 /// # Returns
471 ///
472 /// A vector of `CacheEntry` structures containing library names, paths,
473 /// flags, and hardware capabilities (when available).
474 ///
475 /// # Errors
476 ///
477 /// * `CacheError::InvalidStringOffset` - String offset points outside the string table
478 /// * `CacheError::ParseError` - String contains invalid UTF-8 characters
479 ///
480 /// Invalid entries are silently skipped rather than causing the entire
481 /// operation to fail.
482 ///
483 /// # Example
484 ///
485 /// ```rust
486 /// # use ld_so_cache::parsers::parse_ld_cache;
487 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
488 /// # let data = vec![]; // Would be actual cache data
489 /// # let cache = parse_ld_cache(&data)?;
490 /// let entries = cache.get_entries()?;
491 ///
492 /// for entry in entries {
493 /// println!("{} -> {}", entry.library_name, entry.library_path);
494 ///
495 /// if entry.flags & 1 != 0 {
496 /// println!(" ELF library");
497 /// }
498 ///
499 /// if let Some(hwcap) = entry.hwcap {
500 /// println!(" Hardware capabilities: 0x{:016x}", hwcap);
501 /// }
502 /// }
503 /// # Ok(())
504 /// # }
505 /// ```
506 pub fn get_entries(&self) -> Result<Vec<CacheEntry>, CacheError> {
507 let string_table = &self.string_table;
508 let mut entries = Vec::new();
509
510 if let Some(new_cache) = &self.new_format {
511 for entry in &new_cache.entries {
512 // For new format, offsets might be absolute file offsets
513 let key_offset = if self.string_table_offset > 0 && entry.key as usize >= self.string_table_offset {
514 entry.key as usize - self.string_table_offset
515 } else {
516 entry.key as usize
517 };
518
519 let value_offset = if self.string_table_offset > 0 && entry.value as usize >= self.string_table_offset {
520 entry.value as usize - self.string_table_offset
521 } else {
522 entry.value as usize
523 };
524
525 if key_offset >= string_table.len() || value_offset >= string_table.len() {
526 continue; // Skip invalid entries instead of failing
527 }
528
529 let name = extract_string(string_table, key_offset)?;
530 let path = extract_string(string_table, value_offset)?;
531 entries.push(CacheEntry {
532 library_name: name,
533 library_path: path,
534 flags: entry.flags,
535 hwcap: Some(entry.hwcap),
536 });
537 }
538 } else if let Some(old_cache) = &self.old_format {
539 for entry in &old_cache.entries {
540 if entry.key as usize >= string_table.len() || entry.value as usize >= string_table.len() {
541 continue; // Skip invalid entries instead of failing
542 }
543 let name = extract_string(string_table, entry.key as usize)?;
544 let path = extract_string(string_table, entry.value as usize)?;
545 entries.push(CacheEntry {
546 library_name: name,
547 library_path: path,
548 flags: entry.flags,
549 hwcap: None,
550 });
551 }
552 }
553
554 Ok(entries)
555 }
556}
557
558/// Errors that can occur during cache parsing and processing.
559///
560/// This enum covers all possible failure modes when parsing `ld.so.cache` files,
561/// from file format issues to data corruption.
562///
563/// # Variants
564///
565/// * `ParseError` - Generic parsing failure with descriptive message
566/// * `InvalidStringOffset` - String offset points outside the string table
567/// * `InvalidMagic` - File doesn't start with a recognized magic number
568/// * `TruncatedFile` - File is too short to contain valid cache data
569/// * `InvalidEndianness` - Unsupported byte order in cache file
570///
571/// # Example
572///
573/// ```rust
574/// # use ld_so_cache::{CacheError, parsers::parse_ld_cache};
575/// let invalid_data = b"not a cache file";
576/// let result = parse_ld_cache(invalid_data);
577///
578/// match result {
579/// Err(CacheError::InvalidMagic) => {
580/// println!("File is not a valid ld.so.cache");
581/// }
582/// Err(CacheError::TruncatedFile) => {
583/// println!("Cache file is incomplete");
584/// }
585/// Err(e) => println!("Other error: {}", e),
586/// Ok(_) => println!("Successfully parsed"),
587/// }
588/// ```
589#[derive(Debug, Clone, PartialEq)]
590pub enum CacheError {
591 /// Generic parsing error with descriptive message
592 ParseError(String),
593 /// String offset points beyond the string table bounds
594 InvalidStringOffset(usize),
595 /// File doesn't begin with a recognized magic number
596 InvalidMagic,
597 /// File is too short to contain required cache structures
598 TruncatedFile,
599 /// Cache uses unsupported byte order
600 InvalidEndianness,
601}
602
603impl std::fmt::Display for CacheError {
604 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
605 match self {
606 CacheError::ParseError(msg) => write!(f, "Parse error: {msg}"),
607 CacheError::InvalidStringOffset(offset) => write!(f, "Invalid string offset: {offset}"),
608 CacheError::InvalidMagic => write!(f, "Invalid magic number"),
609 CacheError::TruncatedFile => write!(f, "Truncated file"),
610 CacheError::InvalidEndianness => write!(f, "Invalid endianness"),
611 }
612 }
613}
614
615impl std::error::Error for CacheError {}
616
617fn extract_string(string_table: &[u8], offset: usize) -> Result<String, CacheError> {
618 if offset >= string_table.len() {
619 return Err(CacheError::InvalidStringOffset(offset));
620 }
621
622 let slice = &string_table[offset..];
623 let null_pos = slice
624 .iter()
625 .position(|&b| b == 0)
626 .ok_or(CacheError::InvalidStringOffset(offset))?;
627
628 String::from_utf8(slice[..null_pos].to_vec())
629 .map_err(|_| CacheError::ParseError("Invalid UTF-8 in string".to_string()))
630}
631
632pub mod parsers;
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::parsers::parse_ld_cache;
638
639 #[test]
640 fn test_extract_string() {
641 let string_table = b"hello\0world\0test\0";
642
643 assert_eq!(extract_string(string_table, 0).unwrap(), "hello");
644 assert_eq!(extract_string(string_table, 6).unwrap(), "world");
645 assert_eq!(extract_string(string_table, 12).unwrap(), "test");
646
647 assert!(extract_string(string_table, 100).is_err());
648 }
649
650 #[test]
651 fn test_extract_string_edge_cases() {
652 let string_table = b"a\0";
653 assert_eq!(extract_string(string_table, 0).unwrap(), "a");
654
655 let empty_string_table = b"\0";
656 assert_eq!(extract_string(empty_string_table, 0).unwrap(), "");
657
658 let no_null_terminator = b"hello";
659 assert!(extract_string(no_null_terminator, 0).is_err());
660
661 let string_table = b"hello\0";
662 assert!(extract_string(string_table, 10).is_err());
663 }
664
665 #[test]
666 fn test_cache_entry_creation() {
667 let entry = CacheEntry {
668 library_name: "libc.so.6".to_string(),
669 library_path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
670 flags: 1,
671 hwcap: Some(0x1234_5678),
672 };
673
674 assert_eq!(entry.library_name, "libc.so.6");
675 assert_eq!(entry.flags, 1);
676 assert_eq!(entry.hwcap, Some(0x1234_5678));
677 }
678
679 #[test]
680 fn test_parse_old_format_cache() {
681 let mut data = Vec::new();
682
683 data.extend_from_slice(b"ld.so-1.7.0");
684 data.extend_from_slice(&2u32.to_le_bytes());
685
686 data.extend_from_slice(&1i32.to_le_bytes());
687 data.extend_from_slice(&0u32.to_le_bytes());
688 data.extend_from_slice(&10u32.to_le_bytes());
689
690 data.extend_from_slice(&1i32.to_le_bytes());
691 data.extend_from_slice(&20u32.to_le_bytes());
692 data.extend_from_slice(&40u32.to_le_bytes());
693
694 data.extend_from_slice(b"libc.so.6\0/lib/libc.so.6\0libm.so.6\0/lib/libm.so.6\0");
695
696 let cache = parse_ld_cache(&data).unwrap();
697 assert!(cache.old_format.is_some());
698 assert!(cache.new_format.is_none());
699
700 let old_cache = cache.old_format.as_ref().unwrap();
701 assert_eq!(old_cache.nlibs, 2);
702 assert_eq!(old_cache.entries.len(), 2);
703 assert_eq!(old_cache.entries[0].flags, 1);
704 assert_eq!(old_cache.entries[0].key, 0);
705 assert_eq!(old_cache.entries[0].value, 10);
706
707 let entries = cache.get_entries().unwrap();
708 assert_eq!(entries.len(), 2);
709 assert_eq!(entries[0].library_name, "libc.so.6");
710 assert_eq!(entries[0].library_path, "/lib/libc.so.6");
711 assert!(entries[0].hwcap.is_none());
712 }
713
714 #[test]
715 fn test_parse_new_format_cache() {
716 let mut data = Vec::new();
717
718 data.extend_from_slice(b"glibc-ld.so.cache");
719 data.extend_from_slice(b"1.1");
720 data.extend_from_slice(&1u32.to_le_bytes());
721 data.extend_from_slice(&30u32.to_le_bytes());
722 data.push(2);
723 data.extend_from_slice(&[0, 0, 0]);
724 data.extend_from_slice(&0u32.to_le_bytes());
725 data.extend_from_slice(&[0; 12]);
726
727 data.extend_from_slice(&1i32.to_le_bytes());
728 data.extend_from_slice(&0u32.to_le_bytes());
729 data.extend_from_slice(&10u32.to_le_bytes());
730 data.extend_from_slice(&0u32.to_le_bytes());
731 data.extend_from_slice(&0x1234_5678_u64.to_le_bytes());
732
733 data.extend_from_slice(b"libc.so.6\0/lib/libc.so.6\0");
734
735 let cache = parse_ld_cache(&data).unwrap();
736 assert!(cache.new_format.is_some());
737 assert!(cache.old_format.is_none());
738
739 let new_cache = cache.new_format.as_ref().unwrap();
740 assert_eq!(new_cache.nlibs, 1);
741 assert_eq!(new_cache.len_strings, 30);
742 assert_eq!(new_cache.flags, 2);
743 assert_eq!(new_cache.entries.len(), 1);
744 assert_eq!(new_cache.entries[0].hwcap, 0x1234_5678);
745
746 let entries = cache.get_entries().unwrap();
747 assert_eq!(entries.len(), 1);
748 assert_eq!(entries[0].library_name, "libc.so.6");
749 assert_eq!(entries[0].library_path, "/lib/libc.so.6");
750 assert_eq!(entries[0].hwcap, Some(0x1234_5678));
751 }
752
753 #[test]
754 fn test_parse_invalid_magic() {
755 let invalid_data = b"invalid-magic-that-is-long-enough";
756 let result = parse_ld_cache(invalid_data);
757 assert!(matches!(result, Err(CacheError::InvalidMagic)));
758 }
759
760 #[test]
761 fn test_parse_truncated_file() {
762 let truncated_data = b"ld.so-1.7.0";
763 let result = parse_ld_cache(truncated_data);
764 assert!(matches!(result, Err(CacheError::TruncatedFile)));
765 }
766
767 #[test]
768 fn test_cache_error_display() {
769 let error = CacheError::ParseError("test error".to_string());
770 assert_eq!(format!("{error}"), "Parse error: test error");
771
772 let error = CacheError::InvalidStringOffset(42);
773 assert_eq!(format!("{error}"), "Invalid string offset: 42");
774
775 let error = CacheError::InvalidMagic;
776 assert_eq!(format!("{error}"), "Invalid magic number");
777
778 let error = CacheError::TruncatedFile;
779 assert_eq!(format!("{error}"), "Truncated file");
780
781 let error = CacheError::InvalidEndianness;
782 assert_eq!(format!("{error}"), "Invalid endianness");
783 }
784
785 #[test]
786 fn test_empty_cache() {
787 let mut data = Vec::new();
788 data.extend_from_slice(b"ld.so-1.7.0");
789 data.extend_from_slice(&0u32.to_le_bytes());
790
791 let cache = parse_ld_cache(&data).unwrap();
792 let entries = cache.get_entries().unwrap();
793 assert_eq!(entries.len(), 0);
794 }
795
796 #[test]
797 fn test_ld_cache_get_entries_preference() {
798 let old_cache = OldCache {
799 nlibs: 1,
800 entries: vec![OldFileEntry {
801 flags: 1,
802 key: 0,
803 value: 10,
804 }],
805 };
806
807 let new_cache = NewCache {
808 nlibs: 1,
809 len_strings: 30,
810 flags: 2,
811 extension_offset: 0,
812 entries: vec![NewFileEntry {
813 flags: 1,
814 key: 0,
815 value: 10,
816 osversion_unused: 0,
817 hwcap: 0x1234_5678,
818 }],
819 extensions: None,
820 };
821
822 let string_table = b"libc.so.6\0/lib/libc.so.6\0".to_vec();
823
824 let cache_with_both = LdCache {
825 old_format: Some(old_cache),
826 new_format: Some(new_cache),
827 string_table: string_table.clone(),
828 string_table_offset: 0,
829 };
830
831 let entries = cache_with_both.get_entries().unwrap();
832 assert_eq!(entries.len(), 1);
833 assert_eq!(entries[0].hwcap, Some(0x1234_5678));
834 }
835
836 #[test]
837 fn test_hwcap_bit_decoding() {
838 // Test ISA level extraction (bits 52-61)
839 let hwcap_with_isa_v2: u64 = 2u64 << 52;
840 let isa_level = (hwcap_with_isa_v2 >> 52) & 0x3ff;
841 assert_eq!(isa_level, 2);
842
843 let hwcap_with_isa_v3: u64 = 3u64 << 52;
844 let isa_level = (hwcap_with_isa_v3 >> 52) & 0x3ff;
845 assert_eq!(isa_level, 3);
846
847 // Test extension flag (bit 62)
848 let hwcap_with_extension: u64 = 1u64 << 62;
849 assert!(hwcap_with_extension & (1u64 << 62) != 0);
850
851 let hwcap_without_extension: u64 = 0x0000_1234_5678_9ABC;
852 assert!(hwcap_without_extension & (1u64 << 62) == 0);
853
854 // Test CPU features mask (bits 0-51)
855 let hwcap_with_features: u64 = 0x000F_FFFF_FFFF_FFFF; // All feature bits set
856 let cpu_features = hwcap_with_features & ((1u64 << 52) - 1);
857 assert_eq!(cpu_features, 0x000F_FFFF_FFFF_FFFF);
858
859 // Test combined hwcap
860 let entry = NewFileEntry {
861 flags: 1,
862 key: 0,
863 value: 0,
864 osversion_unused: 0,
865 hwcap: 0x0020_0000_0000_1234, // ISA level 2 (2 << 52) + some features
866 };
867
868 let isa_level = (entry.hwcap >> 52) & 0x3ff;
869 let features = entry.hwcap & ((1u64 << 52) - 1);
870
871 assert_eq!(isa_level, 2);
872 assert_eq!(features, 0x1234);
873
874 // Test with extension flag
875 let entry_with_ext = NewFileEntry {
876 flags: 1,
877 key: 0,
878 value: 0,
879 osversion_unused: 0,
880 hwcap: 0x4000_0000_0000_0000, // Just extension flag
881 };
882
883 let has_extension = entry_with_ext.hwcap & (1u64 << 62) != 0;
884 assert!(has_extension);
885 }
886
887 #[test]
888 fn test_graceful_degradation_with_invalid_entries() {
889 // Create cache with mix of valid and invalid entries
890 let new_cache = NewCache {
891 nlibs: 4,
892 len_strings: 50,
893 flags: 2,
894 extension_offset: 0,
895 entries: vec![
896 NewFileEntry {
897 flags: 1,
898 key: 0,
899 value: 10,
900 osversion_unused: 0,
901 hwcap: 0x1234,
902 },
903 NewFileEntry {
904 flags: 1,
905 key: 1000, // Invalid offset
906 value: 2000, // Invalid offset
907 osversion_unused: 0,
908 hwcap: 0x5678,
909 },
910 NewFileEntry {
911 flags: 1,
912 key: 26,
913 value: 36,
914 osversion_unused: 0,
915 hwcap: 0x9ABC,
916 },
917 NewFileEntry {
918 flags: 1,
919 key: 500, // Invalid offset
920 value: 10,
921 osversion_unused: 0,
922 hwcap: 0xDEF0,
923 },
924 ],
925 extensions: None,
926 };
927
928 let string_table = b"libc.so.6\0/lib/libc.so.6\0\0libm.so.6\0/lib/libm.so.6\0".to_vec();
929
930 let cache = LdCache {
931 old_format: None,
932 new_format: Some(new_cache),
933 string_table,
934 string_table_offset: 0,
935 };
936
937 // Should skip invalid entries and return only valid ones
938 let entries = cache.get_entries().unwrap();
939 assert_eq!(entries.len(), 2); // Only 2 valid entries
940 assert_eq!(entries[0].library_name, "libc.so.6");
941 assert_eq!(entries[1].library_name, "libm.so.6");
942 }
943
944 #[test]
945 fn test_string_table_offset_adjustment() {
946 // Test with absolute file offsets
947 let new_cache = NewCache {
948 nlibs: 1,
949 len_strings: 30,
950 flags: 2,
951 extension_offset: 0,
952 entries: vec![NewFileEntry {
953 flags: 1,
954 key: 1000, // Absolute offset
955 value: 1010, // Absolute offset
956 osversion_unused: 0,
957 hwcap: 0x1234,
958 }],
959 extensions: None,
960 };
961
962 let string_table = b"libc.so.6\0/lib/libc.so.6\0".to_vec();
963
964 let cache = LdCache {
965 old_format: None,
966 new_format: Some(new_cache),
967 string_table,
968 string_table_offset: 1000, // String table starts at offset 1000
969 };
970
971 let entries = cache.get_entries().unwrap();
972 assert_eq!(entries.len(), 1);
973 assert_eq!(entries[0].library_name, "libc.so.6");
974 assert_eq!(entries[0].library_path, "/lib/libc.so.6");
975 }
976
977 #[test]
978 fn test_flags_architecture_decoding() {
979 // Test i386 (arch bits = 0)
980 let i386_flags = 0x0001; // ELF + arch 0
981 let arch_bits = (i386_flags >> 8) & 0xf;
982 assert_eq!(arch_bits, 0);
983
984 // Test x86_64 (arch bits = 3)
985 let x86_64_flags = 0x0301; // ELF + arch 3
986 let arch_bits = (x86_64_flags >> 8) & 0xf;
987 assert_eq!(arch_bits, 3);
988
989 // Test libx32 (arch bits = 8)
990 let libx32_flags = 0x0801; // ELF + arch 8
991 let arch_bits = (libx32_flags >> 8) & 0xf;
992 assert_eq!(arch_bits, 8);
993 }
994
995 #[test]
996 fn test_endianness_flag_values() {
997 // Test all documented flag values
998 let test_cases = vec![
999 (0u8, "Endianness unset (legacy)"),
1000 (1u8, "Invalid cache"),
1001 (2u8, "Little endian"),
1002 (3u8, "Big endian"),
1003 ];
1004
1005 for (flag_value, expected_meaning) in test_cases {
1006 let new_cache = NewCache {
1007 nlibs: 0,
1008 len_strings: 0,
1009 flags: flag_value,
1010 extension_offset: 0,
1011 entries: vec![],
1012 extensions: None,
1013 };
1014
1015 // Verify the flag value is stored correctly
1016 assert_eq!(new_cache.flags, flag_value);
1017
1018 // Verify the meaning matches documentation
1019 let meaning = match flag_value {
1020 0 => "Endianness unset (legacy)",
1021 1 => "Invalid cache",
1022 2 => "Little endian",
1023 3 => "Big endian",
1024 _ => "Unknown",
1025 };
1026 assert_eq!(meaning, expected_meaning);
1027 }
1028 }
1029
1030 #[test]
1031 fn test_extension_tag_meanings() {
1032 let ext_dir = ExtensionDirectory {
1033 count: 2,
1034 sections: vec![
1035 ExtensionSection {
1036 tag: 1,
1037 flags: 0,
1038 offset: 100,
1039 size: 50,
1040 },
1041 ExtensionSection {
1042 tag: 2,
1043 flags: 0,
1044 offset: 150,
1045 size: 30,
1046 },
1047 ],
1048 };
1049
1050 // Verify tag meanings as documented
1051 assert_eq!(ext_dir.sections[0].tag, 1); // cache_extension_tag_generator
1052 assert_eq!(ext_dir.sections[1].tag, 2); // cache_extension_tag_glibc_hwcaps
1053 }
1054
1055 #[test]
1056 fn test_cache_entry_serialization_with_hwcap() {
1057 let entry = CacheEntry {
1058 library_name: "libc.so.6".to_string(),
1059 library_path: "/lib/x86_64-linux-gnu/libc.so.6".to_string(),
1060 flags: 0x0301, // x86_64 ELF
1061 hwcap: Some(0x0000_0000_0000_1000),
1062 };
1063
1064 // Serialize to JSON
1065 let json = serde_json::to_string(&entry).unwrap();
1066
1067 // Verify hwcap is formatted as hex string
1068 assert!(json.contains("\"hwcap\":\"0x0000000000001000\""));
1069
1070 // Test without hwcap
1071 let entry_no_hwcap = CacheEntry {
1072 library_name: "libm.so.6".to_string(),
1073 library_path: "/lib/libm.so.6".to_string(),
1074 flags: 1,
1075 hwcap: None,
1076 };
1077
1078 let json_no_hwcap = serde_json::to_string(&entry_no_hwcap).unwrap();
1079 assert!(!json_no_hwcap.contains("hwcap"));
1080 }
1081}