Skip to content
This repository has been archived by the owner on Mar 15, 2024. It is now read-only.

Commit

Permalink
feat(fs): add read-only types
Browse files Browse the repository at this point in the history
  • Loading branch information
RatCornu committed Feb 10, 2024
1 parent 24cff84 commit 1e0da21
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ An OS and architecture independent implementation of some Unix filesystems in Ru

* [`ext4`](https://en.wikipedia.org/wiki/Ext2): ❌

* [`fatfs`](https://en.wikipedia.org/wiki/FatFs): ❌
* [`SquashFS`](https://fr.wikipedia.org/wiki/SquashFS): ❌

## Usage

Expand Down
145 changes: 145 additions & 0 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use alloc::vec::Vec;

use itertools::Itertools;

use crate::error::Error;
use crate::io::{Read, Seek, Write};
use crate::path::{UnixStr, PARENT_DIR};
Expand Down Expand Up @@ -85,6 +87,13 @@ pub trait Regular: File + Read + Write + Seek {
fn truncate(&mut self, size: u64) -> Result<(), Error<<Self as File>::Error>>;
}

/// A read-only file that is a randomly accessible sequence of bytes, with no further structure imposed by the system.
///
/// This is the read-only version of [`Regular`].
pub trait ReadOnlyRegular: File + Read + Seek {}

impl<R: Regular> ReadOnlyRegular for R {}

/// An object that associates a filename with a file. Several directory entries can associate names with the same file.
///
/// Defined in [this POSIX definition](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_130).
Expand All @@ -98,6 +107,29 @@ pub struct DirectoryEntry<'path, Dir: Directory> {
pub file: TypeWithFile<Dir>,
}

/// An read-only object that associates a filename with a file. Several directory entries can associate names with the same file.
///
/// This is the read-only version of [`DirectoryEntry`].
pub struct ReadOnlyDirectoryEntry<'path, RoDir: ReadOnlyDirectory> {
/// Name of the file pointed by this directory entry.
///
/// See more information on valid filenames in [this POSIX definition](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_170).
pub filename: UnixStr<'path>,

/// File pointed by this directory entry.
pub file: ReadOnlyTypeWithFile<RoDir>,
}

impl<'path, Dir: Directory> From<DirectoryEntry<'path, Dir>> for ReadOnlyDirectoryEntry<'path, Dir> {
#[inline]
fn from(value: DirectoryEntry<'path, Dir>) -> Self {
Self {
filename: value.filename,
file: value.file.into(),
}
}
}

/// A file that contains directory entries. No two directory entries in the same directory have the same name.
///
/// Defined in [this POSIX definition](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_129).
Expand Down Expand Up @@ -169,6 +201,69 @@ pub trait Directory: Sized + File {
}
}

/// A read-only file that contains directory entries. No two directory entries in the same directory have the same name.
///
/// This is the read-only version of [`Directory`].
pub trait ReadOnlyDirectory: Sized + File {
/// Type of the regular files in the [`FileSystem`](crate::fs::FileSystem) this directory belongs to.
type Regular: ReadOnlyRegular<Error = Self::Error>;

/// Type of the symbolic links in the [`FileSystem`](crate::fs::FileSystem) this directory belongs to.
type SymbolicLink: ReadOnlySymbolicLink<Error = Self::Error>;

/// Type of the other files (if any) in the [`FileSystem`](crate::fs::FileSystem) this directory belongs to.
type File: File<Error = Self::Error>;

/// Returns the directory entries contained.
///
/// No two [`DirectoryEntry`] returned can have the same `filename`.
///
/// The result must contain at least the entries `.` (the current directory) and `..` (the parent directory).
///
/// # Errors
///
/// Returns an [`DevError`](crate::dev::error::DevError) if the device on which the directory is located could not be read.
fn entries(&self) -> Result<Vec<ReadOnlyDirectoryEntry<Self>>, Error<Self::Error>>;

/// Returns the entry with the given name.
///
/// # Errors
///
/// Returns an [`DevError`](crate::dev::error::DevError) if the device on which the directory is located could not be read.
#[allow(clippy::type_complexity)]
#[inline]
fn entry(&self, name: UnixStr) -> Result<Option<ReadOnlyTypeWithFile<Self>>, Error<Self::Error>> {
let children = self.entries()?;
Ok(children.into_iter().find(|entry| entry.filename == name).map(|entry| entry.file))
}

/// Returns the parent directory.
///
/// If `self` if the root directory, it must return itself.
///
/// # Errors
///
/// Returns an [`DevError`](crate::dev::error::DevError) if the device on which the directory is located could not be read.
#[inline]
fn parent(&self) -> Result<Self, Error<Self::Error>> {
let Some(ReadOnlyTypeWithFile::Directory(parent_entry)) = self.entry(PARENT_DIR.clone())? else {
unreachable!("`entries` must return `..` that corresponds to the parent directory.")
};
Ok(parent_entry)
}
}

impl<Dir: Directory> ReadOnlyDirectory for Dir {
type File = Dir::File;
type Regular = Dir::Regular;
type SymbolicLink = Dir::SymbolicLink;

#[inline]
fn entries(&self) -> Result<Vec<ReadOnlyDirectoryEntry<Self>>, Error<Self::Error>> {
<Self as Directory>::entries(self).map(|entries| entries.into_iter().map(Into::into).collect_vec())
}
}

/// A type of file with the property that when the file is encountered during pathname resolution, a string stored by the file is
/// used to modify the pathname resolution.
///
Expand All @@ -189,6 +284,26 @@ pub trait SymbolicLink: File {
fn set_pointed_file(&mut self, pointed_file: &str) -> Result<(), Error<Self::Error>>;
}

/// A read-only type of file with the property that when the file is encountered during pathname resolution, a string stored by the
/// file is used to modify the pathname resolution.
///
/// This is the read-only version of [`SymbolicLink`].
pub trait ReadOnlySymbolicLink: File {
/// Returns the string stored in this symbolic link.
///
/// # Errors
///
/// Returns an [`DevError`](crate::dev::error::DevError) if the device on which the directory is located could not be read.
fn get_pointed_file(&self) -> Result<&str, Error<Self::Error>>;
}

impl<Symlink: SymbolicLink> ReadOnlySymbolicLink for Symlink {
#[inline]
fn get_pointed_file(&self) -> Result<&str, Error<Self::Error>> {
<Self as SymbolicLink>::get_pointed_file(self)
}
}

/// Enumeration of possible file types in a standard UNIX-like filesystem.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
Expand Down Expand Up @@ -241,6 +356,36 @@ pub enum TypeWithFile<Dir: Directory> {
Other(Dir::File),
}

/// Enumeration of possible file types in a standard UNIX-like filesystem with an attached file object.
///
/// This is the read-only version of [`TypeWithFile`].
#[allow(clippy::module_name_repetitions)]
pub enum ReadOnlyTypeWithFile<RoDir: ReadOnlyDirectory> {
/// Storage unit of a filesystem.
Regular(RoDir::Regular),

/// Node containing other nodes.
Directory(RoDir),

/// Node pointing towards an other node in the filesystem.
SymbolicLink(RoDir::SymbolicLink),

/// A file system dependant file (e.g [the Doors](https://en.wikipedia.org/wiki/Doors_(computing)) on Solaris systems).
Other(RoDir::File),
}

impl<Dir: Directory> From<TypeWithFile<Dir>> for ReadOnlyTypeWithFile<Dir> {
#[inline]
fn from(value: TypeWithFile<Dir>) -> Self {
match value {
TypeWithFile::Regular(file) => Self::Regular(file),
TypeWithFile::Directory(file) => Self::Directory(file),
TypeWithFile::SymbolicLink(file) => Self::SymbolicLink(file),
TypeWithFile::Other(file) => Self::Other(file),
}
}
}

impl<Dir: Directory> From<TypeWithFile<Dir>> for Type {
#[inline]
fn from(value: TypeWithFile<Dir>) -> Self {
Expand Down
171 changes: 169 additions & 2 deletions src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use core::str::FromStr;
use itertools::{Itertools, Position};

use crate::error::Error;
use crate::file::{Directory, SymbolicLink, TypeWithFile};
use crate::file::{Directory, ReadOnlyDirectory, ReadOnlySymbolicLink, ReadOnlyTypeWithFile, SymbolicLink, TypeWithFile};
use crate::fs::error::FsError;
use crate::path::{Component, Path};

Expand Down Expand Up @@ -139,7 +139,7 @@ pub trait FileSystem<Dir: Directory> {
|| !trailing_blackslash
|| !symlink_resolution =>
{
let pointed_file = symlink.get_pointed_file()?.to_owned();
let pointed_file = SymbolicLink::get_pointed_file(&symlink)?.to_owned();
if pointed_file.is_empty() {
return Err(Error::Fs(FsError::NoEnt(filename.to_string())));
};
Expand Down Expand Up @@ -186,3 +186,170 @@ pub trait FileSystem<Dir: Directory> {
path_resolution(self, path, current_dir, symlink_resolution, vec![])
}
}

/// A read-only filesystem.
trait ReadOnlyFileSystem<RoDir: ReadOnlyDirectory> {
/// Returns the root directory of the filesystem.
///
/// # Errors
///
/// Returns a [`DevError`](crate::dev::error::DevError) if the device could not be read.
fn root(&self) -> Result<RoDir, Error<RoDir::Error>>;

/// Returns the double slash root directory of the filesystem.
///
/// If you do not have any idea of what this is, you are probably looking for [`root`](trait.FileSystem.html#tymethod.root).
///
/// See [`DoubleSlashRootDir`](Component::DoubleSlashRootDir) and [`Path`] for more information.
///
/// # Errors
///
/// Returns a [`DevError`](crate::dev::error::DevError) if the device could not be read.
fn double_slash_root(&self) -> Result<RoDir, Error<RoDir::Error>>;

/// Performs a pathname resolution as described in [this POSIX definition](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13).
///
/// Returns the file of this filesystem corresponding to the given `path`, starting at the `current_dir`.
///
/// `symlink_resolution` indicates whether the function calling this method is required to act on the symbolic link itself, or
/// certain arguments direct that the function act on the symbolic link itself.
///
/// # Errors
///
/// Returns an [`NotFound`](FsError::NotFound) error if the given path does not leed to an existing path.
///
/// Returns an [`NotDir`](FsError::NotDir) error if one of the components of the file is not a directory.
///
/// Returns an [`Loop`](FsError::Loop) error if a loop is found during the symbolic link resolution.
///
/// Returns an [`NameTooLong`](FsError::NameTooLong) error if the complete path contains more than [`PATH_MAX`] characters.
///
/// Returns an [`NoEnt`](FsError::NoEnt) error if an encountered symlink points to a non-existing file.
#[inline]
fn get_file(
&self,
path: &Path,
current_dir: RoDir,
symlink_resolution: bool,
) -> Result<ReadOnlyTypeWithFile<RoDir>, Error<RoDir::Error>>
where
Self: Sized,
{
/// Auxiliary function used to store the visited symlinks during the pathname resolution to detect loops caused bt symbolic
/// links.
#[inline]
fn path_resolution<E: core::error::Error, D: ReadOnlyDirectory<Error = E>>(
fs: &impl ReadOnlyFileSystem<D>,
path: &Path,
mut current_dir: D,
symlink_resolution: bool,
mut visited_symlinks: Vec<String>,
) -> Result<ReadOnlyTypeWithFile<D>, Error<E>> {
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;
},

// This case is the symbolic link resolution, which is the one described as **not** being the one
// explained in the following paragraph from the POSIX definition of the
// pathname resolution:
//
// If a symbolic link is encountered during pathname resolution, the behavior shall depend on whether
// the pathname component is at the end of the pathname and on the function
// being performed. If all of the following are true, then pathname
// resolution is complete:
// 1. This is the last pathname component of the pathname.
// 2. The pathname has no trailing <slash>.
// 3. The function is required to act on the symbolic link itself, or certain arguments direct that
// the function act on the symbolic link itself.
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![])
}
}

0 comments on commit 1e0da21

Please sign in to comment.