Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion litebox/src/fs/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use crate::{
FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence, UserInfo,
errors::{
ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError,
ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError,
ReadDirError, ReadError, ReadlinkError, RmdirError, SeekError, SymlinkError,
TruncateError, UnlinkError, WriteError,
},
},
path::Arg,
Expand Down Expand Up @@ -357,6 +358,16 @@ impl<
unimplemented!()
}

fn symlink(&self, _target: impl Arg, _linkpath: impl Arg) -> Result<(), SymlinkError> {
// Device filesystem doesn't support creating symlinks
Err(SymlinkError::ReadOnlyFileSystem)
}

fn readlink(&self, _path: impl Arg) -> Result<String, ReadlinkError> {
// Device filesystem doesn't have symlinks - return PathError for consistency
Err(ReadlinkError::PathError(PathError::NoSuchFileOrDirectory))
}

fn read_dir(
&self,
_fd: &FileFd<Platform>,
Expand Down
39 changes: 39 additions & 0 deletions litebox/src/fs/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub enum SeekError {
}

/// Possible errors from [`FileSystem::truncate`]
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum TruncateError {
#[error("fd has been closed already")]
Expand Down Expand Up @@ -163,6 +164,32 @@ pub enum RmdirError {
PathError(#[from] PathError),
}

/// Possible errors from [`FileSystem::symlink`]
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum SymlinkError {
#[error("the parent directory does not allow write permission")]
NoWritePerms,
#[error("linkpath already exists")]
AlreadyExists,
#[error("the named file resides on a read-only filesystem")]
ReadOnlyFileSystem,
#[error("target is an empty string")]
EmptyTarget,
#[error(transparent)]
PathError(#[from] PathError),
}

/// Possible errors from [`FileSystem::readlink`]
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ReadlinkError {
#[error("the named file is not a symbolic link")]
NotASymlink,
#[error(transparent)]
PathError(#[from] PathError),
}

/// Possible errors from [`FileSystem::read_dir`]
#[non_exhaustive]
#[derive(Error, Debug)]
Expand Down Expand Up @@ -208,3 +235,15 @@ impl From<crate::path::ConversionError> for PathError {
Self::InvalidPathname
}
}

impl From<crate::path::ConversionError> for SymlinkError {
fn from(value: crate::path::ConversionError) -> Self {
Self::PathError(value.into())
}
}

impl From<crate::path::ConversionError> for ReadlinkError {
fn from(value: crate::path::ConversionError) -> Self {
Self::PathError(value.into())
}
}
128 changes: 123 additions & 5 deletions litebox/src/fs/in_mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! An in-memory file system, not backed by any physical device.

use alloc::string::String;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use hashbrown::HashMap;
Expand All @@ -14,7 +14,8 @@ use crate::sync;

use super::errors::{
ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError,
ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError,
ReadDirError, ReadError, ReadlinkError, RmdirError, SeekError, SymlinkError, TruncateError,
UnlinkError, WriteError,
};
use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, SeekWhence, UserInfo};

Expand Down Expand Up @@ -275,6 +276,16 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
.litebox
.descriptor_table_mut()
.insert(Descriptor::Dir { dir: dir.clone() }),
Entry::Symlink(_) => {
// Opening a symlink directly with O_NOFOLLOW would be a special case
// For now, we don't support following symlinks in open()
// Return an error if O_NOFOLLOW is set, otherwise this is unimplemented
if flags.contains(OFlags::NOFOLLOW) {
return Err(OpenError::PathError(PathError::ComponentNotADirectory));
}
// TODO: Implement symlink following in open()
unimplemented!("opening symlinks by following them is not yet implemented")
}
};
if flags.contains(OFlags::TRUNC) {
match self.truncate(&fd, 0, true) {
Expand Down Expand Up @@ -472,6 +483,14 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
perms.mode = mode;
Ok(())
}
Entry::Symlink(symlink) => {
let perms = &mut symlink.write().perms;
if !(self.current_user.user == 0 || self.current_user.user == perms.userinfo.user) {
return Err(ChmodError::NotTheOwner);
}
perms.mode = mode;
Ok(())
}
}
}

Expand Down Expand Up @@ -514,6 +533,19 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
}
Ok(())
}
Entry::Symlink(symlink) => {
let perms = &mut symlink.write().perms;
if !(self.current_user.user == 0 || self.current_user.user == perms.userinfo.user) {
return Err(ChownError::NotTheOwner);
}
if let Some(new_user) = user {
perms.userinfo.user = new_user;
}
if let Some(new_group) = group {
perms.userinfo.group = new_group;
}
Ok(())
}
}
}

Expand All @@ -538,11 +570,14 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
let removed = parent
.children
.remove(path.components().unwrap().last().unwrap());
// Just a sanity check
assert!(matches!(removed, Some(FileType::RegularFile)));
// Just a sanity check - could be a regular file or symlink
assert!(matches!(
removed,
Some(FileType::RegularFile | FileType::SymbolicLink)
));
let removed = root.entries.remove(&path).unwrap();
// Just a sanity check
assert!(matches!(removed, Entry::File(File { .. })));
assert!(matches!(removed, Entry::File(_) | Entry::Symlink(_)));
Ok(())
}

Expand Down Expand Up @@ -613,6 +648,63 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
Ok(())
}

fn symlink(
&self,
target: impl crate::path::Arg,
linkpath: impl crate::path::Arg,
) -> Result<(), SymlinkError> {
let target_str = target.as_rust_str()?.to_string();
if target_str.is_empty() {
return Err(SymlinkError::EmptyTarget);
}
let linkpath = self.absolute_path(linkpath)?;
let mut root = self.root.write();
let (parent, entry) = root.parent_and_entry(&linkpath, self.current_user)?;
let Some((_parent_path, parent)) = parent else {
// Attempted to create symlink at `/`
return Err(SymlinkError::AlreadyExists);
};
if entry.is_some() {
return Err(SymlinkError::AlreadyExists);
}
let mut parent = parent.write();
if !self.current_user.can_write(&parent.perms) {
return Err(SymlinkError::NoWritePerms);
}
let old = parent.children.insert(
linkpath.components().unwrap().last().unwrap().into(),
FileType::SymbolicLink,
);
assert!(old.is_none());
let old = root.entries.insert(
linkpath,
Entry::Symlink(Arc::new(sync::RwLock::new(SymlinkX {
perms: Permissions {
// Symlinks typically have 0777 permissions (lrwxrwxrwx)
mode: Mode::RWXU | Mode::RWXG | Mode::RWXO,
userinfo: self.current_user,
},
target: target_str,
unique_id: self.fresh_id(),
}))),
);
assert!(old.is_none());
Ok(())
}

fn readlink(&self, path: impl crate::path::Arg) -> Result<String, ReadlinkError> {
let path = self.absolute_path(path)?;
let root = self.root.read();
let (_, entry) = root.parent_and_entry(&path, self.current_user)?;
let Some(entry) = entry else {
return Err(PathError::NoSuchFileOrDirectory)?;
};
match entry {
Entry::Symlink(symlink) => Ok(symlink.read().target.clone()),
_ => Err(ReadlinkError::NotASymlink),
}
}

fn read_dir(&self, fd: &FileFd<Platform>) -> Result<Vec<DirEntry>, ReadDirError> {
let descriptor_table = self.litebox.descriptor_table();
let Descriptor::Dir { dir } = &descriptor_table
Expand Down Expand Up @@ -641,6 +733,7 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
let ino = match entry {
Entry::File(file) => file.read().unique_id,
Entry::Dir(dir) => dir.read().unique_id,
Entry::Symlink(symlink) => symlink.read().unique_id,
};
NodeInfo {
dev: DEVICE_ID,
Expand Down Expand Up @@ -713,6 +806,15 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
dir.unique_id,
)
}
Entry::Symlink(symlink) => {
let symlink = symlink.read();
(
super::FileType::SymbolicLink,
symlink.perms.clone(),
symlink.target.len(),
symlink.unique_id,
)
}
};
Ok(FileStatus {
file_type,
Expand Down Expand Up @@ -823,6 +925,11 @@ impl<Platform: sync::RawSyncPrimitivesProvider> RootDir<Platform> {
.ok_or(PathError::MissingComponent)?
{
(_, Entry::File(_)) => return Err(PathError::ComponentNotADirectory),
(_, Entry::Symlink(_)) => {
// Symlinks used as directory components need to be followed
// For now, treat as not a directory (symlink resolution not implemented)
return Err(PathError::ComponentNotADirectory);
}
(parent_path, Entry::Dir(dir)) => {
if !current_user.can_execute(&dir.read().perms) {
return Err(PathError::NoSearchPerms {
Expand All @@ -845,13 +952,15 @@ impl<Platform: sync::RawSyncPrimitivesProvider> RootDir<Platform> {
enum Entry<Platform: sync::RawSyncPrimitivesProvider> {
File(File<Platform>),
Dir(Dir<Platform>),
Symlink(Symlink<Platform>),
}

impl<Platform: sync::RawSyncPrimitivesProvider> Entry<Platform> {
fn perms(&self) -> Permissions {
match self {
Self::File(file) => file.read().perms.clone(),
Self::Dir(dir) => dir.read().perms.clone(),
Self::Symlink(symlink) => symlink.read().perms.clone(),
}
}
}
Expand All @@ -861,6 +970,7 @@ impl<Platform: sync::RawSyncPrimitivesProvider> Clone for Entry<Platform> {
match self {
Self::File(file) => Self::File(file.clone()),
Self::Dir(dir) => Self::Dir(dir.clone()),
Self::Symlink(symlink) => Self::Symlink(symlink.clone()),
}
}
}
Expand All @@ -881,6 +991,14 @@ pub(crate) struct FileX {
unique_id: usize,
}

type Symlink<Platform> = Arc<sync::RwLock<Platform, SymlinkX>>;

pub(crate) struct SymlinkX {
perms: Permissions,
target: String,
unique_id: usize,
}

#[derive(Clone, Debug)]
struct Permissions {
mode: Mode,
Expand Down
49 changes: 45 additions & 4 deletions litebox/src/fs/layered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! An layered file system, layering on [`FileSystem`](super::FileSystem) on top of another.

use alloc::string::String;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicUsize, Ordering::SeqCst};
Expand All @@ -16,7 +16,8 @@ use crate::sync;

use super::errors::{
ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError,
ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError,
ReadDirError, ReadError, ReadlinkError, RmdirError, SeekError, SymlinkError, TruncateError,
UnlinkError, WriteError,
};
use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence};

Expand Down Expand Up @@ -148,7 +149,7 @@ impl<Platform: sync::RawSyncPrimitivesProvider, Upper: super::FileSystem, Lower:
},
}
}
Ok(FileType::RegularFile | FileType::CharacterDevice)
Ok(FileType::RegularFile | FileType::CharacterDevice | FileType::SymbolicLink)
| Err(PathError::MissingComponent) => unreachable!(),
Err(PathError::ComponentNotADirectory) => unimplemented!(),
Err(PathError::InvalidPathname) => unreachable!("we just confirmed valid path"),
Expand Down Expand Up @@ -1019,7 +1020,7 @@ impl<
// We must now check if the lower level contains the file; if it does not, we
// must exit with failure. Otherwise, we fallthrough to place the tombstone.
match self.ensure_lower_contains(&path)? {
FileType::RegularFile => {
FileType::RegularFile | FileType::SymbolicLink => {
// fallthrough
}
FileType::Directory => {
Expand Down Expand Up @@ -1167,6 +1168,46 @@ impl<
Ok(())
}

fn symlink(
&self,
target: impl crate::path::Arg,
linkpath: impl crate::path::Arg,
) -> Result<(), SymlinkError> {
let linkpath_str = linkpath.as_rust_str()?.to_string();
// Migrate parent directory to upper layer if needed
if let Some(parent_path) = linkpath_str.rsplit_once('/').map(|(p, _)| p)
&& !parent_path.is_empty()
{
self.mkdir_migrating_ancestor_dirs(parent_path)
.map_err(|e| match e {
MkdirError::AlreadyExists => {
SymlinkError::PathError(PathError::NoSuchFileOrDirectory)
} // This shouldn't happen
MkdirError::NoWritePerms => SymlinkError::NoWritePerms,
MkdirError::ReadOnlyFileSystem => SymlinkError::ReadOnlyFileSystem,
MkdirError::PathError(p) => SymlinkError::PathError(p),
})?;
}
// Create symlink in upper layer
self.upper.symlink(target, &linkpath_str)
}

fn readlink(
&self,
path: impl crate::path::Arg,
) -> Result<alloc::string::String, ReadlinkError> {
let path_str = path.as_rust_str()?.to_string();
// Try upper layer first
match self.upper.readlink(&path_str) {
Ok(target) => Ok(target),
Err(ReadlinkError::PathError(PathError::NoSuchFileOrDirectory)) => {
// Try lower layer
self.lower.readlink(&path_str)
}
Err(e) => Err(e),
}
}

fn read_dir(&self, fd: &FileFd<Platform, Upper, Lower>) -> Result<Vec<DirEntry>, ReadDirError> {
let (entry, path) = self
.litebox
Expand Down
Loading