1use crate::{
8 elf::ElfMetadata,
9 error::{Error, Result, io},
10};
11use std::{
12 collections::HashMap,
13 path::{Component, Path, PathBuf},
14};
15
16const SYMLINK_HOPS_MAX: usize = 40;
21
22const PENDING_COMPONENTS_MAX: usize = 1024;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct SymlinkEntry {
31 pub logical: PathBuf,
33 pub target: PathBuf,
35}
36
37#[derive(Debug, Clone)]
39pub struct Resolved {
40 pub logical: PathBuf,
42 pub host: PathBuf,
44 pub links: Vec<SymlinkEntry>,
46 pub kind: EntryKind,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum EntryKind {
51 File,
52 Directory,
53 Other,
54}
55
56#[derive(Debug, Clone)]
57pub struct SourceRoot {
58 path: PathBuf,
59}
60
61impl SourceRoot {
62 pub fn new(path: impl Into<PathBuf>) -> SourceRoot {
63 SourceRoot { path: path.into() }
64 }
65
66 pub fn path(&self) -> &Path {
67 &self.path
68 }
69
70 pub fn host_path(&self, logical: &Path) -> PathBuf {
72 crate::paths::join_under(&self.path, logical)
73 }
74
75 pub fn resolve(&self, logical: &Path) -> Result<Option<Resolved>> {
84 self.walk(logical, Absence::NotFoundOnly)
85 }
86
87 pub fn probe(&self, logical: &Path) -> Result<Option<Resolved>> {
97 self.walk(logical, Absence::AnyFailureToStat)
98 }
99
100 fn walk(&self, logical: &Path, absence: Absence) -> Result<Option<Resolved>> {
101 let mut pending = components_reversed(logical);
106 let mut current = PathBuf::from("/");
107 let mut links: Vec<SymlinkEntry> = Vec::new();
108 let mut hops = 0usize;
109
110 while let Some(component) = pending.pop() {
111 if component == ".." {
112 current.pop();
113 continue;
114 }
115 if component == "." {
116 continue;
117 }
118
119 let next_logical = current.join(&component);
120 let host = self.host_path(&next_logical);
121 let Some(metadata) = symlink_metadata_optional(&host, absence)? else {
122 return Ok(None);
123 };
124 if !metadata.is_symlink() {
125 current = next_logical;
126 continue;
127 }
128
129 if hops == SYMLINK_HOPS_MAX || pending.len() > PENDING_COMPONENTS_MAX {
132 return Err(Error::SymlinkLoop {
133 path: logical.to_path_buf(),
134 });
135 }
136 hops += 1;
137
138 let target = std::fs::read_link(&host).map_err(|e| io(&host, e))?;
139 links.push(SymlinkEntry {
140 logical: next_logical,
141 target: target.clone(),
142 });
143 if target.is_absolute() {
144 current = PathBuf::from("/");
145 }
146 pending.extend(components_reversed(&target));
147 }
148
149 self.describe(current, links, absence)
150 }
151
152 fn describe(
154 &self,
155 logical: PathBuf,
156 links: Vec<SymlinkEntry>,
157 absence: Absence,
158 ) -> Result<Option<Resolved>> {
159 assert!(logical.is_absolute());
160
161 let host = self.host_path(&logical);
162 let Some(metadata) = metadata_optional(&host, absence)? else {
163 return Ok(None);
164 };
165 let kind = if metadata.is_dir() {
166 EntryKind::Directory
167 } else if metadata.is_file() {
168 EntryKind::File
169 } else {
170 EntryKind::Other
171 };
172 Ok(Some(Resolved {
173 logical,
174 host,
175 links,
176 kind,
177 }))
178 }
179
180 pub fn read(&self, logical: &Path) -> Result<Option<Vec<u8>>> {
182 self.read_bounded(logical, usize::MAX)
183 }
184
185 pub fn read_bounded(&self, logical: &Path, limit_bytes: usize) -> Result<Option<Vec<u8>>> {
193 use std::io::Read;
194
195 let Some(resolved) = self.resolve(logical)? else {
196 return Ok(None);
197 };
198 if resolved.kind != EntryKind::File {
199 return Ok(None);
200 }
201 let file = std::fs::File::open(&resolved.host).map_err(|e| io(&resolved.host, e))?;
202 let mut bytes = Vec::new();
203 file.take(limit_bytes as u64)
204 .read_to_end(&mut bytes)
205 .map_err(|e| io(&resolved.host, e))?;
206 Ok(Some(bytes))
207 }
208
209 pub fn exists(&self, logical: &Path) -> bool {
210 matches!(self.probe(logical), Ok(Some(_)))
211 }
212
213 pub fn is_dir(&self, logical: &Path) -> bool {
214 matches!(self.probe(logical), Ok(Some(r)) if r.kind == EntryKind::Directory)
215 }
216
217 pub fn read_dir(&self, logical: &Path) -> Result<Vec<std::ffi::OsString>> {
219 let host = match self.resolve(logical)? {
220 Some(resolved) if resolved.kind == EntryKind::Directory => resolved.host,
221 _ => return Ok(Vec::new()),
222 };
223 let mut names = Vec::new();
224 for entry in std::fs::read_dir(&host).map_err(|e| io(&host, e))? {
225 let entry = entry.map_err(|e| io(&host, e))?;
226 names.push(entry.file_name());
227 }
228 names.sort();
231 Ok(names)
232 }
233}
234
235fn components_reversed(path: &Path) -> Vec<std::ffi::OsString> {
238 path.components()
239 .filter_map(|c| match c {
240 Component::Normal(part) => Some(part.to_os_string()),
241 Component::ParentDir => Some(std::ffi::OsString::from("..")),
242 Component::RootDir | Component::CurDir | Component::Prefix(_) => None,
243 })
244 .rev()
245 .collect()
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250enum Absence {
251 NotFoundOnly,
254 AnyFailureToStat,
257}
258
259impl Absence {
260 fn covers(self, error: &std::io::Error) -> bool {
261 use std::io::ErrorKind;
262
263 match self {
264 Absence::NotFoundOnly => error.kind() == ErrorKind::NotFound,
265 Absence::AnyFailureToStat => matches!(
266 error.kind(),
267 ErrorKind::NotFound
268 | ErrorKind::NotADirectory
269 | ErrorKind::PermissionDenied
270 | ErrorKind::InvalidFilename
271 ),
272 }
273 }
274}
275
276fn symlink_metadata_optional(host: &Path, absence: Absence) -> Result<Option<std::fs::Metadata>> {
279 match std::fs::symlink_metadata(host) {
280 Ok(metadata) => Ok(Some(metadata)),
281 Err(e) if absence.covers(&e) => Ok(None),
282 Err(e) => Err(io(host, e)),
283 }
284}
285
286fn metadata_optional(host: &Path, absence: Absence) -> Result<Option<std::fs::Metadata>> {
288 match std::fs::metadata(host) {
289 Ok(metadata) => Ok(Some(metadata)),
290 Err(e) if absence.covers(&e) => Ok(None),
291 Err(e) => Err(io(host, e)),
292 }
293}
294
295#[derive(Debug, Default)]
297pub struct ElfCache {
298 entries: HashMap<PathBuf, Option<ElfMetadata>>,
299}
300
301impl ElfCache {
302 pub fn new() -> ElfCache {
303 ElfCache::default()
304 }
305
306 pub fn get(&mut self, host: &Path) -> Result<Option<ElfMetadata>> {
309 if let Some(cached) = self.entries.get(host) {
310 return Ok(cached.clone());
311 }
312 let parsed = match ElfMetadata::parse_file(host) {
313 Ok(metadata) => Some(metadata),
314 Err(Error::NotElf { .. }) | Err(Error::Elf { .. }) => None,
315 Err(e) => return Err(e),
316 };
317 self.entries.insert(host.to_path_buf(), parsed.clone());
318 Ok(parsed)
319 }
320
321 pub fn require(&mut self, host: &Path) -> Result<ElfMetadata> {
323 match self.get(host)? {
324 Some(metadata) => Ok(metadata),
325 None => ElfMetadata::parse_file(host),
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn sysroot() -> (tempfile::TempDir, SourceRoot) {
335 let temp = tempfile::tempdir().expect("tempdir");
336 let root = SourceRoot::new(temp.path());
337 (temp, root)
338 }
339
340 #[test]
344 fn parent_components_are_applied_after_symlinks() {
345 let (temp, root) = sysroot();
346 std::fs::create_dir_all(temp.path().join("real/sub")).unwrap();
347 std::fs::create_dir_all(temp.path().join("real/lib")).unwrap();
348 std::fs::create_dir_all(temp.path().join("lib")).unwrap();
349 std::fs::write(temp.path().join("real/lib/libbase.so.1"), b"right").unwrap();
350 std::fs::write(temp.path().join("lib/libbase.so.1"), b"wrong").unwrap();
351 std::os::unix::fs::symlink("real/sub", temp.path().join("link")).unwrap();
352
353 let resolved = root
354 .resolve(Path::new("/link/../lib/libbase.so.1"))
355 .unwrap()
356 .expect("resolves through the symlink");
357 assert_eq!(resolved.logical, Path::new("/real/lib/libbase.so.1"));
358 assert_eq!(std::fs::read(&resolved.host).unwrap(), b"right");
359 }
360
361 #[test]
364 fn a_non_directory_component_is_absent_rather_than_an_error() {
365 let (temp, root) = sysroot();
366 std::fs::write(temp.path().join("notadir"), b"file").unwrap();
367
368 assert!(
369 root.probe(Path::new("/notadir/libbase.so.1"))
370 .unwrap()
371 .is_none()
372 );
373 assert!(!root.exists(Path::new("/notadir/libbase.so.1")));
374 let error = root
376 .resolve(Path::new("/notadir/libbase.so.1"))
377 .expect_err("a named path reports why it could not be read");
378 assert_eq!(error.code(), "E1000");
379 }
380
381 #[test]
382 fn a_symlink_chain_longer_than_the_loader_allows_is_an_error() {
383 let (temp, root) = sysroot();
384 for hop in 0..=SYMLINK_HOPS_MAX {
385 std::os::unix::fs::symlink(
386 format!("link{}", hop + 1),
387 temp.path().join(format!("link{hop}")),
388 )
389 .unwrap();
390 }
391
392 let error = root.resolve(Path::new("/link0")).unwrap_err();
393 assert_eq!(error.code(), "E3003");
394 }
395}