1use std::path::Path;
9
10pub fn host_driver_paths(arch: &str) -> Vec<String> {
21 let mut dirs: Vec<&str> = vec!["/run/opengl-driver/lib", "/run/opengl-driver-32/lib"];
25 dirs.extend(multiarch_dirs(arch));
26 dirs.extend(["/usr/lib64", "/usr/lib", "/lib64", "/lib"]);
27
28 let mut seen: Vec<String> = Vec::new();
29 for d in dirs {
30 if Path::new(d).is_dir() && !seen.iter().any(|s| s == d) {
31 seen.push(d.to_string());
32 }
33 }
34 for d in cache_dirs() {
37 if !seen.contains(&d) {
38 seen.push(d);
39 }
40 }
41 seen
42}
43
44fn cache_dirs() -> Vec<String> {
60 let Ok(data) = std::fs::read("/etc/ld.so.cache") else {
61 return Vec::new();
62 };
63
64 let mut dirs: Vec<String> = Vec::new();
65 for entry in cache_entries(&data) {
66 let Some((dir, _)) = entry.rsplit_once('/') else {
67 continue;
68 };
69 if !dir.is_empty() && !dirs.iter().any(|d| d == dir) {
70 dirs.push(dir.to_string());
71 }
72 }
73 dirs.retain(|d| Path::new(d).is_dir());
77 dirs
78}
79
80const CACHE_MAGIC_OLD: &[u8] = b"ld.so-1.7.0";
81const CACHE_MAGIC_NEW: &[u8] = b"glibc-ld.so.cache1.1";
82
83fn cache_entries(data: &[u8]) -> Vec<&str> {
91 if data.starts_with(CACHE_MAGIC_NEW) {
92 return new_entries(data, 0);
93 }
94 if data.starts_with(CACHE_MAGIC_OLD) {
95 let Some(nlibs) = read_u32(data, 12) else {
98 return Vec::new();
99 };
100 let Some(entries_len) = (nlibs as usize).checked_mul(12) else {
101 return Vec::new();
102 };
103 let Some(end) = entries_len.checked_add(16) else {
104 return Vec::new();
105 };
106 let aligned = end.next_multiple_of(8);
108 if data.len() > aligned && data[aligned..].starts_with(CACHE_MAGIC_NEW) {
109 return new_entries(data, aligned);
110 }
111 return old_entries(data, nlibs as usize, end);
112 }
113 Vec::new()
114}
115
116fn new_entries(data: &[u8], base: usize) -> Vec<&str> {
118 let Some(nlibs) = read_u32(data, base + 20) else {
122 return Vec::new();
123 };
124 let mut out = Vec::new();
125 for i in 0..nlibs as usize {
126 let Some(entry) = base.checked_add(48).and_then(|s| s.checked_add(i * 24)) else {
127 break;
128 };
129 let Some(value) = read_u32(data, entry + 8) else {
130 break;
131 };
132 if let Some(s) = read_str(data, base + value as usize) {
133 out.push(s);
134 }
135 }
136 out
137}
138
139fn old_entries(data: &[u8], nlibs: usize, strings: usize) -> Vec<&str> {
142 let mut out = Vec::new();
143 for i in 0..nlibs {
144 let entry = 16 + i * 12;
145 let Some(value) = read_u32(data, entry + 8) else {
146 break;
147 };
148 if let Some(s) = read_str(data, strings + value as usize) {
149 out.push(s);
150 }
151 }
152 out
153}
154
155fn read_u32(data: &[u8], at: usize) -> Option<u32> {
156 let bytes = data.get(at..at.checked_add(4)?)?;
157 Some(u32::from_le_bytes(bytes.try_into().ok()?))
158}
159
160fn read_str(data: &[u8], at: usize) -> Option<&str> {
164 let rest = data.get(at..)?;
165 let end = rest.iter().position(|&b| b == 0)?;
166 let s = std::str::from_utf8(&rest[..end]).ok()?;
167 s.starts_with('/').then_some(s)
168}
169
170fn multiarch_dirs(arch: &str) -> &'static [&'static str] {
173 match arch {
174 "x86_64" => &["/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu"],
175 "aarch64" => &["/usr/lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu"],
176 "x86" => &["/usr/lib/i386-linux-gnu", "/lib/i386-linux-gnu"],
177 "arm" => &["/usr/lib/arm-linux-gnueabihf", "/lib/arm-linux-gnueabihf"],
178 _ => &[],
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn multiarch_follows_the_architecture() {
188 assert_eq!(multiarch_dirs("x86_64")[0], "/usr/lib/x86_64-linux-gnu");
189 assert_eq!(multiarch_dirs("aarch64")[0], "/usr/lib/aarch64-linux-gnu");
190 assert_eq!(multiarch_dirs("x86")[0], "/usr/lib/i386-linux-gnu");
191 assert!(multiarch_dirs("riscv64").is_empty());
192 }
193
194 #[test]
195 fn results_exist_and_are_unique() {
196 let dirs = host_driver_paths(std::env::consts::ARCH);
197 for d in &dirs {
198 assert!(std::path::Path::new(d).is_dir(), "{d} must exist");
199 }
200 let mut sorted = dirs.clone();
201 sorted.sort();
202 sorted.dedup();
203 assert_eq!(sorted.len(), dirs.len(), "no directory is listed twice");
204 }
205
206 fn new_cache(paths: &[&str]) -> Vec<u8> {
208 let mut out = Vec::from(CACHE_MAGIC_NEW);
209 out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
210 out.extend_from_slice(&0u32.to_le_bytes()); out.push(0); out.extend_from_slice(&[0; 3]); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&[0; 12]); assert_eq!(out.len(), 48, "entries must start at 48");
216
217 let base = out.len();
218 let strings_at = base + paths.len() * 24;
219 let (offsets, strings) = string_table(paths, strings_at);
220 for off in offsets {
221 out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(off as u32).to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u64.to_le_bytes()); }
227 out.extend_from_slice(&strings);
228 out
229 }
230
231 fn old_cache(paths: &[&str], also: Option<&[&str]>) -> Vec<u8> {
238 let mut out = Vec::from(CACHE_MAGIC_OLD);
239 out.push(0); out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
241 assert_eq!(out.len(), 16, "entries must start at 16");
242
243 let entries_end = 16 + paths.len() * 12;
244 let Some(also) = also else {
245 let (offsets, strings) = string_table(paths, 0);
246 for off in offsets {
247 out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(off as u32).to_le_bytes()); }
251 assert_eq!(out.len(), entries_end);
252 out.extend_from_slice(&strings);
253 return out;
254 };
255
256 let new_base = entries_end.next_multiple_of(8);
257 let strings_at = new_base + 48 + also.len() * 24;
258 let all: Vec<&str> = paths.iter().chain(also.iter()).copied().collect();
261 let (offsets, strings) = string_table(&all, strings_at - new_base);
262
263 for off in &offsets[..paths.len()] {
264 out.extend_from_slice(&0i32.to_le_bytes());
265 out.extend_from_slice(&0u32.to_le_bytes());
266 out.extend_from_slice(&(*off as u32).to_le_bytes());
267 }
268 out.resize(new_base, 0);
269
270 out.extend_from_slice(CACHE_MAGIC_NEW);
271 out.extend_from_slice(&(also.len() as u32).to_le_bytes());
272 out.extend_from_slice(&0u32.to_le_bytes()); out.push(0); out.extend_from_slice(&[0; 3]); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&[0; 12]); for off in &offsets[paths.len()..] {
278 out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(*off as u32).to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u64.to_le_bytes()); }
284 assert_eq!(out.len(), strings_at);
285 out.extend_from_slice(&strings);
286 out
287 }
288
289 fn string_table(paths: &[&str], base: usize) -> (Vec<usize>, Vec<u8>) {
290 let mut offsets = Vec::new();
291 let mut strings: Vec<u8> = Vec::new();
292 for p in paths {
293 offsets.push(base + strings.len());
294 strings.extend_from_slice(p.as_bytes());
295 strings.push(0);
296 }
297 (offsets, strings)
298 }
299
300 #[test]
301 fn reads_a_new_format_cache() {
302 let img = new_cache(&[
303 "/usr/lib64/libc.so.6",
304 "/usr/lib/llvm/22/lib64/libLLVM.so.22.1",
305 ]);
306 assert_eq!(
307 cache_entries(&img),
308 [
309 "/usr/lib64/libc.so.6",
310 "/usr/lib/llvm/22/lib64/libLLVM.so.22.1"
311 ]
312 );
313 }
314
315 #[test]
316 fn prefers_the_new_cache_appended_after_an_old_one() {
317 let img = old_cache(&["/lib/old.so.1"], Some(&["/usr/lib64/new.so.2"]));
320 assert_eq!(cache_entries(&img), ["/usr/lib64/new.so.2"]);
321 }
322
323 #[test]
324 fn reads_an_old_format_cache_with_nothing_appended() {
325 let img = old_cache(&["/lib/libz.so.1", "/usr/lib/libm.so.6"], None);
326 assert_eq!(
327 cache_entries(&img),
328 ["/lib/libz.so.1", "/usr/lib/libm.so.6"]
329 );
330 }
331
332 #[test]
333 fn a_damaged_cache_yields_nothing_rather_than_panicking() {
334 assert!(cache_entries(b"").is_empty());
335 assert!(cache_entries(b"not a cache at all").is_empty());
336 let img = new_cache(&["/usr/lib64/libc.so.6"]);
339 for cut in 0..img.len() {
340 let _ = cache_entries(&img[..cut]);
341 }
342 let mut lying = new_cache(&["/usr/lib64/libc.so.6"]);
343 lying[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
344 assert!(cache_entries(&lying).len() < 2);
345 }
346
347 #[test]
348 fn relative_and_unterminated_entries_are_skipped() {
349 let img = new_cache(&["not/absolute", "/usr/lib64/fine.so"]);
352 assert_eq!(cache_entries(&img), ["/usr/lib64/fine.so"]);
353
354 let mut unterminated = new_cache(&["/usr/lib64/fine.so"]);
355 let last = unterminated.len() - 1;
356 unterminated[last] = b'x'; assert!(cache_entries(&unterminated).is_empty());
358 }
359
360 #[test]
361 fn cache_directories_come_after_the_well_known_ones() {
362 let dirs = host_driver_paths(std::env::consts::ARCH);
365 let from_cache = cache_dirs();
366 let Some(first_cache_only) = dirs
367 .iter()
368 .position(|d| from_cache.contains(d) && !well_known(d))
369 else {
370 return; };
372 let last_well_known = dirs.iter().rposition(|d| well_known(d)).unwrap_or(0);
373 assert!(
374 last_well_known < first_cache_only,
375 "cache directory ordered before a well-known one in {dirs:?}"
376 );
377 }
378
379 fn well_known(dir: &str) -> bool {
380 matches!(
381 dir,
382 "/run/opengl-driver/lib"
383 | "/run/opengl-driver-32/lib"
384 | "/usr/lib64"
385 | "/usr/lib"
386 | "/lib64"
387 | "/lib"
388 ) || multiarch_dirs(std::env::consts::ARCH).contains(&dir)
389 }
390
391 #[test]
392 fn driver_paths_outrank_distribution_paths() {
393 let all = ["/run/opengl-driver/lib", "/usr/lib"];
396 let present: Vec<&str> = all
397 .into_iter()
398 .filter(|d| std::path::Path::new(d).is_dir())
399 .collect();
400 if present.len() == 2 {
401 let dirs = host_driver_paths(std::env::consts::ARCH);
402 let driver = dirs.iter().position(|d| d == "/run/opengl-driver/lib");
403 let usrlib = dirs.iter().position(|d| d == "/usr/lib");
404 assert!(driver < usrlib);
405 }
406 }
407}