pub mod traits {
#[cfg(doc)]
use std::{
fs, io,
path::{Path, PathBuf},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum FileType {
File,
Dir,
Link,
}
pub trait Stat {
type Mode;
fn file_type(&self) -> Result<FileType, Self::Mode>;
}
pub trait DirectoryEntry {
type PathRef<'s>;
type OwnedPath;
fn name<'e>(&'e self) -> Self::PathRef<'e>;
fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath;
type EagerMode;
fn eager_file_type(&self) -> Option<Result<FileType, Self::EagerMode>>;
}
pub trait DirectoryStream {
type DirEntry<'dir>: DirectoryEntry
where Self: 'dir;
type Err;
fn read_dir<'dir>(&'dir mut self) -> Result<Option<Self::DirEntry<'dir>>, Self::Err>;
}
pub trait VFS {
type Ctx<'vfs>
where Self: 'vfs;
type Err;
type PathRef<'s>;
type OwnedPath;
fn path_ref<'s>(p: &'s Self::OwnedPath) -> Self::PathRef<'s>;
fn initial_context<'vfs>(&'vfs self) -> Result<Self::Ctx<'vfs>, Self::Err>;
fn join_context_dir<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Self::Ctx<'vfs>;
fn join_context_link<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
link_rel: Self::PathRef<'_>,
target: Self::PathRef<'_>,
) -> Self::Ctx<'vfs>;
type Stat: Stat;
type File;
type FileOptions;
type Dir<'vfs>: DirectoryStream
where Self: 'vfs;
fn entry_rel<'vfs, 'dir, 's>(
name: <<<Self as VFS>::Dir<'vfs> as DirectoryStream>::DirEntry<'dir> as DirectoryEntry>::PathRef<'s>,
) -> Self::PathRef<'s>;
fn entry_owned_rel<'vfs, 'dir>(
name: <<<Self as VFS>::Dir<'vfs> as DirectoryStream>::DirEntry<'dir> as DirectoryEntry>::OwnedPath,
) -> Self::OwnedPath;
fn stat<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::Stat, Self::Err>;
fn read_link<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::OwnedPath, Self::Err>;
fn open_file<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
opts: Self::FileOptions,
) -> Result<Self::File, Self::Err>;
fn open_dir<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::Dir<'vfs>, Self::Err>;
}
}
#[cfg(unix)]
pub mod posix {}
pub mod path_based {
use std::{
env, ffi, fs, io,
path::{Path, PathBuf},
};
use super::traits;
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct Stat(fs::Metadata);
impl traits::Stat for Stat {
type Mode = fs::FileType;
fn file_type(&self) -> Result<traits::FileType, Self::Mode> {
let ty = self.0.file_type();
if ty.is_symlink() {
return Ok(traits::FileType::Link);
}
if ty.is_file() {
return Ok(traits::FileType::File);
}
if ty.is_dir() {
return Ok(traits::FileType::Dir);
}
Err(ty)
}
}
cfg_if::cfg_if! {
if #[cfg(all(unix, feature = "nightly"))] {
#[derive(Debug)]
#[repr(transparent)]
pub struct DirectoryEntry(fs::DirEntry);
impl DirectoryEntry {
pub fn new(entry: fs::DirEntry) -> Self { Self(entry) }
}
impl traits::DirectoryEntry for DirectoryEntry {
type PathRef<'s> = &'s ffi::OsStr;
type OwnedPath = ffi::OsString;
fn name<'e>(&'e self) -> Self::PathRef<'e> {
use std::os::unix::fs::DirEntryExt2;
self.0.file_name_ref()
}
fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath {
path.to_os_string()
}
type EagerMode = fs::FileType;
cfg_if::cfg_if! {
if #[cfg(any(
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
target_os = "vxworks",
target_os = "aix",
target_os = "nto",
target_os = "vita",
))] {
fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> { None }
} else {
fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> {
let ty = self.0.file_type().unwrap();
if ty.is_symlink() {
return Some(Ok(traits::FileType::Link));
}
if ty.is_file() {
return Some(Ok(traits::FileType::File));
}
if ty.is_dir() {
return Some(Ok(traits::FileType::Dir));
}
Some(Err(ty))
}
}
}
}
} else {
#[derive(Debug)]
pub struct DirectoryEntry {
inner: fs::DirEntry,
name: ffi::OsString,
}
impl DirectoryEntry {
pub fn new(entry: fs::DirEntry) -> Self {
let name = entry.file_name();
Self {
inner: entry,
name,
}
}
}
impl traits::DirectoryEntry for DirectoryEntry {
type PathRef<'s> = &'s ffi::OsStr;
type OwnedPath = ffi::OsString;
fn name<'e>(&'e self) -> Self::PathRef<'e> {
&self.name
}
fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath {
path.to_os_string()
}
type EagerMode = fs::FileType;
cfg_if::cfg_if! {
if #[cfg(any(
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
target_os = "vxworks",
target_os = "aix",
target_os = "nto",
target_os = "vita",
))] {
fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> { None }
} else {
fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> {
let ty = self.inner.file_type().unwrap();
if ty.is_symlink() {
return Some(Ok(traits::FileType::Link));
}
if ty.is_file() {
return Some(Ok(traits::FileType::File));
}
if ty.is_dir() {
return Some(Ok(traits::FileType::Dir));
}
Some(Err(ty))
}
}
}
}
}
}
pub struct DirectoryStream {
inner: fs::ReadDir,
done: bool,
}
impl traits::DirectoryStream for DirectoryStream {
type DirEntry<'dir> = DirectoryEntry;
type Err = io::Error;
fn read_dir<'dir>(&'dir mut self) -> Result<Option<Self::DirEntry<'dir>>, Self::Err> {
if self.done {
return Ok(None);
}
loop {
use traits::DirectoryEntry as _;
let Some(result) = self.inner.next().transpose()? else {
self.done = true;
return Ok(None);
};
let entry = DirectoryEntry::new(result);
let name = entry.name();
if name == "." || name == ".." {
continue;
}
return Ok(Some(entry));
}
}
}
#[derive(Debug)]
pub struct VFS;
impl traits::VFS for VFS {
type Ctx<'vfs> = PathBuf;
type Err = io::Error;
type PathRef<'s> = &'s Path;
type OwnedPath = PathBuf;
fn path_ref<'s>(p: &'s Self::OwnedPath) -> Self::PathRef<'s> { p.as_ref() }
fn initial_context<'vfs>(&'vfs self) -> Result<Self::Ctx<'vfs>, Self::Err> {
env::current_dir()
}
fn join_context_dir<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Self::Ctx<'vfs> {
ctx.join(rel)
}
fn join_context_link<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
_link_rel: Self::PathRef<'_>,
target: Self::PathRef<'_>,
) -> Self::Ctx<'vfs> {
ctx.join(target)
}
type Stat = Stat;
type File = fs::File;
type FileOptions = fs::OpenOptions;
type Dir<'vfs> = DirectoryStream;
fn entry_rel<'vfs, 'dir, 's>(
name: <<<Self as traits::VFS>::Dir<'vfs> as traits::DirectoryStream>::DirEntry<'dir> as traits::DirectoryEntry>::PathRef<'s>,
) -> Self::PathRef<'s> {
name.as_ref()
}
fn entry_owned_rel<'vfs, 'dir>(
name: <<<Self as traits::VFS>::Dir<'vfs> as traits::DirectoryStream>::DirEntry<'dir> as traits::DirectoryEntry>::OwnedPath,
) -> Self::OwnedPath {
name.into()
}
fn stat<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::Stat, Self::Err> {
fs::symlink_metadata(ctx.join(rel)).map(Stat)
}
fn read_link<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::OwnedPath, Self::Err> {
fs::read_link(ctx.join(rel))
}
fn open_file<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
opts: Self::FileOptions,
) -> Result<Self::File, Self::Err> {
opts.open(ctx.join(rel))
}
fn open_dir<'vfs>(
&'vfs self,
ctx: Self::Ctx<'vfs>,
rel: Self::PathRef<'_>,
) -> Result<Self::Dir<'vfs>, Self::Err> {
fs::read_dir(ctx.join(rel)).map(|inner| DirectoryStream { inner, done: false })
}
}
#[cfg(test)]
mod test {
use tempdir::TempDir;
use super::*;
#[test]
fn file() -> io::Result<()> {
let td = TempDir::new("asdf")?;
fs::write(td.path().join("f.txt"), "asdf\n")?;
use traits::VFS as _;
let vfs = VFS;
let ctx = vfs.initial_context()?;
let ctx = vfs.join_context_dir(ctx, td.path());
use traits::Stat as _;
let stat = vfs.stat(ctx.clone(), Path::new("f.txt"))?;
assert_eq!(stat.file_type(), Ok(traits::FileType::File));
let mut opts = fs::OpenOptions::new();
opts.read(true);
let mut f = vfs.open_file(ctx, Path::new("f.txt"), opts)?;
use io::Read;
let mut s = String::new();
f.read_to_string(&mut s)?;
assert_eq!(s, "asdf\n");
Ok(())
}
#[test]
fn link() -> io::Result<()> {
let td = TempDir::new("asdf")?;
cfg_if::cfg_if! {
if #[cfg(unix)] {
std::os::unix::fs::symlink("wow.txt", td.path().join("l.txt"))?;
} else {
std::os::windows::fs::symlink_file("wow.txt", td.path().join("l.txt"))?;
}
}
use traits::VFS as _;
let vfs = VFS;
let ctx = vfs.initial_context()?;
let ctx = vfs.join_context_dir(ctx, td.path());
use traits::Stat as _;
let stat = vfs.stat(ctx.clone(), Path::new("l.txt"))?;
assert_eq!(stat.file_type(), Ok(traits::FileType::Link));
let target = vfs.read_link(ctx, Path::new("l.txt"))?;
assert_eq!(&target, Path::new("wow.txt"));
Ok(())
}
#[test]
fn dir() -> io::Result<()> {
let td = TempDir::new("asdf")?;
fs::write(td.path().join("f.txt"), "asdf\n")?;
fs::create_dir(td.path().join("a"))?;
fs::write(td.path().join("a/g.txt"), "asdf2\n")?;
use traits::VFS as _;
let vfs = VFS;
let ctx = vfs.initial_context()?;
use traits::Stat as _;
let stat = vfs.stat(ctx.clone(), td.path())?;
assert_eq!(stat.file_type(), Ok(traits::FileType::Dir));
let mut opts = fs::OpenOptions::new();
opts.read(true);
use io::Read;
use traits::{DirectoryEntry as _, DirectoryStream as _, Stat as _};
let mut dir = vfs.open_dir(ctx.clone(), td.path())?;
let ctx = vfs.join_context_dir(ctx, td.path());
while let Some(entry) = dir.read_dir()? {
let ty = entry
.eager_file_type()
.map(|r| r.unwrap())
.unwrap_or_else(|| {
vfs
.stat(ctx.clone(), VFS::entry_rel(entry.name()))
.unwrap()
.file_type()
.unwrap()
});
match VFS::path_ref(&VFS::entry_owned_rel(DirectoryEntry::owned_name(
entry.name(),
)))
.to_str()
.unwrap()
{
"f.txt" => {
assert_eq!(ty, traits::FileType::File);
let mut f = vfs.open_file(ctx.clone(), VFS::entry_rel(entry.name()), opts.clone())?;
let mut s = String::new();
f.read_to_string(&mut s)?;
assert_eq!(s, "asdf\n");
},
"a" => {
assert_eq!(ty, traits::FileType::Dir);
let mut dir = vfs.open_dir(ctx.clone(), VFS::entry_rel(entry.name()))?;
let ctx = vfs.join_context_dir(ctx.clone(), VFS::entry_rel(entry.name()));
while let Some(entry) = dir.read_dir()? {
let ty = entry
.eager_file_type()
.map(|r| r.unwrap())
.unwrap_or_else(|| {
vfs
.stat(ctx.clone(), VFS::entry_rel(entry.name()))
.unwrap()
.file_type()
.unwrap()
});
match DirectoryEntry::owned_name(entry.name()).to_str().unwrap() {
"g.txt" => {
assert_eq!(ty, traits::FileType::File);
let mut f =
vfs.open_file(ctx.clone(), VFS::entry_rel(entry.name()), opts.clone())?;
let mut s = String::new();
f.read_to_string(&mut s)?;
assert_eq!(s, "asdf2\n");
},
_ => unreachable!(),
}
}
},
_ => unreachable!(),
}
}
Ok(())
}
}
}