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}