use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::str::FromStr;
use itertools::{Itertools, Position};
use crate::error::Error;
use crate::file::{
Directory, ReadOnlyDirectory, ReadOnlySymbolicLink, ReadOnlyTypeWithFile, SymbolicLink, Type, TypeWithFile,
};
use crate::fs::error::FsError;
use crate::path::{Component, Path, PathError};
use crate::permissions::Permissions;
use crate::types::{Gid, Uid};
pub mod error;
pub mod structures;
#[cfg(feature = "ext2")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext2")))]
pub mod ext2;
pub const PATH_MAX: usize = 4_096;
pub trait FileSystem<Dir: Directory> {
fn root(&self) -> Result<Dir, Error<Dir::FsError>>;
fn double_slash_root(&self) -> Result<Dir, Error<Dir::FsError>>;
fn get_file(
&self,
path: &Path,
current_dir: Dir,
symlink_resolution: bool,
) -> Result<TypeWithFile<Dir>, Error<Dir::FsError>>
where
Self: Sized,
{
fn path_resolution<FSE: core::error::Error, D: Directory<FsError = FSE>>(
fs: &impl FileSystem<D>,
path: &Path,
mut current_dir: D,
symlink_resolution: bool,
mut visited_symlinks: Vec<String>,
) -> Result<TypeWithFile<D>, Error<FSE>> {
let canonical_path = path.canonical();
if canonical_path.len() > PATH_MAX {
return Err(Error::Fs(FsError::NameTooLong(canonical_path.to_string())));
}
let trailing_blackslash = canonical_path.as_unix_str().has_trailing_backslash();
let mut symlink_encountered = None;
let mut components = canonical_path.components();
for (pos, comp) in components.with_position() {
match comp {
Component::RootDir => {
if pos == Position::First || pos == Position::Only {
current_dir = fs.root()?;
} else {
unreachable!("The root directory cannot be encountered during the pathname resolution");
}
},
Component::DoubleSlashRootDir => {
if pos == Position::First || pos == Position::Only {
current_dir = fs.double_slash_root()?;
} else {
unreachable!(
"The double slash root directory cannot be encountered during the pathname resolution"
);
}
},
Component::CurDir => {},
Component::ParentDir => {
current_dir = current_dir.parent()?;
},
Component::Normal(filename) => {
let children = current_dir.entries()?;
let Some(entry) =
children.into_iter().find(|entry| entry.filename == filename).map(|entry| entry.file)
else {
return Err(Error::Fs(FsError::NotFound(filename.to_string())));
};
#[allow(clippy::wildcard_enum_match_arm)]
match entry {
TypeWithFile::Directory(dir) => {
current_dir = dir;
},
TypeWithFile::SymbolicLink(symlink)
if (pos != Position::Last && pos != Position::Only && !trailing_blackslash)
|| symlink_resolution =>
{
let pointed_file = SymbolicLink::get_pointed_file(&symlink)?.to_owned();
if pointed_file.is_empty() {
return Err(Error::Fs(FsError::NoEnt(filename.to_string())));
};
symlink_encountered = Some(pointed_file);
break;
},
_ => {
return if (pos == Position::Last || pos == Position::Only) && !trailing_blackslash {
Ok(entry)
} else {
Err(Error::Fs(FsError::NotDir(filename.to_string())))
};
},
}
},
}
}
match symlink_encountered {
None => Ok(TypeWithFile::Directory(current_dir)),
Some(pointed_file) => {
if visited_symlinks.contains(&pointed_file) {
return Err(Error::Fs(FsError::Loop(pointed_file)));
}
visited_symlinks.push(pointed_file.clone());
let pointed_path = Path::from_str(&pointed_file).map_err(Error::Path)?;
let complete_path = match TryInto::<Path>::try_into(&components) {
Ok(remaining_path) => pointed_path.join(&remaining_path),
Err(_) => pointed_path,
};
if complete_path.len() >= PATH_MAX {
Err(Error::Fs(FsError::NameTooLong(complete_path.to_string())))
} else {
path_resolution(fs, &complete_path, current_dir, symlink_resolution, visited_symlinks)
}
},
}
}
path_resolution(self, path, current_dir, symlink_resolution, vec![])
}
fn create_file(
&mut self,
path: &Path<'_>,
file_type: Type,
permissions: Permissions,
user_id: Uid,
group_id: Gid,
) -> Result<TypeWithFile<Dir>, Error<Dir::FsError>>
where
Self: Sized,
{
if path.is_relative() {
return Err(Error::Path(PathError::AbsolutePathRequired(path.to_string())));
}
let Some(parent_dir_path) = path.parent() else {
return Err(Error::Fs(FsError::EntryAlreadyExist(path.to_string())));
};
let parent_dir_file = self.get_file(&parent_dir_path, self.root()?, true)?;
let mut parent_dir = match parent_dir_file {
TypeWithFile::Directory(dir) => dir,
TypeWithFile::Regular(_)
| TypeWithFile::SymbolicLink(_)
| TypeWithFile::Fifo(_)
| TypeWithFile::CharacterDevice(_)
| TypeWithFile::BlockDevice(_)
| TypeWithFile::Socket(_) => {
return Err(Error::Fs(FsError::WrongFileType {
expected: Type::Directory,
given: parent_dir_file.into(),
}));
},
};
parent_dir.add_entry(
unsafe { path.file_name().unwrap_unchecked() },
file_type,
permissions,
user_id,
group_id,
)
}
fn remove_file(&mut self, path: Path<'_>) -> Result<(), Error<Dir::FsError>>
where
Self: Sized,
{
if path.is_relative() {
return Err(Error::Path(PathError::AbsolutePathRequired(path.to_string())));
}
let Some(parent_dir_path) = path.parent() else {
return Err(Error::Fs(FsError::EntryAlreadyExist(path.to_string())));
};
let parent_dir_file = self.get_file(&parent_dir_path, self.root()?, true)?;
let mut parent_dir = match parent_dir_file {
TypeWithFile::Directory(dir) => dir,
TypeWithFile::Regular(_)
| TypeWithFile::SymbolicLink(_)
| TypeWithFile::Fifo(_)
| TypeWithFile::CharacterDevice(_)
| TypeWithFile::BlockDevice(_)
| TypeWithFile::Socket(_) => {
return Err(Error::Fs(FsError::WrongFileType {
expected: Type::Directory,
given: parent_dir_file.into(),
}));
},
};
parent_dir.remove_entry(unsafe { path.file_name().unwrap_unchecked() })
}
}
pub trait ReadOnlyFileSystem<RoDir: ReadOnlyDirectory> {
fn root(&self) -> Result<RoDir, Error<RoDir::FsError>>;
fn double_slash_root(&self) -> Result<RoDir, Error<RoDir::FsError>>;
fn get_file(
&self,
path: &Path,
current_dir: RoDir,
symlink_resolution: bool,
) -> Result<ReadOnlyTypeWithFile<RoDir>, Error<RoDir::FsError>>
where
Self: Sized,
{
fn path_resolution<FSE: core::error::Error, D: ReadOnlyDirectory<FsError = FSE>>(
fs: &impl ReadOnlyFileSystem<D>,
path: &Path,
mut current_dir: D,
symlink_resolution: bool,
mut visited_symlinks: Vec<String>,
) -> Result<ReadOnlyTypeWithFile<D>, Error<FSE>> {
let canonical_path = path.canonical();
if canonical_path.len() > PATH_MAX {
return Err(Error::Fs(FsError::NameTooLong(canonical_path.to_string())));
}
let trailing_blackslash = canonical_path.as_unix_str().has_trailing_backslash();
let mut symlink_encountered = None;
let mut components = canonical_path.components();
for (pos, comp) in components.with_position() {
match comp {
Component::RootDir => {
if pos == Position::First || pos == Position::Only {
current_dir = fs.root()?;
} else {
unreachable!("The root directory cannot be encountered during the pathname resolution");
}
},
Component::DoubleSlashRootDir => {
if pos == Position::First || pos == Position::Only {
current_dir = fs.double_slash_root()?;
} else {
unreachable!(
"The double slash root directory cannot be encountered during the pathname resolution"
);
}
},
Component::CurDir => {},
Component::ParentDir => {
current_dir = current_dir.parent()?;
},
Component::Normal(filename) => {
let children = current_dir.entries()?;
let Some(entry) =
children.into_iter().find(|entry| entry.filename == filename).map(|entry| entry.file)
else {
return Err(Error::Fs(FsError::NotFound(filename.to_string())));
};
#[allow(clippy::wildcard_enum_match_arm)]
match entry {
ReadOnlyTypeWithFile::Directory(dir) => {
current_dir = dir;
},
ReadOnlyTypeWithFile::SymbolicLink(symlink)
if (pos != Position::Last && pos != Position::Only)
|| !trailing_blackslash
|| !symlink_resolution =>
{
let pointed_file = symlink.get_pointed_file()?.to_owned();
if pointed_file.is_empty() {
return Err(Error::Fs(FsError::NoEnt(filename.to_string())));
};
symlink_encountered = Some(pointed_file);
break;
},
_ => {
return if (pos == Position::Last || pos == Position::Only) && !trailing_blackslash {
Ok(entry)
} else {
Err(Error::Fs(FsError::NotDir(filename.to_string())))
};
},
}
},
}
}
match symlink_encountered {
None => Ok(ReadOnlyTypeWithFile::Directory(current_dir)),
Some(pointed_file) => {
if visited_symlinks.contains(&pointed_file) {
return Err(Error::Fs(FsError::Loop(pointed_file)));
}
visited_symlinks.push(pointed_file.clone());
let pointed_path = Path::from_str(&pointed_file).map_err(Error::Path)?;
let complete_path = match TryInto::<Path>::try_into(&components) {
Ok(remaining_path) => pointed_path.join(&remaining_path),
Err(_) => pointed_path,
};
if complete_path.len() >= PATH_MAX {
Err(Error::Fs(FsError::NameTooLong(complete_path.to_string())))
} else {
path_resolution(fs, &complete_path, current_dir, symlink_resolution, visited_symlinks)
}
},
}
}
path_resolution(self, path, current_dir, symlink_resolution, vec![])
}
}