1use crate::elf::{Architecture, ElfClass, Endianness, Machine};
13use std::{
14 cmp::Ordering,
15 collections::HashMap,
16 path::{Path, PathBuf},
17};
18
19const OLD_MAGIC: &[u8] = b"ld.so-1.7.0";
20const NEW_MAGIC: &[u8] = b"glibc-ld.so.cache";
21const NEW_VERSION: &[u8] = b"1.1";
22
23const OLD_HEADER_LEN: usize = 16;
25const OLD_ENTRY_LEN: usize = 12;
26const NEW_HEADER_LEN: usize = 48;
28const NEW_ENTRY_LEN: usize = 24;
29
30const CACHE_ENTRIES_MAX: usize = 65_536;
35const CACHE_BYTES_MAX: u64 = 16 * 1024 * 1024;
38const CACHE_STRING_LEN_MAX: usize = 4096;
41const CACHE_CANDIDATES_PER_SONAME_MAX: usize = 256;
44
45#[derive(Debug, Clone, Default)]
46pub struct LdCache {
47 entries: HashMap<String, Vec<PathBuf>>,
49 records: HashMap<String, Vec<CacheRecord>>,
51 len: usize,
54}
55
56#[derive(Debug, Clone)]
60struct CacheRecord {
61 soname: String,
62 path: PathBuf,
63 flags: u32,
64 osversion: u32,
65 hwcap: u64,
66}
67
68impl LdCache {
69 pub fn parse(bytes: &[u8]) -> LdCache {
72 let mut cache = LdCache::default();
73 if u64::try_from(bytes.len())
74 .ok()
75 .is_none_or(|len| len > CACHE_BYTES_MAX)
76 {
77 return cache;
78 }
79 let pairs = if starts_with(bytes, 0, NEW_MAGIC) {
80 parse_new(bytes, 0)
81 } else if starts_with(bytes, 0, OLD_MAGIC) {
82 let nlibs = match read_u32(bytes, 12) {
83 Some(n) => n as usize,
84 None => return cache,
85 };
86 let new_offset = align8(
87 nlibs
88 .saturating_mul(OLD_ENTRY_LEN)
89 .saturating_add(OLD_HEADER_LEN),
90 );
91 if starts_with(bytes, new_offset, NEW_MAGIC) {
92 parse_new(bytes, new_offset)
93 } else {
94 parse_old(bytes, nlibs)
95 }
96 } else {
97 Vec::new()
98 };
99
100 for record in pairs {
101 if !record.path.is_absolute() {
104 continue;
105 }
106 let list = cache.entries.entry(record.soname.clone()).or_default();
107 if list.len() < CACHE_CANDIDATES_PER_SONAME_MAX && !list.contains(&record.path) {
108 list.push(record.path.clone());
109 cache
110 .records
111 .entry(record.soname.clone())
112 .or_default()
113 .push(record);
114 cache.len += 1;
115 }
116 }
117 cache
118 }
119
120 pub fn load(path: &Path) -> Option<LdCache> {
121 if std::fs::metadata(path).ok()?.len() > CACHE_BYTES_MAX {
122 return None;
123 }
124 let bytes = std::fs::read(path).ok()?;
125 let cache = LdCache::parse(&bytes);
126 if cache.entries.is_empty() {
127 None
128 } else {
129 Some(cache)
130 }
131 }
132
133 pub fn lookup(&self, soname: &str) -> &[PathBuf] {
134 self.entries.get(soname).map(Vec::as_slice).unwrap_or(&[])
135 }
136
137 pub fn lookup_compatible(&self, soname: &str, architecture: &Architecture) -> Vec<PathBuf> {
143 let Some(expected_flags) =
144 entry_flags(architecture).and_then(|flags| u32::try_from(flags).ok())
145 else {
146 return Vec::new();
147 };
148 self.records
149 .get(soname)
150 .into_iter()
151 .flatten()
152 .filter(|entry| {
153 entry.flags == expected_flags && entry.osversion == 0 && entry.hwcap == 0
154 })
155 .map(|entry| entry.path.clone())
156 .collect()
157 }
158
159 pub fn entry_count(&self) -> usize {
161 self.len
162 }
163
164 pub fn is_empty(&self) -> bool {
165 self.entries.is_empty()
166 }
167}
168
169fn align8(value: usize) -> usize {
170 value.saturating_add(7) & !7
171}
172
173fn starts_with(bytes: &[u8], offset: usize, magic: &[u8]) -> bool {
176 let Some(end) = offset.checked_add(magic.len()) else {
177 return false;
178 };
179 bytes.get(offset..end) == Some(magic)
180}
181
182fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
183 let slice = bytes.get(offset..offset.checked_add(4)?)?;
184 Some(u32::from_le_bytes(slice.try_into().ok()?))
185}
186
187fn read_string(bytes: &[u8], base: usize, offset: u32) -> Option<String> {
189 let start = base.checked_add(offset as usize)?;
190 let rest = bytes.get(start..)?;
191 let end = rest
192 .iter()
193 .take(CACHE_STRING_LEN_MAX + 1)
194 .position(|&b| b == 0)?;
195 if end > CACHE_STRING_LEN_MAX {
196 return None;
197 }
198 std::str::from_utf8(&rest[..end]).ok().map(str::to_string)
199}
200
201fn entry_capacity(bytes: &[u8], base: usize, header: usize, entry: usize) -> usize {
206 bytes
207 .len()
208 .saturating_sub(base.saturating_add(header))
209 .saturating_div(entry)
210}
211
212fn parse_old(bytes: &[u8], nlibs: usize) -> Vec<CacheRecord> {
215 let capacity = entry_capacity(bytes, 0, OLD_HEADER_LEN, OLD_ENTRY_LEN);
216 let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
217 let mut out = Vec::with_capacity(count);
218 for index in 0..count {
219 let Some(offset) = index
220 .checked_mul(OLD_ENTRY_LEN)
221 .and_then(|at| at.checked_add(OLD_HEADER_LEN))
222 else {
223 break;
224 };
225 let (Some(flags), Some(key), Some(value)) = (
226 read_u32(bytes, offset),
227 read_u32(bytes, offset + 4),
228 read_u32(bytes, offset + 8),
229 ) else {
230 break;
231 };
232 if let (Some(soname), Some(path)) =
233 (read_string(bytes, 0, key), read_string(bytes, 0, value))
234 {
235 out.push(CacheRecord {
236 soname,
237 path: PathBuf::from(path),
238 flags,
239 osversion: 0,
240 hwcap: 0,
241 });
242 }
243 }
244 out
245}
246
247fn parse_new(bytes: &[u8], base: usize) -> Vec<CacheRecord> {
248 if !starts_with(bytes, base + NEW_MAGIC.len(), NEW_VERSION) {
249 return Vec::new();
250 }
251 let Some(header_flags) = bytes.get(base + 28).copied() else {
254 return Vec::new();
255 };
256 if header_flags != 0 && header_flags & 0x03 != FLAGS_ENDIAN_LITTLE {
259 return Vec::new();
260 }
261 let nlibs = match read_u32(bytes, base + 20) {
262 Some(n) => n as usize,
263 None => return Vec::new(),
264 };
265 let capacity = entry_capacity(bytes, base, NEW_HEADER_LEN, NEW_ENTRY_LEN);
266 let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
267 let mut out = Vec::with_capacity(count);
268 for index in 0..count {
269 let Some(offset) = base
270 .checked_add(NEW_HEADER_LEN)
271 .and_then(|start| index.checked_mul(NEW_ENTRY_LEN)?.checked_add(start))
272 else {
273 break;
274 };
275 let (Some(flags), Some(key), Some(value), Some(osversion)) = (
276 read_u32(bytes, offset),
277 read_u32(bytes, offset + 4),
278 read_u32(bytes, offset + 8),
279 read_u32(bytes, offset + 12),
280 ) else {
281 break;
282 };
283 let hwcap = bytes
284 .get(offset + 16..offset + NEW_ENTRY_LEN)
285 .and_then(|value| value.try_into().ok())
286 .map(u64::from_le_bytes);
287 if let (Some(soname), Some(path), Some(hwcap)) = (
288 read_string(bytes, base, key),
289 read_string(bytes, base, value),
290 hwcap,
291 ) {
292 out.push(CacheRecord {
293 soname,
294 path: PathBuf::from(path),
295 flags,
296 osversion,
297 hwcap,
298 });
299 }
300 }
301 out
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct CacheEntry {
307 pub soname: String,
308 pub path: PathBuf,
310}
311
312fn entry_flags(architecture: &Architecture) -> Option<i32> {
317 const FLAG_ELF_LIBC6: i32 = 0x0003;
318 const FLAG_X8664_LIB64: i32 = 0x0300;
319 const FLAG_AARCH64_LIB64: i32 = 0x0a00;
320 match (architecture.machine, architecture.class) {
321 (Machine::X86_64, ElfClass::Elf64) => Some(FLAG_X8664_LIB64 | FLAG_ELF_LIBC6),
322 (Machine::Aarch64, ElfClass::Elf64) => Some(FLAG_AARCH64_LIB64 | FLAG_ELF_LIBC6),
323 _ => None,
324 }
325}
326
327const FLAGS_ENDIAN_LITTLE: u8 = 2;
331
332struct StringTable {
335 bytes: Vec<u8>,
336 offsets: Vec<(u32, u32)>,
337}
338
339fn encode_strings(entries: &[&CacheEntry], base: usize) -> Option<StringTable> {
346 let mut strings: Vec<u8> = Vec::new();
347 let mut offsets: Vec<(u32, u32)> = Vec::with_capacity(entries.len());
348
349 for entry in entries {
350 let path = entry.path.to_str()?;
353 if entry.soname.contains('\0') || path.contains('\0') {
354 return None;
355 }
356 let key = u32::try_from(base + strings.len()).ok()?;
357 strings.extend_from_slice(entry.soname.as_bytes());
358 strings.push(0);
359 let value = u32::try_from(base + strings.len()).ok()?;
360 strings.extend_from_slice(path.as_bytes());
361 strings.push(0);
362 offsets.push((key, value));
363 }
364 Some(StringTable {
365 bytes: strings,
366 offsets,
367 })
368}
369
370pub fn build(architecture: &Architecture, entries: &[CacheEntry]) -> Option<Vec<u8>> {
376 if entries.iter().any(|entry| !entry.path.is_absolute()) {
377 return None;
378 }
379
380 let flags = entry_flags(architecture)?;
381 if architecture.endianness != Endianness::Little {
382 return None;
385 }
386
387 let mut entries: Vec<&CacheEntry> = entries.iter().collect();
388 entries.sort_by(|a, b| libcmp(&b.soname, &a.soname).then_with(|| a.path.cmp(&b.path)));
393 entries.dedup_by(|a, b| a.soname == b.soname && a.path == b.path);
394 assert!(
395 entries
396 .windows(2)
397 .all(|pair| libcmp(&pair[0].soname, &pair[1].soname) != Ordering::Less),
398 "the loader binary-searches downwards and needs descending order"
399 );
400
401 let base = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
402 let StringTable {
403 bytes: strings,
404 offsets,
405 } = encode_strings(&entries, base)?;
406 let mut out = Vec::with_capacity(base + strings.len());
407 out.extend_from_slice(NEW_MAGIC);
408 out.extend_from_slice(NEW_VERSION);
409 out.extend_from_slice(&u32::try_from(entries.len()).ok()?.to_le_bytes());
410 out.extend_from_slice(&u32::try_from(strings.len()).ok()?.to_le_bytes());
411 out.push(FLAGS_ENDIAN_LITTLE);
412 out.extend_from_slice(&[0, 0, 0]); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&[0u8; 12]); assert_eq!(out.len(), NEW_HEADER_LEN);
416
417 for (key, value) in offsets {
418 out.extend_from_slice(&flags.to_le_bytes());
419 out.extend_from_slice(&key.to_le_bytes());
420 out.extend_from_slice(&value.to_le_bytes());
421 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u64.to_le_bytes()); }
424 assert_eq!(out.len(), base);
425 out.extend_from_slice(&strings);
426 assert_eq!(out.len(), base + strings.len());
427
428 let written = LdCache::parse(&out);
431 assert_eq!(written.entry_count(), entries.len());
432 assert!(
433 entries
434 .iter()
435 .all(|entry| written.lookup(&entry.soname).contains(&entry.path))
436 );
437 Some(out)
438}
439
440fn libcmp(left: &str, right: &str) -> Ordering {
447 let (p1, p2) = (left.as_bytes(), right.as_bytes());
448 let (mut i, mut j) = (0usize, 0usize);
449 let at = |s: &[u8], k: usize| s.get(k).copied().unwrap_or(0);
450
451 while at(p1, i) != 0 {
452 let (c1, c2) = (at(p1, i), at(p2, j));
453 if c1.is_ascii_digit() {
454 if !c2.is_ascii_digit() {
455 return Ordering::Greater;
456 }
457 let mut v1 = 0u64;
458 while at(p1, i).is_ascii_digit() {
459 v1 = v1
460 .saturating_mul(10)
461 .saturating_add(u64::from(at(p1, i) - b'0'));
462 i += 1;
463 }
464 let mut v2 = 0u64;
465 while at(p2, j).is_ascii_digit() {
466 v2 = v2
467 .saturating_mul(10)
468 .saturating_add(u64::from(at(p2, j) - b'0'));
469 j += 1;
470 }
471 if v1 != v2 {
472 return v1.cmp(&v2);
473 }
474 } else if c2.is_ascii_digit() {
475 return Ordering::Less;
476 } else if c1 != c2 {
477 return c1.cmp(&c2);
478 } else {
479 i += 1;
480 j += 1;
481 }
482 }
483 at(p1, i).cmp(&at(p2, j))
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 fn offset(value: usize) -> u32 {
491 u32::try_from(value).expect("fixture offsets fit in u32")
492 }
493
494 fn new_format(entries: &[(&str, &str)]) -> Vec<u8> {
496 let mut strings = Vec::new();
497 let mut offsets = Vec::new();
498 for (soname, path) in entries {
499 let key = offset(strings.len());
500 strings.extend_from_slice(soname.as_bytes());
501 strings.push(0);
502 let value = offset(strings.len());
503 strings.extend_from_slice(path.as_bytes());
504 strings.push(0);
505 offsets.push((key, value));
506 }
507 let header_len = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
508
509 let mut out = Vec::new();
510 out.extend_from_slice(NEW_MAGIC);
511 out.extend_from_slice(NEW_VERSION);
512 out.extend_from_slice(&offset(entries.len()).to_le_bytes());
513 out.extend_from_slice(&offset(strings.len()).to_le_bytes());
514 out.push(FLAGS_ENDIAN_LITTLE);
515 out.extend_from_slice(&[0, 0, 0]);
516 out.extend_from_slice(&0u32.to_le_bytes());
517 out.extend_from_slice(&[0u8; 12]);
518 assert_eq!(out.len(), NEW_HEADER_LEN);
519
520 for (key, value) in &offsets {
521 out.extend_from_slice(&0x0300_0003u32.to_le_bytes());
522 out.extend_from_slice(&(key + offset(header_len)).to_le_bytes());
523 out.extend_from_slice(&(value + offset(header_len)).to_le_bytes());
524 out.extend_from_slice(&0u32.to_le_bytes());
525 out.extend_from_slice(&0u64.to_le_bytes());
526 }
527 out.extend_from_slice(&strings);
528 out
529 }
530
531 #[test]
532 fn parses_the_new_format() {
533 let bytes = new_format(&[
534 ("libc.so.6", "/lib/x86_64-linux-gnu/libc.so.6"),
535 ("libm.so.6", "/lib/x86_64-linux-gnu/libm.so.6"),
536 ]);
537 let cache = LdCache::parse(&bytes);
538 assert_eq!(cache.entry_count(), 2);
539 assert_eq!(
540 cache.lookup("libc.so.6"),
541 [PathBuf::from("/lib/x86_64-linux-gnu/libc.so.6")]
542 );
543 assert!(cache.lookup("libnope.so.1").is_empty());
544 }
545
546 #[test]
547 fn parses_an_old_format_header_with_an_appended_new_cache() {
548 let new = new_format(&[("libz.so.1", "/usr/lib/libz.so.1")]);
549 let nlibs = 1usize;
550 let mut bytes = Vec::new();
551 bytes.extend_from_slice(OLD_MAGIC);
552 bytes.push(0); bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
554 bytes.extend_from_slice(&[0u8; OLD_ENTRY_LEN]);
556 while bytes.len() < align8(OLD_HEADER_LEN + nlibs * OLD_ENTRY_LEN) {
557 bytes.push(0);
558 }
559 bytes.extend_from_slice(&new);
560
561 let cache = LdCache::parse(&bytes);
562 assert_eq!(
563 cache.lookup("libz.so.1"),
564 [PathBuf::from("/usr/lib/libz.so.1")]
565 );
566 }
567
568 fn x86_64() -> Architecture {
569 Architecture {
570 machine: Machine::X86_64,
571 class: ElfClass::Elf64,
572 endianness: Endianness::Little,
573 }
574 }
575
576 fn entry(soname: &str, path: &str) -> CacheEntry {
577 CacheEntry {
578 soname: soname.to_string(),
579 path: PathBuf::from(path),
580 }
581 }
582
583 fn header(bytes: &[u8]) -> (u32, u32, u8, u32) {
586 (
587 read_u32(bytes, 20).unwrap(),
588 read_u32(bytes, 24).unwrap(),
589 bytes[28],
590 read_u32(bytes, 32).unwrap(),
591 )
592 }
593
594 fn keys_in_file_order(bytes: &[u8]) -> Vec<String> {
595 let nlibs = read_u32(bytes, 20).unwrap() as usize;
596 (0..nlibs)
597 .map(|index| {
598 let offset = NEW_HEADER_LEN + index * NEW_ENTRY_LEN;
599 read_string(bytes, 0, read_u32(bytes, offset + 4).unwrap()).unwrap()
600 })
601 .collect()
602 }
603
604 #[test]
605 fn a_generated_cache_round_trips_through_the_parser() {
606 let bytes = build(
607 &x86_64(),
608 &[
609 entry("libcached.so.1", "/opt/cached/libcached.so.1"),
610 entry("libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6"),
611 ],
612 )
613 .unwrap();
614
615 let (nlibs, len_strings, flags, extension_offset) = header(&bytes);
616 assert_eq!(nlibs, 2);
617 assert_eq!(flags, FLAGS_ENDIAN_LITTLE, "glibc checks the byte order");
618 assert_eq!(extension_offset, 0, "no extension section is written");
619 assert_eq!(
620 bytes.len(),
621 NEW_HEADER_LEN + 2 * NEW_ENTRY_LEN + len_strings as usize
622 );
623
624 let cache = LdCache::parse(&bytes);
625 assert_eq!(cache.entry_count(), 2);
626 assert_eq!(
627 cache.lookup("libcached.so.1"),
628 [PathBuf::from("/opt/cached/libcached.so.1")]
629 );
630 assert!(cache.lookup("libnope.so.1").is_empty());
631 }
632
633 #[test]
635 fn entries_are_written_in_descending_libcmp_order() {
636 let bytes = build(
637 &x86_64(),
638 &[
639 entry("libaaa.so.1", "/a"),
640 entry("libzzz.so.1", "/z"),
641 entry("libmmm.so.9", "/m9"),
642 entry("libmmm.so.10", "/m10"),
643 ],
644 )
645 .unwrap();
646 assert_eq!(
647 keys_in_file_order(&bytes),
648 [
649 "libzzz.so.1",
650 "libmmm.so.10",
652 "libmmm.so.9",
653 "libaaa.so.1",
654 ]
655 );
656 }
657
658 #[test]
659 fn digit_runs_compare_numerically() {
660 assert_eq!(libcmp("libfoo.so.9", "libfoo.so.10"), Ordering::Less);
661 assert_eq!(libcmp("libfoo.so.10", "libfoo.so.9"), Ordering::Greater);
662 assert_eq!(libcmp("libfoo.so.1", "libfoo.so.1"), Ordering::Equal);
663 assert_eq!(libcmp("libfoo.so", "libfoo.so.1"), Ordering::Less);
665 assert_eq!(libcmp("lib1", "liba"), Ordering::Greater);
667 assert_eq!(libcmp("liba", "lib1"), Ordering::Less);
668 let huge = format!("lib{}", "9".repeat(40));
670 assert_eq!(libcmp(&huge, &huge), Ordering::Equal);
671 }
672
673 #[test]
674 fn identical_entries_collapse_and_output_is_stable() {
675 let entries = [
676 entry("libc.so.6", "/lib/libc.so.6"),
677 entry("libc.so.6", "/lib/libc.so.6"),
678 entry("libc.so.6", "/other/libc.so.6"),
679 ];
680 let bytes = build(&x86_64(), &entries).unwrap();
681 assert_eq!(read_u32(&bytes, 20).unwrap(), 2, "duplicates collapse");
682
683 let mut shuffled = entries.to_vec();
684 shuffled.reverse();
685 assert_eq!(
686 build(&x86_64(), &shuffled).unwrap(),
687 bytes,
688 "input order must not change the image"
689 );
690 }
691
692 #[test]
693 fn an_architecture_without_a_known_cache_id_is_refused() {
694 let unsupported = Architecture {
695 machine: Machine::RiscV64,
696 ..x86_64()
697 };
698 assert!(build(&unsupported, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
699 let big_endian = Architecture {
700 endianness: Endianness::Big,
701 ..x86_64()
702 };
703 assert!(build(&big_endian, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
704 assert!(build(&x86_64(), &[]).is_some());
706 }
707
708 #[test]
709 fn dropped_entries_are_not_counted() {
710 let bytes = new_format(&[
713 ("libc.so.6", "/lib/libc.so.6"),
714 ("librel.so.1", "relative/librel.so.1"),
715 ("libc.so.6", "/lib/libc.so.6"),
716 ]);
717 let cache = LdCache::parse(&bytes);
718 assert!(cache.lookup("librel.so.1").is_empty());
719 assert_eq!(
720 cache.entry_count(),
721 1,
722 "only what the cache kept is counted"
723 );
724 }
725
726 #[test]
727 fn candidates_per_soname_are_bounded_in_cache_order() {
728 let owned: Vec<(String, String)> = (0..=CACHE_CANDIDATES_PER_SONAME_MAX)
729 .map(|index| ("libmany.so".to_string(), format!("/lib/libmany-{index}.so")))
730 .collect();
731 let entries: Vec<(&str, &str)> = owned
732 .iter()
733 .map(|(soname, path)| (soname.as_str(), path.as_str()))
734 .collect();
735
736 let cache = LdCache::parse(&new_format(&entries));
737 let found = cache.lookup("libmany.so");
738 assert_eq!(found.len(), CACHE_CANDIDATES_PER_SONAME_MAX);
739 assert_eq!(found[0], Path::new("/lib/libmany-0.so"));
740 assert_eq!(
741 found.last(),
742 Some(&PathBuf::from(format!(
743 "/lib/libmany-{}.so",
744 CACHE_CANDIDATES_PER_SONAME_MAX - 1
745 )))
746 );
747 }
748
749 #[test]
750 fn compatible_lookup_rejects_foreign_abi_and_cpu_specific_entries() {
751 let mut bytes = new_format(&[("libpick.so", "/lib/libpick.so")]);
752 bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0a03u32.to_le_bytes()); let cache = LdCache::parse(&bytes);
755 let x86 = Architecture {
756 machine: Machine::X86_64,
757 class: ElfClass::Elf64,
758 endianness: Endianness::Little,
759 };
760 assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
761
762 bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0303u32.to_le_bytes());
763 bytes[NEW_HEADER_LEN + 16..NEW_HEADER_LEN + 24].copy_from_slice(&1u64.to_le_bytes());
764 let cache = LdCache::parse(&bytes);
765 assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
766 }
767
768 #[test]
769 fn garbage_degrades_to_an_empty_cache() {
770 assert!(LdCache::parse(b"not a cache at all").is_empty());
771 assert!(LdCache::parse(&[]).is_empty());
772 }
773
774 #[test]
775 fn an_absurd_entry_count_allocates_nothing() {
776 assert_eq!(
777 entry_capacity(&[0u8; 48], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
778 0
779 );
780 assert_eq!(
781 entry_capacity(&[0u8; 48 + 24], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
782 1
783 );
784 assert_eq!(entry_capacity(&[], 4096, NEW_HEADER_LEN, NEW_ENTRY_LEN), 0);
785
786 let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
787 bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
788 let cache = LdCache::parse(&bytes);
789 assert_eq!(cache.lookup("libc.so.6"), [PathBuf::from("/lib/libc.so.6")]);
790 assert_eq!(
791 cache.entry_count(),
792 1,
793 "parsing stops at the end of the file"
794 );
795 }
796
797 #[test]
798 fn truncated_entries_do_not_panic() {
799 let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
800 bytes.truncate(NEW_HEADER_LEN + 4);
801 assert!(LdCache::parse(&bytes).is_empty());
802 }
803}