Skip to content
Merged
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
22 changes: 22 additions & 0 deletions litebox/src/broker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,13 @@ pub(crate) trait BrokerControl: Send + Sync {
user: FileUser,
) -> core::result::Result<core::result::Result<(), FileError>, BrokerControlError>;

fn rename_file(
&self,
old_path: &str,
new_path: &str,
user: FileUser,
) -> core::result::Result<core::result::Result<(), FileError>, BrokerControlError>;

fn mkdir_file(
&self,
path: &str,
Expand Down Expand Up @@ -1580,6 +1587,21 @@ where
self.request(|local| local.unlink_file(lease.sequence(), path, user))
}

fn rename_file(
&self,
old_path: &str,
new_path: &str,
user: FileUser,
) -> core::result::Result<core::result::Result<(), FileError>, BrokerControlError> {
let length = old_path
.len()
.checked_add(new_path.len())
.filter(|&length| length <= SHARED_BUFFER_SLOT_SIZE as usize)
.ok_or(BrokerControlError::Broker(ErrorCode::ResourceExhausted))?;
let lease = self.acquire_shared_buffer(length)?;
self.request(|local| local.rename_file(lease.sequence(), old_path, new_path, user))
}

fn mkdir_file(
&self,
path: &str,
Expand Down
26 changes: 26 additions & 0 deletions litebox/src/fs/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,32 @@ pub enum UnlinkError {
PathError(#[from] PathError),
}

/// Possible errors from [`crate::LiteBox::rename_file`].
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum RenameError {
#[error("the parent directory does not allow write permission")]
NoWritePerms,
#[error("the path is in use by the system")]
Busy,
#[error("paths are on different filesystems")]
CrossDevice,
#[error("invalid rename argument")]
InvalidArgument,
#[error("the destination is a directory but the source is not")]
IsADirectory,
#[error("the source is a directory but the destination is not")]
NotADirectory,
#[error("the destination directory is not empty")]
NotEmpty,
#[error("the named file resides on a read-only filesystem")]
ReadOnlyFileSystem,
#[error("I/O error")]
Io,
#[error(transparent)]
PathError(#[from] PathError),
}

/// Possible errors from [`crate::LiteBox::mkdir_file`].
#[non_exhaustive]
#[derive(Error, Debug)]
Expand Down
57 changes: 55 additions & 2 deletions litebox/src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ use litebox_platform::buffer::{Buffer, ReadBuffer, WriteBuffer};

use super::errors::{
ChmodError, ChownError, FilePathError, FileStatusError, IsTerminalError, MkdirError, OpenError,
PathError, ReadDirError, ReadError, RmdirError, SeekError, StatusFlagsError, SymlinkError,
TerminalControlError, TruncateError, UnlinkError, WalkError, WriteError,
PathError, ReadDirError, ReadError, RenameError, RmdirError, SeekError, StatusFlagsError,
SymlinkError, TerminalControlError, TruncateError, UnlinkError, WalkError, WriteError,
};

impl<Platform: sync::RawSyncPrimitivesProvider + TimeProvider> LiteBox<Platform> {
Expand Down Expand Up @@ -140,6 +140,40 @@ impl<Platform: sync::RawSyncPrimitivesProvider + TimeProvider> LiteBox<Platform>
.map_err(UnlinkError::from)
}

/// Rename a file or directory, replacing an existing destination when permitted.
pub fn rename_file(
&self,
context: &Context,
old_path: &str,
new_path: &str,
) -> Result<(), RenameError> {
if old_path.is_empty() || new_path.is_empty() {
return Err(PathError::NoSuchFileOrDirectory.into());
}
// Not normalized: dot components and trailing slashes are meaningful to rename.
let absolute_path = |path: &str| {
if path.starts_with('/') {
return String::from(path);
}
// XXX(jayb): cwd is a path snapshot and goes stale if an ancestor is renamed.
let mut absolute = context.cwd().to_string();
if !absolute.ends_with('/') {
absolute.push('/');
}
absolute.push_str(path);
absolute
};
self.broker_control()
.ok_or(RenameError::Io)?
.rename_file(
&absolute_path(old_path),
&absolute_path(new_path),
context.acting_user(),
)
.map_err(|_| RenameError::Io)?
.map_err(RenameError::from)
}

/// Create a new directory.
pub fn mkdir_file(&self, context: &Context, path: &str, mode: Mode) -> Result<(), MkdirError> {
let path = Self::broker_path(context, path);
Expand Down Expand Up @@ -823,6 +857,25 @@ impl From<FileError> for UnlinkError {
}
}

impl From<FileError> for RenameError {
fn from(error: FileError) -> Self {
if let Some(error) = path_error(error) {
return error.into();
}
match error {
FileError::NoWritePermissions => Self::NoWritePerms,
FileError::Busy => Self::Busy,
FileError::CrossDevice => Self::CrossDevice,
FileError::InvalidArgument => Self::InvalidArgument,
FileError::IsDirectory => Self::IsADirectory,
FileError::NotDirectory => Self::NotADirectory,
FileError::NotEmpty => Self::NotEmpty,
FileError::ReadOnlyFs => Self::ReadOnlyFileSystem,
_ => Self::Io,
}
}
}

impl From<FileError> for MkdirError {
fn from(error: FileError) -> Self {
if let Some(error) = path_error(error) {
Expand Down
13 changes: 12 additions & 1 deletion litebox_broker_core/src/fs/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use litebox_platform::buffer::{ReadBuffer, WriteBuffer};

use super::errors::{
ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError,
RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError, WriteError,
RenameError, RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError, WriteError,
};
use super::{DirEntry, FileStatus, FileType, Mode, OFlags, UserInfo};
use crate::readiness::ReadinessSource;
Expand Down Expand Up @@ -220,6 +220,17 @@ pub trait Backend: Send + Sync + Any {
// XXX(jayb): I don't like that unlink and rmdir exist separately, we should probably merge them.
fn rmdir_at(&self, dir: DirHandle, name: &str) -> Result<(), RmdirError>;

/// Atomically move an entry, replacing a compatible destination without following final
/// symbolic links. Backends must reject directory cycles and preserve open file contents.
/// Directory handles retain the backend's documented object or path-locator semantics.
fn rename_at(
&self,
old_dir: DirHandle,
old_name: &str,
new_dir: DirHandle,
new_name: &str,
) -> Result<(), RenameError>;

/// Update the permissions for the file/dir `h` refers to.
fn chmod(&self, h: HandleRef<'_>, mode: Mode) -> Result<(), ChmodError>;

Expand Down
47 changes: 46 additions & 1 deletion litebox_broker_core/src/fs/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use super::backend::{
};
use super::errors::{
ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError,
ReadError, RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError, WriteError,
ReadError, RenameError, RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError,
WriteError,
};
use super::inode_allocator::{InodeAllocator, InodeAllocators};
use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo};
Expand Down Expand Up @@ -866,6 +867,50 @@ impl Backend for Composer {
}
}

fn rename_at(
&self,
old_dir: DirHandle,
old_name: &str,
new_dir: DirHandle,
new_name: &str,
) -> Result<(), RenameError> {
if [old_name, new_name].iter().any(|name| {
name.is_empty() || matches!(*name, "." | "..") || name.contains(['/', '\0'])
}) {
return Err(PathError::InvalidPathname.into());
}
let old_dir = old_dir.into_typed::<Self>().inner;
let new_dir = new_dir.into_typed::<Self>().inner;
for (dir, name) in [(&old_dir, old_name), (&new_dir, new_name)] {
let path = match dir {
ComposerDirHandleInner::Virtual { path }
| ComposerDirHandleInner::Mounted { path, .. } => path,
};
self.checked_child_path(path.clone(), name, RenameError::Busy)?;
}
match (old_dir, new_dir) {
(
ComposerDirHandleInner::Mounted {
mount_index: old_mount,
handle: old_handle,
..
},
ComposerDirHandleInner::Mounted {
mount_index: new_mount,
handle: new_handle,
..
},
) if old_mount == new_mount => self.mounts[old_mount]
.backend
.rename_at(old_handle, old_name, new_handle, new_name),
(ComposerDirHandleInner::Virtual { .. }, ComposerDirHandleInner::Virtual { .. }) => {
Err(RenameError::ReadOnlyFileSystem)
}
// Different mounts are EXDEV even if they share a backend device.
_ => Err(RenameError::CrossDevice),
}
}

fn chmod(&self, h: HandleRef<'_>, mode: Mode) -> Result<(), ChmodError> {
match h {
HandleRef::File(h) => {
Expand Down
13 changes: 12 additions & 1 deletion litebox_broker_core/src/fs/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use super::backend::{
};
use super::errors::{
ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError,
ReadError, RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError, WriteError,
ReadError, RenameError, RmdirError, SymlinkError, TruncateError, UnlinkError, WalkError,
WriteError,
};
use super::inode_allocator::InodeAllocator;
use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo};
Expand Down Expand Up @@ -619,6 +620,16 @@ impl Backend for Devices {
Err(UnlinkError::ReadOnlyFileSystem)
}

fn rename_at(
&self,
_old_dir: DirHandle,
_old_name: &str,
_new_dir: DirHandle,
_new_name: &str,
) -> Result<(), RenameError> {
Err(RenameError::ReadOnlyFileSystem)
}

fn rmdir_at(&self, _dir: DirHandle, _name: &str) -> Result<(), RmdirError> {
Err(RmdirError::ReadOnlyFileSystem)
}
Expand Down
25 changes: 25 additions & 0 deletions litebox_broker_core/src/fs/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,31 @@ pub enum SymlinkError {
PathError(#[from] PathError),
}

/// Possible errors from [`super::resolver::Resolver::rename`].
#[derive(Error, Debug)]
pub enum RenameError {
#[error("a parent directory does not allow write permission")]
NoWritePerms,
#[error("the path is a root, mount point, or otherwise busy")]
Busy,
#[error("the paths cannot be renamed across filesystems")]
CrossDevice,
#[error("a directory cannot be moved into itself or its descendants")]
InvalidArgument,
#[error("a non-directory cannot replace a directory")]
IsADirectory,
#[error("a directory cannot replace a non-directory")]
NotADirectory,
#[error("the destination directory is not empty")]
NotEmpty,
#[error("the named file resides on a read-only filesystem")]
ReadOnlyFileSystem,
#[error("I/O error")]
Io,
#[error(transparent)]
PathError(#[from] PathError),
}

/// Possible errors from [`super::resolver::Resolver::mkdir`]
#[derive(Error, Debug)]
pub enum MkdirError {
Expand Down
Loading
Loading