use std::{
ffi::OsString,
fs, io,
path::{Component, Path, PathBuf},
sync::OnceLock,
};
use crate::{
BoxFuture,
fs::{Dirent, DirentKind, FileSystem, Stat},
};
const MAX_LINK_HOPS: u32 = 32;
pub struct PassthroughFs {
root: PathBuf,
canonical_root: OnceLock<PathBuf>,
}
impl PassthroughFs {
pub fn new(root: impl Into<PathBuf>) -> Self {
PassthroughFs {
root: root.into(),
canonical_root: OnceLock::new(),
}
}
pub fn root(&self) -> &Path {
&self.root
}
fn canonical_root(&self) -> io::Result<&Path> {
if let Some(root) = self.canonical_root.get() {
return Ok(root);
}
let built = self.root.canonicalize()?;
Ok(self.canonical_root.get_or_init(|| built))
}
fn resolve_within_root(&self, real: &Path) -> io::Result<PathBuf> {
let root = self.canonical_root()?;
let mut pending: Vec<OsString> = real
.strip_prefix(&self.root)
.map_err(|_| io::Error::from(io::ErrorKind::NotFound))?
.components()
.rev()
.map(|comp| comp.as_os_str().to_os_string())
.collect();
let mut resolved = root.to_path_buf();
let mut hops = 0u32;
while let Some(name) = pending.pop() {
if name == "." {
continue;
}
if name == ".." {
resolved.pop();
continue;
}
resolved.push(&name);
let Ok(target) = fs::read_link(&resolved) else {
continue; };
hops += 1;
if hops > MAX_LINK_HOPS {
return Err(io::ErrorKind::NotFound.into());
}
resolved.pop();
pending.extend(
target
.components()
.rev()
.map(|comp| comp.as_os_str().to_os_string()),
);
}
if !resolved.starts_with(root) {
return Err(io::ErrorKind::NotFound.into());
}
Ok(resolved)
}
fn fold(&self, path: &Path) -> io::Result<Folded> {
let mut folded = Folded {
real: self.root.clone(),
depth: 0,
through_a_link: false,
};
for comp in path.components() {
match comp {
Component::RootDir | Component::CurDir => {}
Component::Normal(name) => {
folded.real.push(name);
folded.depth += 1;
folded.through_a_link =
folded.through_a_link || fs::read_link(&folded.real).is_ok();
}
Component::ParentDir => {
if folded.depth == 0 {
return Err(io::ErrorKind::InvalidFilename.into());
}
folded.real.pop();
folded.depth -= 1;
}
Component::Prefix(_) => return Err(io::ErrorKind::InvalidFilename.into()),
}
}
Ok(folded)
}
fn real_path(&self, path: &Path) -> io::Result<PathBuf> {
let folded = self.fold(path)?;
if folded.through_a_link && folded.depth > 0 {
self.resolve_within_root(&folded.real)?;
}
Ok(folded.real)
}
fn entry_path(&self, path: &Path) -> io::Result<PathBuf> {
let folded = self.fold(path)?;
if folded.through_a_link && folded.depth > 1 {
let parent = folded.real.parent().expect("depth > 1 leaves a parent");
self.resolve_within_root(parent)?;
}
Ok(folded.real)
}
fn open(&self, path: &Path, writable: bool) -> io::Result<fs::File> {
let real = self.real_path(path)?;
fs::OpenOptions::new()
.read(!writable)
.write(writable)
.open(real)
}
fn stat_of(meta: &fs::Metadata) -> Stat {
let kind = if meta.is_dir() {
DirentKind::Dir
} else {
DirentKind::File
};
let mut stat = Stat::new(kind, meta.len());
stat.mtime = meta.modified().ok();
stat.atime = meta.accessed().ok();
stat.created = meta.created().ok();
stat
}
}
struct Folded {
real: PathBuf,
depth: usize,
through_a_link: bool,
}
impl FileSystem for PassthroughFs {
fn stat<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Stat>> {
Box::pin(async move {
let real = self.real_path(path)?;
Ok(Self::stat_of(&fs::metadata(&real)?))
})
}
fn list<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Vec<Dirent>>> {
Box::pin(async move {
let real = self.real_path(path)?;
if !fs::metadata(&real)?.is_dir() {
return Err(io::ErrorKind::NotADirectory.into());
}
let mut out = Vec::new();
for entry in fs::read_dir(&real)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().into_owned();
let file_type = entry.file_type()?;
let kind = if file_type.is_symlink() {
match self.resolve_within_root(&entry.path()) {
Ok(target) if fs::metadata(&target).is_ok_and(|meta| meta.is_dir()) => {
DirentKind::Dir
}
_ => DirentKind::File,
}
} else if file_type.is_dir() {
DirentKind::Dir
} else {
DirentKind::File
};
out.push(Dirent::new(name, kind));
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
})
}
fn read_at<'a>(
&'a self,
path: &'a Path,
buf: &'a mut [u8],
offset: u64,
) -> BoxFuture<'a, io::Result<usize>> {
Box::pin(async move { pread(&self.open(path, false)?, buf, offset) })
}
fn create<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Stat>> {
Box::pin(async move {
let real = self.entry_path(path)?;
let file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(real)?;
Ok(Self::stat_of(&file.metadata()?))
})
}
fn mkdir<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Stat>> {
Box::pin(async move {
let real = self.entry_path(path)?;
fs::create_dir(&real)?;
Ok(Self::stat_of(&fs::symlink_metadata(&real)?))
})
}
fn unlink<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<()>> {
Box::pin(async move {
let real = self.entry_path(path)?;
if fs::symlink_metadata(&real)?.is_dir() {
return Err(io::ErrorKind::IsADirectory.into());
}
fs::remove_file(&real)?;
Ok(())
})
}
fn rmdir<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<()>> {
Box::pin(async move {
let real = self.entry_path(path)?;
if !fs::symlink_metadata(&real)?.is_dir() {
return Err(io::ErrorKind::NotADirectory.into());
}
fs::remove_dir(&real)?;
Ok(())
})
}
fn write_at<'a>(
&'a self,
path: &'a Path,
buf: &'a [u8],
offset: u64,
) -> BoxFuture<'a, io::Result<usize>> {
Box::pin(async move { pwrite(&self.open(path, true)?, buf, offset) })
}
fn truncate<'a>(&'a self, path: &'a Path, size: u64) -> BoxFuture<'a, io::Result<()>> {
Box::pin(async move { self.open(path, true)?.set_len(size) })
}
fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, io::Result<()>> {
Box::pin(async move { fs::rename(self.entry_path(from)?, self.entry_path(to)?) })
}
fn flush<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<()>> {
Box::pin(async move { self.open(path, true)?.sync_all() })
}
}
#[cfg(unix)]
fn pread(file: &fs::File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
std::os::unix::fs::FileExt::read_at(file, buf, offset)
}
#[cfg(unix)]
fn pwrite(file: &fs::File, buf: &[u8], offset: u64) -> io::Result<usize> {
std::os::unix::fs::FileExt::write_at(file, buf, offset)
}
#[cfg(windows)]
fn pread(file: &fs::File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
std::os::windows::fs::FileExt::seek_read(file, buf, offset)
}
#[cfg(windows)]
fn pwrite(file: &fs::File, buf: &[u8], offset: u64) -> io::Result<usize> {
std::os::windows::fs::FileExt::seek_write(file, buf, offset)
}