use crate::{
elf::ElfMetadata,
error::{Error, Result, io},
paths::normalize_absolute,
};
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>> {
let mut pending = components_reversed(&normalize_absolute(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)? 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)
}
fn describe(&self, logical: PathBuf, links: Vec<SymlinkEntry>) -> Result<Option<Resolved>> {
assert!(logical.is_absolute());
let host = self.host_path(&logical);
let Some(metadata) = metadata_optional(&host)? 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>>> {
match self.resolve(logical)? {
Some(resolved) if resolved.kind == EntryKind::File => Ok(Some(
std::fs::read(&resolved.host).map_err(|e| io(&resolved.host, e))?,
)),
_ => Ok(None),
}
}
pub fn exists(&self, logical: &Path) -> bool {
matches!(self.resolve(logical), Ok(Some(_)))
}
pub fn is_dir(&self, logical: &Path) -> bool {
matches!(self.resolve(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()
}
fn symlink_metadata_optional(host: &Path) -> Result<Option<std::fs::Metadata>> {
match std::fs::symlink_metadata(host) {
Ok(metadata) => Ok(Some(metadata)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(io(host, e)),
}
}
fn metadata_optional(host: &Path) -> Result<Option<std::fs::Metadata>> {
match std::fs::metadata(host) {
Ok(metadata) => Ok(Some(metadata)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => 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),
}
}
}