use crate::{
elf::ElfMetadata,
error::{Error, Result, io},
};
use std::{
collections::HashMap,
path::{Component, Path, PathBuf},
};
const SYMLINK_HOPS_MAX: usize = 40;
const PENDING_COMPONENTS_MAX: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymlinkEntry {
pub logical: PathBuf,
pub target: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub logical: PathBuf,
pub host: PathBuf,
pub links: Vec<SymlinkEntry>,
pub kind: EntryKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
File,
Directory,
Other,
}
#[derive(Debug, Clone)]
pub struct SourceRoot {
path: PathBuf,
}
impl SourceRoot {
pub fn new(path: impl Into<PathBuf>) -> SourceRoot {
SourceRoot { path: path.into() }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn host_path(&self, logical: &Path) -> PathBuf {
crate::paths::join_under(&self.path, logical)
}
pub fn resolve(&self, logical: &Path) -> Result<Option<Resolved>> {
self.walk(logical, Absence::NotFoundOnly)
}
pub fn probe(&self, logical: &Path) -> Result<Option<Resolved>> {
self.walk(logical, Absence::AnyFailureToStat)
}
fn walk(&self, logical: &Path, absence: Absence) -> Result<Option<Resolved>> {
let mut pending = components_reversed(logical);
let mut current = PathBuf::from("/");
let mut links: Vec<SymlinkEntry> = Vec::new();
let mut hops = 0usize;
while let Some(component) = pending.pop() {
if component == ".." {
current.pop();
continue;
}
if component == "." {
continue;
}
let next_logical = current.join(&component);
let host = self.host_path(&next_logical);
let Some(metadata) = symlink_metadata_optional(&host, absence)? else {
return Ok(None);
};
if !metadata.is_symlink() {
current = next_logical;
continue;
}
if hops == SYMLINK_HOPS_MAX || pending.len() > PENDING_COMPONENTS_MAX {
return Err(Error::SymlinkLoop {
path: logical.to_path_buf(),
});
}
hops += 1;
let target = std::fs::read_link(&host).map_err(|e| io(&host, e))?;
links.push(SymlinkEntry {
logical: next_logical,
target: target.clone(),
});
if target.is_absolute() {
current = PathBuf::from("/");
}
pending.extend(components_reversed(&target));
}
self.describe(current, links, absence)
}
fn describe(
&self,
logical: PathBuf,
links: Vec<SymlinkEntry>,
absence: Absence,
) -> Result<Option<Resolved>> {
assert!(logical.is_absolute());
let host = self.host_path(&logical);
let Some(metadata) = metadata_optional(&host, absence)? else {
return Ok(None);
};
let kind = if metadata.is_dir() {
EntryKind::Directory
} else if metadata.is_file() {
EntryKind::File
} else {
EntryKind::Other
};
Ok(Some(Resolved {
logical,
host,
links,
kind,
}))
}
pub fn read(&self, logical: &Path) -> Result<Option<Vec<u8>>> {
self.read_bounded(logical, usize::MAX)
}
pub fn read_bounded(&self, logical: &Path, limit_bytes: usize) -> Result<Option<Vec<u8>>> {
use std::io::Read;
let Some(resolved) = self.resolve(logical)? else {
return Ok(None);
};
if resolved.kind != EntryKind::File {
return Ok(None);
}
let file = std::fs::File::open(&resolved.host).map_err(|e| io(&resolved.host, e))?;
let mut bytes = Vec::new();
file.take(limit_bytes as u64)
.read_to_end(&mut bytes)
.map_err(|e| io(&resolved.host, e))?;
Ok(Some(bytes))
}
pub fn exists(&self, logical: &Path) -> bool {
matches!(self.probe(logical), Ok(Some(_)))
}
pub fn is_dir(&self, logical: &Path) -> bool {
matches!(self.probe(logical), Ok(Some(r)) if r.kind == EntryKind::Directory)
}
pub fn read_dir(&self, logical: &Path) -> Result<Vec<std::ffi::OsString>> {
let host = match self.resolve(logical)? {
Some(resolved) if resolved.kind == EntryKind::Directory => resolved.host,
_ => return Ok(Vec::new()),
};
let mut names = Vec::new();
for entry in std::fs::read_dir(&host).map_err(|e| io(&host, e))? {
let entry = entry.map_err(|e| io(&host, e))?;
names.push(entry.file_name());
}
names.sort();
Ok(names)
}
}
fn components_reversed(path: &Path) -> Vec<std::ffi::OsString> {
path.components()
.filter_map(|c| match c {
Component::Normal(part) => Some(part.to_os_string()),
Component::ParentDir => Some(std::ffi::OsString::from("..")),
Component::RootDir | Component::CurDir | Component::Prefix(_) => None,
})
.rev()
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Absence {
NotFoundOnly,
AnyFailureToStat,
}
impl Absence {
fn covers(self, error: &std::io::Error) -> bool {
use std::io::ErrorKind;
match self {
Absence::NotFoundOnly => error.kind() == ErrorKind::NotFound,
Absence::AnyFailureToStat => matches!(
error.kind(),
ErrorKind::NotFound
| ErrorKind::NotADirectory
| ErrorKind::PermissionDenied
| ErrorKind::InvalidFilename
),
}
}
}
fn symlink_metadata_optional(host: &Path, absence: Absence) -> Result<Option<std::fs::Metadata>> {
match std::fs::symlink_metadata(host) {
Ok(metadata) => Ok(Some(metadata)),
Err(e) if absence.covers(&e) => Ok(None),
Err(e) => Err(io(host, e)),
}
}
fn metadata_optional(host: &Path, absence: Absence) -> Result<Option<std::fs::Metadata>> {
match std::fs::metadata(host) {
Ok(metadata) => Ok(Some(metadata)),
Err(e) if absence.covers(&e) => Ok(None),
Err(e) => Err(io(host, e)),
}
}
#[derive(Debug, Default)]
pub struct ElfCache {
entries: HashMap<PathBuf, Option<ElfMetadata>>,
}
impl ElfCache {
pub fn new() -> ElfCache {
ElfCache::default()
}
pub fn get(&mut self, host: &Path) -> Result<Option<ElfMetadata>> {
if let Some(cached) = self.entries.get(host) {
return Ok(cached.clone());
}
let parsed = match ElfMetadata::parse_file(host) {
Ok(metadata) => Some(metadata),
Err(Error::NotElf { .. }) | Err(Error::Elf { .. }) => None,
Err(e) => return Err(e),
};
self.entries.insert(host.to_path_buf(), parsed.clone());
Ok(parsed)
}
pub fn require(&mut self, host: &Path) -> Result<ElfMetadata> {
match self.get(host)? {
Some(metadata) => Ok(metadata),
None => ElfMetadata::parse_file(host),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sysroot() -> (tempfile::TempDir, SourceRoot) {
let temp = tempfile::tempdir().expect("tempdir");
let root = SourceRoot::new(temp.path());
(temp, root)
}
#[test]
fn parent_components_are_applied_after_symlinks() {
let (temp, root) = sysroot();
std::fs::create_dir_all(temp.path().join("real/sub")).unwrap();
std::fs::create_dir_all(temp.path().join("real/lib")).unwrap();
std::fs::create_dir_all(temp.path().join("lib")).unwrap();
std::fs::write(temp.path().join("real/lib/libbase.so.1"), b"right").unwrap();
std::fs::write(temp.path().join("lib/libbase.so.1"), b"wrong").unwrap();
std::os::unix::fs::symlink("real/sub", temp.path().join("link")).unwrap();
let resolved = root
.resolve(Path::new("/link/../lib/libbase.so.1"))
.unwrap()
.expect("resolves through the symlink");
assert_eq!(resolved.logical, Path::new("/real/lib/libbase.so.1"));
assert_eq!(std::fs::read(&resolved.host).unwrap(), b"right");
}
#[test]
fn a_non_directory_component_is_absent_rather_than_an_error() {
let (temp, root) = sysroot();
std::fs::write(temp.path().join("notadir"), b"file").unwrap();
assert!(
root.probe(Path::new("/notadir/libbase.so.1"))
.unwrap()
.is_none()
);
assert!(!root.exists(Path::new("/notadir/libbase.so.1")));
let error = root
.resolve(Path::new("/notadir/libbase.so.1"))
.expect_err("a named path reports why it could not be read");
assert_eq!(error.code(), "E1000");
}
#[test]
fn a_symlink_chain_longer_than_the_loader_allows_is_an_error() {
let (temp, root) = sysroot();
for hop in 0..=SYMLINK_HOPS_MAX {
std::os::unix::fs::symlink(
format!("link{}", hop + 1),
temp.path().join(format!("link{hop}")),
)
.unwrap();
}
let error = root.resolve(Path::new("/link0")).unwrap_err();
assert_eq!(error.code(), "E3003");
}
}