use std::path::Path;
pub fn host_driver_paths(arch: &str) -> Vec<String> {
let mut dirs: Vec<&str> = vec!["/run/opengl-driver/lib", "/run/opengl-driver-32/lib"];
dirs.extend(multiarch_dirs(arch));
dirs.extend(["/usr/lib64", "/usr/lib", "/lib64", "/lib"]);
let mut seen: Vec<String> = Vec::new();
for d in dirs {
if Path::new(d).is_dir() && !seen.iter().any(|s| s == d) {
seen.push(d.to_string());
}
}
for d in cache_dirs() {
if !seen.contains(&d) {
seen.push(d);
}
}
seen
}
fn cache_dirs() -> Vec<String> {
let Ok(data) = std::fs::read("/etc/ld.so.cache") else {
return Vec::new();
};
let mut dirs: Vec<String> = Vec::new();
for entry in cache_entries(&data) {
let Some((dir, _)) = entry.rsplit_once('/') else {
continue;
};
if !dir.is_empty() && !dirs.iter().any(|d| d == dir) {
dirs.push(dir.to_string());
}
}
dirs.retain(|d| Path::new(d).is_dir());
dirs
}
const CACHE_MAGIC_OLD: &[u8] = b"ld.so-1.7.0";
const CACHE_MAGIC_NEW: &[u8] = b"glibc-ld.so.cache1.1";
fn cache_entries(data: &[u8]) -> Vec<&str> {
if data.starts_with(CACHE_MAGIC_NEW) {
return new_entries(data, 0);
}
if data.starts_with(CACHE_MAGIC_OLD) {
let Some(nlibs) = read_u32(data, 12) else {
return Vec::new();
};
let Some(entries_len) = (nlibs as usize).checked_mul(12) else {
return Vec::new();
};
let Some(end) = entries_len.checked_add(16) else {
return Vec::new();
};
let aligned = end.next_multiple_of(8);
if data.len() > aligned && data[aligned..].starts_with(CACHE_MAGIC_NEW) {
return new_entries(data, aligned);
}
return old_entries(data, nlibs as usize, end);
}
Vec::new()
}
fn new_entries(data: &[u8], base: usize) -> Vec<&str> {
let Some(nlibs) = read_u32(data, base + 20) else {
return Vec::new();
};
let mut out = Vec::new();
for i in 0..nlibs as usize {
let Some(entry) = base.checked_add(48).and_then(|s| s.checked_add(i * 24)) else {
break;
};
let Some(value) = read_u32(data, entry + 8) else {
break;
};
if let Some(s) = read_str(data, base + value as usize) {
out.push(s);
}
}
out
}
fn old_entries(data: &[u8], nlibs: usize, strings: usize) -> Vec<&str> {
let mut out = Vec::new();
for i in 0..nlibs {
let entry = 16 + i * 12;
let Some(value) = read_u32(data, entry + 8) else {
break;
};
if let Some(s) = read_str(data, strings + value as usize) {
out.push(s);
}
}
out
}
fn read_u32(data: &[u8], at: usize) -> Option<u32> {
let bytes = data.get(at..at.checked_add(4)?)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
fn read_str(data: &[u8], at: usize) -> Option<&str> {
let rest = data.get(at..)?;
let end = rest.iter().position(|&b| b == 0)?;
let s = std::str::from_utf8(&rest[..end]).ok()?;
s.starts_with('/').then_some(s)
}
fn multiarch_dirs(arch: &str) -> &'static [&'static str] {
match arch {
"x86_64" => &["/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu"],
"aarch64" => &["/usr/lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu"],
"x86" => &["/usr/lib/i386-linux-gnu", "/lib/i386-linux-gnu"],
"arm" => &["/usr/lib/arm-linux-gnueabihf", "/lib/arm-linux-gnueabihf"],
_ => &[],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn multiarch_follows_the_architecture() {
assert_eq!(multiarch_dirs("x86_64")[0], "/usr/lib/x86_64-linux-gnu");
assert_eq!(multiarch_dirs("aarch64")[0], "/usr/lib/aarch64-linux-gnu");
assert_eq!(multiarch_dirs("x86")[0], "/usr/lib/i386-linux-gnu");
assert!(multiarch_dirs("riscv64").is_empty());
}
#[test]
fn results_exist_and_are_unique() {
let dirs = host_driver_paths(std::env::consts::ARCH);
for d in &dirs {
assert!(std::path::Path::new(d).is_dir(), "{d} must exist");
}
let mut sorted = dirs.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), dirs.len(), "no directory is listed twice");
}
fn new_cache(paths: &[&str]) -> Vec<u8> {
let mut out = Vec::from(CACHE_MAGIC_NEW);
out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
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");
let base = out.len();
let strings_at = base + paths.len() * 24;
let (offsets, strings) = string_table(paths, strings_at);
for off in offsets {
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()); }
out.extend_from_slice(&strings);
out
}
fn old_cache(paths: &[&str], also: Option<&[&str]>) -> Vec<u8> {
let mut out = Vec::from(CACHE_MAGIC_OLD);
out.push(0); out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
assert_eq!(out.len(), 16, "entries must start at 16");
let entries_end = 16 + paths.len() * 12;
let Some(also) = also else {
let (offsets, strings) = string_table(paths, 0);
for off in offsets {
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()); }
assert_eq!(out.len(), entries_end);
out.extend_from_slice(&strings);
return out;
};
let new_base = entries_end.next_multiple_of(8);
let strings_at = new_base + 48 + also.len() * 24;
let all: Vec<&str> = paths.iter().chain(also.iter()).copied().collect();
let (offsets, strings) = string_table(&all, strings_at - new_base);
for off in &offsets[..paths.len()] {
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.resize(new_base, 0);
out.extend_from_slice(CACHE_MAGIC_NEW);
out.extend_from_slice(&(also.len() as u32).to_le_bytes());
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()..] {
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()); }
assert_eq!(out.len(), strings_at);
out.extend_from_slice(&strings);
out
}
fn string_table(paths: &[&str], base: usize) -> (Vec<usize>, Vec<u8>) {
let mut offsets = Vec::new();
let mut strings: Vec<u8> = Vec::new();
for p in paths {
offsets.push(base + strings.len());
strings.extend_from_slice(p.as_bytes());
strings.push(0);
}
(offsets, strings)
}
#[test]
fn reads_a_new_format_cache() {
let img = new_cache(&[
"/usr/lib64/libc.so.6",
"/usr/lib/llvm/22/lib64/libLLVM.so.22.1",
]);
assert_eq!(
cache_entries(&img),
[
"/usr/lib64/libc.so.6",
"/usr/lib/llvm/22/lib64/libLLVM.so.22.1"
]
);
}
#[test]
fn prefers_the_new_cache_appended_after_an_old_one() {
let img = old_cache(&["/lib/old.so.1"], Some(&["/usr/lib64/new.so.2"]));
assert_eq!(cache_entries(&img), ["/usr/lib64/new.so.2"]);
}
#[test]
fn reads_an_old_format_cache_with_nothing_appended() {
let img = old_cache(&["/lib/libz.so.1", "/usr/lib/libm.so.6"], None);
assert_eq!(
cache_entries(&img),
["/lib/libz.so.1", "/usr/lib/libm.so.6"]
);
}
#[test]
fn a_damaged_cache_yields_nothing_rather_than_panicking() {
assert!(cache_entries(b"").is_empty());
assert!(cache_entries(b"not a cache at all").is_empty());
let img = new_cache(&["/usr/lib64/libc.so.6"]);
for cut in 0..img.len() {
let _ = cache_entries(&img[..cut]);
}
let mut lying = new_cache(&["/usr/lib64/libc.so.6"]);
lying[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(cache_entries(&lying).len() < 2);
}
#[test]
fn relative_and_unterminated_entries_are_skipped() {
let img = new_cache(&["not/absolute", "/usr/lib64/fine.so"]);
assert_eq!(cache_entries(&img), ["/usr/lib64/fine.so"]);
let mut unterminated = new_cache(&["/usr/lib64/fine.so"]);
let last = unterminated.len() - 1;
unterminated[last] = b'x'; assert!(cache_entries(&unterminated).is_empty());
}
#[test]
fn cache_directories_come_after_the_well_known_ones() {
let dirs = host_driver_paths(std::env::consts::ARCH);
let from_cache = cache_dirs();
let Some(first_cache_only) = dirs
.iter()
.position(|d| from_cache.contains(d) && !well_known(d))
else {
return; };
let last_well_known = dirs.iter().rposition(|d| well_known(d)).unwrap_or(0);
assert!(
last_well_known < first_cache_only,
"cache directory ordered before a well-known one in {dirs:?}"
);
}
fn well_known(dir: &str) -> bool {
matches!(
dir,
"/run/opengl-driver/lib"
| "/run/opengl-driver-32/lib"
| "/usr/lib64"
| "/usr/lib"
| "/lib64"
| "/lib"
) || multiarch_dirs(std::env::consts::ARCH).contains(&dir)
}
#[test]
fn driver_paths_outrank_distribution_paths() {
let all = ["/run/opengl-driver/lib", "/usr/lib"];
let present: Vec<&str> = all
.into_iter()
.filter(|d| std::path::Path::new(d).is_dir())
.collect();
if present.len() == 2 {
let dirs = host_driver_paths(std::env::consts::ARCH);
let driver = dirs.iter().position(|d| d == "/run/opengl-driver/lib");
let usrlib = dirs.iter().position(|d| d == "/usr/lib");
assert!(driver < usrlib);
}
}
}