use std::io::ErrorKind;
use ahash::AHashSet;
use cap_std::fs::Dir;
use monty_types::{FileMode, MontyObject, dir_stat, file_stat};
use super::{
common::{
LISTING_ENTRY_MEMORY_USAGE, MemoryBudget, MountContext, as_u64, bytes_to_utf8, check_write_limit,
commit_write_bytes, current_timestamp, format_child_path, host_dir_mtime, host_is_dir, host_is_file,
host_list_visible_dir_entry_names, host_read_bytes, host_read_text, host_stat, join_mount_relative, map_io,
},
dispatch::{FsRequest, file_handle_result},
error::MountError,
overlay_state::{ENTRY_MEMORY_USAGE, OverlayEntry, OverlayFile, OverlayFileRef, OverlayState},
path_security::{
MountRelativePath, normalize_virtual_path, reject_drive_or_unc_segments, reject_null_bytes,
resolve_virtual_path, strip_mount_prefix,
},
};
const REAL_DESCENDANT_MEMORY_USAGE: u64 = 512;
fn relative_path(path: &str, ctx: &MountContext<'_>) -> Result<String, MountError> {
reject_null_bytes(path)?;
let normalized = normalize_virtual_path(path);
let relative = strip_mount_prefix(&normalized, ctx.mount_virtual)
.map(str::to_owned)
.ok_or_else(|| MountError::NoMountPoint(path.to_owned()))?;
reject_drive_or_unc_segments(&relative, &normalized)?;
Ok(relative)
}
fn resolve_real(vpath: &str, ctx: &MountContext<'_>) -> Result<MountRelativePath, MountError> {
let target = resolve_virtual_path(vpath, ctx.mount_virtual)?;
reject_symlink_chain(ctx.mount_dir, target.for_dir_op(), vpath)?;
Ok(target)
}
fn reject_symlink_chain(dir: &Dir, rel: &str, vpath: &str) -> Result<(), MountError> {
if rel.is_empty() || rel == "." {
return Ok(());
}
let mut walk = ComponentWalk::new(dir);
for component in rel.split('/') {
match walk.step(component, vpath)? {
RealTarget::Symlink => {
return Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
});
}
RealTarget::Absent | RealTarget::File => return Ok(()),
RealTarget::Dir => {}
}
}
Ok(())
}
fn checked_ref_path<'r>(
file_ref: &'r OverlayFileRef,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<&'r str, MountError> {
reject_symlink_chain(ctx.mount_dir, &file_ref.relative, vpath)?;
Ok(&file_ref.relative)
}
fn available_memory(state: &OverlayState, ctx: &MountContext<'_>) -> Result<MemoryBudget, MountError> {
let available = ctx
.memory_usage_limit
.checked_sub(state.memory_usage())
.ok_or(MountError::MemoryUsageLimitExceeded(ctx.memory_usage_limit))?;
Ok(MemoryBudget {
available,
limit: ctx.memory_usage_limit,
})
}
pub(super) fn execute(
request: FsRequest,
ctx: &mut MountContext<'_>,
state: &mut OverlayState,
) -> Result<MontyObject, MountError> {
match request {
FsRequest::Exists { path } => exists(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::IsFile { path } => is_file(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::IsDir { path } => is_dir(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::IsSymlink { path } => Ok(is_symlink(state, &relative_path(&path, ctx)?, ctx, &path)),
FsRequest::ReadText { path } => read_text(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::ReadBytes { path } => read_bytes(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::WriteText { path, data } => write_text(state, &path, data, ctx),
FsRequest::WriteBytes { path, data } => write_bytes(state, &path, data, ctx),
FsRequest::AppendText { path, data } => append_text(state, &path, &data, ctx),
FsRequest::AppendBytes { path, data } => append_bytes(state, &path, &data, ctx),
FsRequest::Mkdir {
path,
parents,
exist_ok,
} => mkdir(state, &relative_path(&path, ctx)?, parents, exist_ok, ctx, &path),
FsRequest::Unlink { path } => unlink(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::Rmdir { path } => rmdir(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::Iterdir { path } => iterdir(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::Stat { path } => stat(state, &relative_path(&path, ctx)?, ctx, &path),
FsRequest::Rename { src, dst } => rename(state, &src, &dst, ctx),
FsRequest::Resolve { path } | FsRequest::Absolute { path } => {
Ok(MontyObject::Path(normalize_virtual_path(&path)))
}
FsRequest::Open { path, mode } => open(state, &path, mode, ctx),
}
}
fn open(
state: &mut OverlayState,
path: &str,
file_mode: FileMode,
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
match file_mode {
FileMode::Read(_) | FileMode::ReadUpdate(_) => {
let relative = relative_path(path, ctx)?;
match state.get(&relative) {
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {}
Some(OverlayEntry::Directory { .. }) => {
return Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", path));
}
Some(OverlayEntry::Deleted) => return Err(MountError::not_found(path)),
None => match resolve_real_path_state(path, ctx, OnLookupFailure::Propagate)? {
RealPathState::Present(rel) if host_is_dir(ctx.mount_dir, &rel) => {
return Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", path));
}
RealPathState::Present(_) => {}
RealPathState::Missing => return Err(MountError::not_found(path)),
},
}
}
FileMode::Write(_) | FileMode::WriteUpdate(_) => {
write_text(state, path, String::new(), ctx)?;
}
FileMode::Append(_) | FileMode::AppendUpdate(_) => {
ensure_append_target_exists(state, path, ctx)?;
}
}
Ok(file_handle_result(path, file_mode))
}
fn ensure_append_target_exists(
state: &mut OverlayState,
vpath: &str,
ctx: &mut MountContext<'_>,
) -> Result<(), MountError> {
let relative = relative_path(vpath, ctx)?;
match state.get(&relative) {
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => Ok(()),
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => {
ensure_parent_exists(state, &relative, ctx, vpath)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: Vec::new(),
mtime: current_timestamp(),
}),
ctx.memory_usage_limit,
)?;
Ok(())
}
None => {
let target = resolve_real(vpath, ctx)?;
match classify_target(ctx.mount_dir, target.for_dir_op(), vpath)? {
RealTarget::Dir => Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath)),
RealTarget::Symlink => Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
}),
RealTarget::File => ensure_parent_exists(state, &relative, ctx, vpath),
RealTarget::Absent => {
ensure_parent_exists(state, &relative, ctx, vpath)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: Vec::new(),
mtime: current_timestamp(),
}),
ctx.memory_usage_limit,
)?;
Ok(())
}
}
}
}
}
fn exists(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
let exists = match state.get(relative) {
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_) | OverlayEntry::Directory { .. }) => true,
Some(OverlayEntry::Deleted) => false,
None => match resolve_real_path_state(vpath, ctx, OnLookupFailure::Missing)? {
RealPathState::Present(_) => true,
RealPathState::Missing => false,
},
};
Ok(MontyObject::Bool(exists))
}
fn is_file(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
let is_file = match state.get(relative) {
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => true,
Some(OverlayEntry::Directory { .. } | OverlayEntry::Deleted) => false,
None => match resolve_real_path_state(vpath, ctx, OnLookupFailure::Missing)? {
RealPathState::Present(rel) => host_is_file(ctx.mount_dir, &rel),
RealPathState::Missing => false,
},
};
Ok(MontyObject::Bool(is_file))
}
fn is_dir(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
let is_dir = match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => true,
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_) | OverlayEntry::Deleted) => false,
None => match resolve_real_path_state(vpath, ctx, OnLookupFailure::Missing)? {
RealPathState::Present(rel) => host_is_dir(ctx.mount_dir, &rel),
RealPathState::Missing => false,
},
};
Ok(MontyObject::Bool(is_dir))
}
fn is_symlink(state: &OverlayState, relative: &str, ctx: &MountContext<'_>, vpath: &str) -> MontyObject {
let is_symlink = match state.get(relative) {
Some(_) => false,
None => resolve_virtual_path(vpath, ctx.mount_virtual).is_ok_and(|target| {
let rel = target.for_dir_op();
let parent = rel.rsplit_once('/').map_or("", |(parent, _)| parent);
reject_symlink_chain(ctx.mount_dir, parent, vpath).is_ok()
&& ctx.mount_dir.symlink_metadata(rel).is_ok_and(|meta| meta.is_symlink())
}),
};
MontyObject::Bool(is_symlink)
}
fn read_text(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => {
available_memory(state, ctx)?.check(as_u64(file.content.len()))?;
Ok(MontyObject::String(bytes_to_utf8(file.content.clone())?))
}
Some(OverlayEntry::RealFileRef(file_ref)) => {
let rel = checked_ref_path(file_ref, ctx, vpath)?;
host_read_text(ctx.mount_dir, rel, vpath, available_memory(state, ctx)?)
}
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
let target = resolve_real(vpath, ctx)?;
host_read_text(ctx.mount_dir, target.for_dir_op(), vpath, available_memory(state, ctx)?)
}
}
}
fn read_bytes(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => {
available_memory(state, ctx)?.check(as_u64(file.content.len()))?;
Ok(MontyObject::Bytes(file.content.clone()))
}
Some(OverlayEntry::RealFileRef(file_ref)) => {
let rel = checked_ref_path(file_ref, ctx, vpath)?;
host_read_bytes(ctx.mount_dir, rel, vpath, available_memory(state, ctx)?)
}
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
let target = resolve_real(vpath, ctx)?;
host_read_bytes(ctx.mount_dir, target.for_dir_op(), vpath, available_memory(state, ctx)?)
}
}
}
fn write_text(
state: &mut OverlayState,
vpath: &str,
data: String,
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
let char_count = data.chars().count();
let byte_len = data.len();
check_write_limit(byte_len, ctx)?;
let relative = relative_path(vpath, ctx)?;
ensure_parent_exists(state, &relative, ctx, vpath)?;
reject_directory_target(state, &relative, ctx, vpath)?;
state.check_file_replacement(&relative, byte_len, ctx.memory_usage_limit)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: data.into_bytes(),
mtime: current_timestamp(),
}),
ctx.memory_usage_limit,
)?;
commit_write_bytes(byte_len, ctx);
Ok(MontyObject::Int(i64::try_from(char_count).unwrap_or(i64::MAX)))
}
fn write_bytes(
state: &mut OverlayState,
vpath: &str,
data: Vec<u8>,
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
let byte_len = data.len();
check_write_limit(byte_len, ctx)?;
let relative = relative_path(vpath, ctx)?;
ensure_parent_exists(state, &relative, ctx, vpath)?;
reject_directory_target(state, &relative, ctx, vpath)?;
state.check_file_replacement(&relative, byte_len, ctx.memory_usage_limit)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: data,
mtime: current_timestamp(),
}),
ctx.memory_usage_limit,
)?;
commit_write_bytes(byte_len, ctx);
Ok(MontyObject::Int(i64::try_from(byte_len).unwrap_or(i64::MAX)))
}
fn append_text(
state: &mut OverlayState,
vpath: &str,
data: &str,
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
append_bytes(state, vpath, data.as_bytes(), ctx)?;
Ok(MontyObject::Int(
i64::try_from(data.chars().count()).unwrap_or(i64::MAX),
))
}
fn append_bytes(
state: &mut OverlayState,
vpath: &str,
data: &[u8],
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
let relative = relative_path(vpath, ctx)?;
ensure_parent_exists(state, &relative, ctx, vpath)?;
reject_directory_target(state, &relative, ctx, vpath)?;
let target_is_overlay_file = matches!(state.get(&relative), Some(OverlayEntry::File(_)));
let existing_len = existing_file_len(state, &relative, ctx, vpath)?;
let charged_bytes = if ctx.write_bytes_limit.is_some() && !target_is_overlay_file {
existing_len.saturating_add(data.len())
} else {
data.len()
};
check_write_limit(charged_bytes, ctx)?;
if !state.append_file(&relative, data, current_timestamp(), ctx.memory_usage_limit)? {
let final_len = existing_len.saturating_add(data.len());
state.check_file_replacement(&relative, final_len, ctx.memory_usage_limit)?;
let budget = available_memory(state, ctx)?;
budget.check(as_u64(final_len))?;
let mut content = existing_file_bytes(state, &relative, ctx, vpath, budget.shrink(as_u64(data.len()))?)?;
content.extend_from_slice(data);
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content,
mtime: current_timestamp(),
}),
ctx.memory_usage_limit,
)?;
}
commit_write_bytes(charged_bytes, ctx);
Ok(MontyObject::Int(i64::try_from(data.len()).unwrap_or(i64::MAX)))
}
fn existing_file_len(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<usize, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => Ok(file.content.len()),
Some(OverlayEntry::Deleted) => Ok(0),
Some(OverlayEntry::RealFileRef(file_ref)) => {
file_len(ctx.mount_dir, checked_ref_path(file_ref, ctx, vpath)?, vpath)
}
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
None => match resolve_real_path_state(vpath, ctx, OnLookupFailure::Propagate)? {
RealPathState::Present(rel) => file_len(ctx.mount_dir, &rel, vpath),
RealPathState::Missing => Ok(0),
},
}
}
fn file_len(dir: &Dir, rel: &str, vpath: &str) -> Result<usize, MountError> {
let len = dir.metadata(rel).map_err(|error| map_io(error, vpath))?.len();
Ok(usize::try_from(len).unwrap_or(usize::MAX))
}
fn existing_file_bytes(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
budget: MemoryBudget,
) -> Result<Vec<u8>, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => {
budget.check(as_u64(file.content.len()))?;
Ok(file.content.clone())
}
Some(OverlayEntry::Deleted) => Ok(Vec::new()),
Some(OverlayEntry::RealFileRef(file_ref)) => {
let rel = checked_ref_path(file_ref, ctx, vpath)?;
match host_read_bytes(ctx.mount_dir, rel, vpath, budget)? {
MontyObject::Bytes(bytes) => Ok(bytes),
_ => unreachable!("host_read_bytes should return bytes"),
}
}
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
None => match resolve_real_path_state(vpath, ctx, OnLookupFailure::Propagate)? {
RealPathState::Present(rel) => match host_read_bytes(ctx.mount_dir, &rel, vpath, budget)? {
MontyObject::Bytes(bytes) => Ok(bytes),
_ => unreachable!("host_read_bytes should return bytes"),
},
RealPathState::Missing => Ok(Vec::new()),
},
}
}
fn reject_directory_target(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<(), MountError> {
match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(_) => Ok(()),
None => {
let target = resolve_real(vpath, ctx)?;
match classify_target(ctx.mount_dir, target.for_dir_op(), vpath)? {
RealTarget::Dir => Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath)),
RealTarget::Symlink => Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
}),
RealTarget::File | RealTarget::Absent => Ok(()),
}
}
}
}
fn ensure_parent_exists(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<(), MountError> {
let Some((parents, _)) = relative.rsplit_once('/') else {
return Ok(());
};
let mut walk = ComponentWalk::new(ctx.mount_dir);
let mut current = String::new();
for component in parents.split('/') {
if !current.is_empty() {
current.push('/');
}
current.push_str(component);
let real = walk.step(component, vpath)?;
match state.get(¤t) {
Some(OverlayEntry::Directory { .. }) => {}
Some(_) => return Err(MountError::not_found(vpath)),
None => match real {
RealTarget::Dir => {}
RealTarget::Symlink => {
return Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
});
}
RealTarget::File | RealTarget::Absent => return Err(MountError::not_found(vpath)),
},
}
}
Ok(())
}
fn mkdir(
state: &mut OverlayState,
relative: &str,
parents: bool,
exist_ok: bool,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => {
return if exist_ok {
Ok(MontyObject::None)
} else {
Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath))
};
}
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
return Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath));
}
Some(OverlayEntry::Deleted) => {}
None => {
let target = resolve_real(vpath, ctx)?;
match classify_target(ctx.mount_dir, target.for_dir_op(), vpath)? {
RealTarget::Dir if exist_ok => return Ok(MontyObject::None),
RealTarget::Dir | RealTarget::File => {
return Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath));
}
RealTarget::Symlink => {
return Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
});
}
RealTarget::Absent => {}
}
}
}
if parents {
create_overlay_parents(state, relative, ctx)?;
} else {
ensure_parent_exists(state, relative, ctx, vpath)?;
}
state.insert(
relative.to_owned(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
ctx.memory_usage_limit,
)?;
Ok(MontyObject::None)
}
fn create_overlay_parents(state: &mut OverlayState, relative: &str, ctx: &MountContext<'_>) -> Result<(), MountError> {
let mut walk = ComponentWalk::new(ctx.mount_dir);
let mut current = String::new();
for component in relative.split('/') {
if !current.is_empty() {
current.push('/');
}
current.push_str(component);
let current_vpath = format_child_path(ctx.mount_virtual, ¤t);
let real = walk.step(component, ¤t_vpath)?;
match state.get(¤t) {
Some(OverlayEntry::Directory { .. }) => {}
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
return Err(MountError::io_err(
ErrorKind::NotADirectory,
"Not a directory",
¤t_vpath,
));
}
Some(OverlayEntry::Deleted) => {
state.insert(
current.clone(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
ctx.memory_usage_limit,
)?;
}
None => {
match real {
RealTarget::Dir => continue,
RealTarget::Symlink => {
return Err(MountError::PathEscape {
virtual_path: current_vpath,
});
}
RealTarget::File => {
return Err(MountError::io_err(
ErrorKind::NotADirectory,
"Not a directory",
¤t_vpath,
));
}
RealTarget::Absent => {}
}
state.insert(
current.clone(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
ctx.memory_usage_limit,
)?;
}
}
}
Ok(())
}
fn unlink(
state: &mut OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
state.insert(relative.to_owned(), OverlayEntry::Deleted, ctx.memory_usage_limit)?;
Ok(MontyObject::None)
}
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
ensure_parent_exists(state, relative, ctx, vpath)?;
let resolved = resolve_real(vpath, ctx)?;
match classify_target(ctx.mount_dir, resolved.for_dir_op(), vpath)? {
RealTarget::File => {
state.insert(relative.to_owned(), OverlayEntry::Deleted, ctx.memory_usage_limit)?;
Ok(MontyObject::None)
}
RealTarget::Dir => Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath)),
RealTarget::Symlink => Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
}),
RealTarget::Absent => Err(MountError::not_found(vpath)),
}
}
}
}
fn rmdir(
state: &mut OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
reject_mount_root(relative, vpath)?;
match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => {
if overlay_directory_has_children(state, relative) {
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
vpath,
));
}
state.insert(relative.to_owned(), OverlayEntry::Deleted, ctx.memory_usage_limit)?;
Ok(MontyObject::None)
}
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
Err(MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
ensure_parent_exists(state, relative, ctx, vpath)?;
let resolved = resolve_real(vpath, ctx)?;
let rel = resolved.for_dir_op();
if !host_is_dir(ctx.mount_dir, rel) {
return Err(if ctx.mount_dir.exists(rel) {
MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath)
} else {
MountError::not_found(vpath)
});
}
if real_directory_has_visible_children(state, relative, ctx.mount_dir, resolved.for_dir_op(), vpath)? {
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
vpath,
));
}
if overlay_directory_has_children(state, relative) {
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
vpath,
));
}
state.insert(relative.to_owned(), OverlayEntry::Deleted, ctx.memory_usage_limit)?;
Ok(MontyObject::None)
}
}
}
fn overlay_directory_has_children(state: &OverlayState, relative: &str) -> bool {
let prefix = directory_prefix(relative);
state
.prefix_iter(&prefix)
.any(|(path, entry)| path != relative && !matches!(entry, OverlayEntry::Deleted))
}
fn real_directory_has_visible_children(
state: &OverlayState,
relative: &str,
dir: &Dir,
rel: &str,
vpath: &str,
) -> Result<bool, MountError> {
let prefix = directory_prefix(relative);
let entries = dir.read_dir(rel).map_err(|err| map_io(err, vpath))?;
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let child_rel = if prefix.is_empty() {
name
} else {
format!("{prefix}{name}")
};
if !matches!(state.get(&child_rel), Some(OverlayEntry::Deleted)) {
return Ok(true);
}
}
Ok(false)
}
fn stat(state: &OverlayState, relative: &str, ctx: &MountContext<'_>, vpath: &str) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => {
let size = i64::try_from(file.content.len()).unwrap_or(i64::MAX);
Ok(file_stat(0o644, size, file.mtime))
}
Some(OverlayEntry::RealFileRef(file_ref)) => {
checked_ref_path(file_ref, ctx, vpath)?;
Ok(file_stat(0o644, file_ref.size, file_ref.mtime))
}
Some(OverlayEntry::Directory { mtime }) => Ok(dir_stat(0o755, *mtime)),
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
let target = resolve_real(vpath, ctx)?;
host_stat(ctx.mount_dir, target.for_dir_op(), vpath)
}
}
}
fn iterdir(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
let host_dir_to_merge = match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => None,
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
return Err(MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath));
}
Some(OverlayEntry::Deleted) => return Err(MountError::not_found(vpath)),
None => match resolve_real(vpath, ctx) {
Ok(target) if host_is_dir(ctx.mount_dir, target.for_dir_op()) => Some(target.for_dir_op().to_owned()),
Ok(target) if !ctx.mount_dir.exists(target.for_dir_op()) => return Err(MountError::not_found(vpath)),
Ok(_) => return Err(MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath)),
Err(err) => return Err(err),
},
};
let prefix = directory_prefix(relative);
let mut seen_names: AHashSet<String> = AHashSet::new();
let mut entries = Vec::new();
let budget = available_memory(state, ctx)?;
let mut transient_usage = 0_u64;
for (path, entry) in state.prefix_iter(&prefix) {
let rest = &path[prefix.len()..];
if rest.is_empty() || rest.contains('/') {
continue;
}
let child_name = rest.to_owned();
transient_usage = transient_usage
.saturating_add(as_u64(child_name.len().saturating_mul(2)))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE);
budget.check(transient_usage)?;
seen_names.insert(child_name.clone());
if !matches!(entry, OverlayEntry::Deleted) {
let child_path = format_child_path(vpath, &child_name);
transient_usage = transient_usage
.saturating_add(as_u64(child_path.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE);
budget.check(transient_usage)?;
entries.push(MontyObject::Path(child_path));
}
}
if let Some(host_dir) = host_dir_to_merge {
let remaining = budget.shrink(transient_usage)?;
let names = match host_list_visible_dir_entry_names(ctx.mount_dir, &host_dir, vpath, remaining.halved()) {
Ok(names) => names,
Err(error @ MountError::MemoryUsageLimitExceeded(_)) => return Err(error),
Err(_) => Vec::new(),
};
if !names.is_empty() {
transient_usage = names.iter().fold(transient_usage, |usage, name| {
usage
.saturating_add(as_u64(name.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE)
});
for name in names {
if !seen_names.contains(&name) {
let child_path = format_child_path(vpath, &name);
transient_usage = transient_usage
.saturating_add(as_u64(child_path.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE);
budget.check(transient_usage)?;
entries.push(MontyObject::Path(child_path));
}
}
}
}
Ok(MontyObject::List(entries))
}
fn rename(
state: &mut OverlayState,
src_vpath: &str,
dst_vpath: &str,
ctx: &MountContext<'_>,
) -> Result<MontyObject, MountError> {
let src_rel = relative_path(src_vpath, ctx)?;
let dst_rel = relative_path(dst_vpath, ctx)?;
reject_mount_root(&src_rel, src_vpath)?;
reject_mount_root(&dst_rel, dst_vpath)?;
ensure_parent_exists(state, &dst_rel, ctx, dst_vpath)?;
if state.get(&dst_rel).is_none() {
resolve_real(dst_vpath, ctx)?;
}
ensure_parent_exists(state, &src_rel, ctx, src_vpath)?;
if matches!(state.get(&src_rel), Some(OverlayEntry::Deleted)) {
return Err(MountError::not_found(src_vpath));
}
let src_is_dir = match state.get(&src_rel) {
Some(OverlayEntry::Directory { .. }) => true,
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => false,
Some(OverlayEntry::Deleted) => return Err(MountError::not_found(src_vpath)),
None => host_is_dir(ctx.mount_dir, resolve_real(src_vpath, ctx)?.for_dir_op()),
};
reject_rename_type_mismatch(state, &dst_rel, src_is_dir, ctx, dst_vpath)?;
if src_is_dir {
reject_rename_onto_nonempty_dir(state, &dst_rel, ctx, dst_vpath)?;
}
if src_is_dir {
let src_prefix = format!("{src_rel}/");
if dst_rel.starts_with(&src_prefix) {
return Err(MountError::io_err(
ErrorKind::InvalidInput,
"Invalid argument",
src_vpath,
));
}
}
let source_is_overlay = state.get(&src_rel).is_some();
let real_source_entry = if source_is_overlay {
None
} else {
let target = resolve_real(src_vpath, ctx)?;
let rel = target.for_dir_op();
let entry = match classify_target(ctx.mount_dir, rel, src_vpath)? {
RealTarget::File => OverlayFileRef::from_relative(ctx.mount_dir, rel)
.map(OverlayEntry::RealFileRef)
.ok_or_else(|| MountError::not_found(src_vpath))?,
RealTarget::Dir => OverlayEntry::Directory {
mtime: host_dir_mtime(ctx.mount_dir, rel),
},
RealTarget::Symlink => {
return Err(MountError::PathEscape {
virtual_path: src_vpath.to_owned(),
});
}
RealTarget::Absent => return Err(MountError::not_found(src_vpath)),
};
Some(entry)
};
let source_entry = state
.get(&src_rel)
.or(real_source_entry.as_ref())
.expect("rename source was resolved above");
let mut overlay_moves = Vec::new();
let mut real_moves = Vec::new();
if src_is_dir {
let src_prefix = format!("{src_rel}/");
let dst_prefix = format!("{dst_rel}/");
let plan_usage = state.prefix_iter(&src_prefix).fold(0_u64, |usage, (key, _)| {
let suffix_len = key.len().saturating_sub(src_prefix.len());
usage
.saturating_add(as_u64(key.len().saturating_mul(2)))
.saturating_add(as_u64(dst_prefix.len().saturating_add(suffix_len)))
.saturating_add(ENTRY_MEMORY_USAGE)
});
let remaining = available_memory(state, ctx)?.shrink(plan_usage)?;
let child_keys: Vec<String> = state.prefix_iter(&src_prefix).map(|(key, _)| key.to_owned()).collect();
let handled_keys: AHashSet<String> = child_keys.iter().cloned().collect();
for key in child_keys {
let new_key = format!("{dst_prefix}{}", &key[src_prefix.len()..]);
overlay_moves.push((key, new_key));
}
if let Ok(target) = resolve_real(src_vpath, ctx)
&& host_is_dir(ctx.mount_dir, target.for_dir_op())
{
let real_children = match collect_real_descendants(
ctx.mount_dir,
target.for_dir_op(),
&src_prefix,
state,
&handled_keys,
src_vpath,
remaining,
) {
Ok(children) => children,
Err(error) => match &error {
MountError::MemoryUsageLimitExceeded(_) | MountError::PathEscape { .. } => return Err(error),
MountError::Io(io_error, _) if io_error.kind() == ErrorKind::InvalidData => {
return Err(error);
}
_ => Vec::new(),
},
};
for (old_rel, child_entry) in real_children {
let new_rel = format!("{dst_prefix}{}", old_rel.strip_prefix(&src_prefix).unwrap_or(&old_rel));
real_moves.push((old_rel, new_rel, child_entry));
}
}
}
let deleted = OverlayEntry::Deleted;
let mut replacements = vec![(src_rel.as_str(), &deleted), (dst_rel.as_str(), source_entry)];
for (old_rel, new_rel) in &overlay_moves {
replacements.push((old_rel, &deleted));
replacements.push((new_rel, state.get(old_rel).expect("overlay descendant still exists")));
}
for (old_rel, new_rel, entry) in &real_moves {
replacements.push((old_rel, &deleted));
replacements.push((new_rel, entry));
}
state.check_replacements(replacements, ctx.memory_usage_limit)?;
let entry = if source_is_overlay {
state.remove(&src_rel).expect("overlay rename source still exists")
} else {
real_source_entry.expect("real rename source was captured")
};
let descendants: Vec<(String, String, OverlayEntry)> = overlay_moves
.into_iter()
.map(|(old_rel, new_rel)| {
let entry = state.remove(&old_rel).expect("overlay rename descendant still exists");
(old_rel, new_rel, entry)
})
.chain(real_moves)
.collect();
state.insert_unchecked(src_rel, OverlayEntry::Deleted);
state.insert_unchecked(dst_rel, entry);
for (old_rel, new_rel, child) in descendants {
state.insert_unchecked(old_rel, OverlayEntry::Deleted);
state.insert_unchecked(new_rel, child);
}
Ok(MontyObject::None)
}
fn reject_mount_root(relative: &str, vpath: &str) -> Result<(), MountError> {
if relative.is_empty() {
Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
})
} else {
Ok(())
}
}
fn reject_rename_type_mismatch(
state: &OverlayState,
dst_rel: &str,
src_is_dir: bool,
ctx: &MountContext<'_>,
dst_vpath: &str,
) -> Result<(), MountError> {
let dst_is_dir = match state.get(dst_rel) {
Some(OverlayEntry::Directory { .. }) => Some(true),
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => Some(false),
Some(OverlayEntry::Deleted) => None,
None => match resolve_virtual_path(dst_vpath, ctx.mount_virtual) {
Ok(target) if host_is_dir(ctx.mount_dir, target.for_dir_op()) => Some(true),
Ok(target) if ctx.mount_dir.exists(target.for_dir_op()) => Some(false),
_ => None,
},
};
match dst_is_dir {
Some(true) if !src_is_dir => Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", dst_vpath)),
Some(false) if src_is_dir => Err(MountError::io_err(
ErrorKind::NotADirectory,
"Not a directory",
dst_vpath,
)),
_ => Ok(()),
}
}
fn reject_rename_onto_nonempty_dir(
state: &OverlayState,
dst_rel: &str,
ctx: &MountContext<'_>,
dst_vpath: &str,
) -> Result<(), MountError> {
let dst_is_dir = match state.get(dst_rel) {
Some(OverlayEntry::Directory { .. }) => true,
Some(OverlayEntry::Deleted | OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => return Ok(()),
None => match resolve_virtual_path(dst_vpath, ctx.mount_virtual) {
Ok(target) if host_is_dir(ctx.mount_dir, target.for_dir_op()) => true,
_ => return Ok(()),
},
};
if !dst_is_dir {
return Ok(());
}
if overlay_directory_has_children(state, dst_rel) {
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
dst_vpath,
));
}
if let Ok(target) = resolve_virtual_path(dst_vpath, ctx.mount_virtual)
&& host_is_dir(ctx.mount_dir, target.for_dir_op())
&& real_directory_has_visible_children(state, dst_rel, ctx.mount_dir, target.for_dir_op(), dst_vpath)?
{
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
dst_vpath,
));
}
Ok(())
}
fn collect_real_descendants(
dir: &Dir,
root_rel: &str,
prefix: &str,
state: &OverlayState,
already_handled: &AHashSet<String>,
vpath: &str,
budget: MemoryBudget,
) -> Result<Vec<(String, OverlayEntry)>, MountError> {
let mut result = Vec::new();
let mut dirs = vec![(root_rel.to_owned(), prefix.to_owned())];
let mut memory_usage = as_u64(root_rel.len().saturating_add(prefix.len()));
while let Some((current_rel, rel_prefix)) = dirs.pop() {
let entries = dir.read_dir(¤t_rel).map_err(|error| map_io(error, vpath))?;
for entry in entries {
let entry = entry.map_err(|error| map_io(error, vpath))?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
return Err(MountError::io_err(
ErrorKind::InvalidData,
"directory contains an entry with a non-UTF-8 name",
vpath,
));
};
let rel_key = format!("{rel_prefix}{name}");
if state.get(&rel_key).is_some() || already_handled.contains(&rel_key) {
continue;
}
let file_type = entry
.file_type()
.map_err(|error| MountError::Io(error, vpath.to_owned()))?;
if file_type.is_symlink() {
return Err(MountError::PathEscape {
virtual_path: vpath.to_owned(),
});
}
let child_rel = join_mount_relative(¤t_rel, name);
if file_type.is_file() {
if let Some(file_ref) = OverlayFileRef::from_relative(dir, &child_rel) {
memory_usage = memory_usage
.saturating_add(as_u64(rel_key.len()))
.saturating_add(as_u64(file_ref.relative.len()))
.saturating_add(REAL_DESCENDANT_MEMORY_USAGE);
budget.check(memory_usage)?;
result.push((rel_key, OverlayEntry::RealFileRef(file_ref)));
}
} else if file_type.is_dir() {
memory_usage = memory_usage
.saturating_add(as_u64(rel_key.len().saturating_mul(2)))
.saturating_add(as_u64(child_rel.len()))
.saturating_add(REAL_DESCENDANT_MEMORY_USAGE);
budget.check(memory_usage)?;
result.push((
rel_key.clone(),
OverlayEntry::Directory {
mtime: host_dir_mtime(dir, &child_rel),
},
));
dirs.push((child_rel, format!("{rel_key}/")));
}
}
}
Ok(result)
}
fn resolve_real_path_state(
vpath: &str,
ctx: &MountContext<'_>,
on_failure: OnLookupFailure,
) -> Result<RealPathState, MountError> {
let target = match resolve_real(vpath, ctx) {
Ok(target) => target,
Err(_) if matches!(on_failure, OnLookupFailure::Missing) => return Ok(RealPathState::Missing),
Err(err) => return Err(err),
};
let rel = target.for_dir_op();
match ctx.mount_dir.metadata(rel) {
Ok(_) => Ok(RealPathState::Present(rel.to_owned())),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(RealPathState::Missing),
Err(_) if matches!(on_failure, OnLookupFailure::Missing) => Ok(RealPathState::Missing),
Err(err) => Err(map_io(err, vpath)),
}
}
#[derive(Clone, Copy)]
enum OnLookupFailure {
Missing,
Propagate,
}
enum RealPathState {
Present(String),
Missing,
}
enum RealTarget {
Dir,
File,
Symlink,
Absent,
}
struct ComponentWalk<'d> {
root: &'d Dir,
current: Option<Dir>,
exhausted: bool,
}
impl<'d> ComponentWalk<'d> {
fn new(root: &'d Dir) -> Self {
Self {
root,
current: None,
exhausted: false,
}
}
fn step(&mut self, component: &str, vpath: &str) -> Result<RealTarget, MountError> {
if self.exhausted {
return Ok(RealTarget::Absent);
}
let target = classify_target(self.here(), component, vpath)?;
if matches!(target, RealTarget::Dir) {
let opened = self.here().open_dir(component);
match opened {
Ok(next) => self.current = Some(next),
Err(err) if err.kind() == ErrorKind::NotFound => {
self.exhausted = true;
return Ok(RealTarget::Absent);
}
Err(err) => return Err(map_io(err, vpath)),
}
} else {
self.exhausted = true;
}
Ok(target)
}
fn here(&self) -> &Dir {
self.current.as_ref().unwrap_or(self.root)
}
}
fn classify_target(dir: &Dir, rel: &str, vpath: &str) -> Result<RealTarget, MountError> {
match dir.symlink_metadata(rel) {
Ok(meta) if meta.is_symlink() => Ok(RealTarget::Symlink),
Ok(meta) if meta.is_dir() => Ok(RealTarget::Dir),
Ok(_) => Ok(RealTarget::File),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(RealTarget::Absent),
Err(err) => Err(map_io(err, vpath)),
}
}
fn directory_prefix(relative: &str) -> String {
if relative.is_empty() {
String::new()
} else {
format!("{relative}/")
}
}