use std::{
fs,
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use ahash::AHashSet;
use super::{
common::{
MountContext, bytes_to_utf8, check_write_limit, commit_write_bytes, current_timestamp, dir_mtime,
format_child_path, list_visible_real_dir_entry_names, read_bytes_fs, read_text_fs, stat_fs,
},
dispatch::{FsRequest, file_handle_result},
error::MountError,
overlay_state::{OverlayEntry, OverlayFile, OverlayFileRef, OverlayState},
path_security::{
ResolveMode, normalize_virtual_path, reject_escaping_symlink, reject_overlong_path, resolve_path,
strip_mount_prefix,
},
};
use crate::{MontyObject, dir_stat, file_stat, types::file::FileMode};
fn relative_path(path: &str, ctx: &MountContext<'_>) -> Result<String, MountError> {
let normalized = normalize_virtual_path(path);
reject_overlong_path(&normalized, path)?;
strip_mount_prefix(&normalized, ctx.mount_virtual)
.map(str::to_owned)
.ok_or_else(|| MountError::NoMountPoint(path.to_owned()))
}
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 } => 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, ResolveMode::Existing)? {
RealPathState::Present(host_path) if host_path.is_dir() => {
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, "", 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(),
}),
);
Ok(())
}
None => match resolve_real_path_state(vpath, ctx, ResolveMode::Existing)? {
RealPathState::Present(host_path) if host_path.is_dir() => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
RealPathState::Present(_) => Ok(()),
RealPathState::Missing => {
ensure_parent_exists(state, &relative, ctx, vpath)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: Vec::new(),
mtime: current_timestamp(),
}),
);
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, ResolveMode::Existing)? {
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, ResolveMode::Existing)? {
RealPathState::Present(host_path) => host_path.is_file(),
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, ResolveMode::Existing)? {
RealPathState::Present(host_path) => host_path.is_dir(),
RealPathState::Missing => false,
},
};
Ok(MontyObject::Bool(is_dir))
}
fn is_symlink(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
let is_symlink = match state.get(relative) {
Some(_) => false,
None => match resolve_real_path_state(vpath, ctx, ResolveMode::Lstat)? {
RealPathState::Present(host_path) => host_path.is_symlink(),
RealPathState::Missing => false,
},
};
Ok(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)) => Ok(MontyObject::String(bytes_to_utf8(file.content.clone())?)),
Some(OverlayEntry::RealFileRef(file_ref)) => read_text_fs(&file_ref.host_path, vpath),
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
let resolved = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)?;
read_text_fs(&resolved.host_path, vpath)
}
}
}
fn read_bytes(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => Ok(MontyObject::Bytes(file.content.clone())),
Some(OverlayEntry::RealFileRef(file_ref)) => read_bytes_fs(&file_ref.host_path, vpath),
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
Some(OverlayEntry::Deleted) => Err(MountError::not_found(vpath)),
None => {
let resolved = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)?;
read_bytes_fs(&resolved.host_path, vpath)
}
}
}
fn write_text(
state: &mut OverlayState,
vpath: &str,
data: &str,
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
check_write_limit(data.len(), ctx)?;
let relative = relative_path(vpath, ctx)?;
ensure_parent_exists(state, &relative, ctx, vpath)?;
reject_directory_target(state, &relative, ctx, vpath)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: data.as_bytes().to_vec(),
mtime: current_timestamp(),
}),
);
commit_write_bytes(data.len(), ctx);
Ok(MontyObject::Int(
i64::try_from(data.chars().count()).unwrap_or(i64::MAX),
))
}
fn write_bytes(
state: &mut OverlayState,
vpath: &str,
data: &[u8],
ctx: &mut MountContext<'_>,
) -> Result<MontyObject, MountError> {
check_write_limit(data.len(), ctx)?;
let relative = relative_path(vpath, ctx)?;
ensure_parent_exists(state, &relative, ctx, vpath)?;
reject_directory_target(state, &relative, ctx, vpath)?;
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content: data.to_vec(),
mtime: current_timestamp(),
}),
);
commit_write_bytes(data.len(), ctx);
Ok(MontyObject::Int(i64::try_from(data.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 charged_bytes = if ctx.write_bytes_limit.is_some() && !target_is_overlay_file {
existing_file_len(state, &relative, ctx, vpath)?.saturating_add(data.len())
} else {
data.len()
};
check_write_limit(charged_bytes, ctx)?;
if let Some(OverlayEntry::File(file)) = state.get_mut(&relative) {
file.content.extend_from_slice(data);
file.mtime = current_timestamp();
} else {
let mut content = existing_file_bytes(state, &relative, ctx, vpath)?;
content.extend_from_slice(data);
state.insert(
relative,
OverlayEntry::File(OverlayFile {
content,
mtime: current_timestamp(),
}),
);
}
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(&file_ref.host_path, vpath),
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
None => match resolve_real_path_state(vpath, ctx, ResolveMode::Existing)? {
RealPathState::Present(host_path) => file_len(&host_path, vpath),
RealPathState::Missing => Ok(0),
},
}
}
fn file_len(path: &Path, vpath: &str) -> Result<usize, MountError> {
let len = fs::metadata(path)
.map_err(|error| MountError::Io(error, vpath.to_owned()))?
.len();
Ok(usize::try_from(len).unwrap_or(usize::MAX))
}
fn existing_file_bytes(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<Vec<u8>, MountError> {
match state.get(relative) {
Some(OverlayEntry::File(file)) => Ok(file.content.clone()),
Some(OverlayEntry::Deleted) => Ok(Vec::new()),
Some(OverlayEntry::RealFileRef(file_ref)) => match read_bytes_fs(&file_ref.host_path, vpath)? {
MontyObject::Bytes(bytes) => Ok(bytes),
_ => unreachable!("read_bytes_fs should return bytes"),
},
Some(OverlayEntry::Directory { .. }) => {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
}
None => match resolve_real_path_state(vpath, ctx, ResolveMode::Existing)? {
RealPathState::Present(host_path) => match read_bytes_fs(&host_path, vpath)? {
MontyObject::Bytes(bytes) => Ok(bytes),
_ => unreachable!("read_bytes_fs should return bytes"),
},
RealPathState::Missing => Ok(Vec::new()),
},
}
}
fn reject_directory_target(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<(), MountError> {
if relative_dir_exists(state, relative, ctx) {
return Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath));
}
Ok(())
}
fn ensure_parent_exists(
state: &OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<(), MountError> {
if let Some((parent_rel, _)) = relative.rsplit_once('/')
&& !relative_dir_exists(state, parent_rel, ctx)
{
return Err(MountError::not_found(vpath));
}
Ok(())
}
fn relative_dir_exists(state: &OverlayState, relative: &str, ctx: &MountContext<'_>) -> bool {
match state.get(relative) {
Some(OverlayEntry::Directory { .. }) => true,
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_) | OverlayEntry::Deleted) => false,
None => {
let parent_vpath = format!("{}/{relative}", ctx.mount_virtual);
resolve_path(&parent_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)
.is_ok_and(|resolved| resolved.host_path.is_dir())
}
}
}
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 => {
if let Ok(resolved) = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)
&& let Ok(meta) = resolved.host_path.symlink_metadata()
{
return if meta.is_dir() && exist_ok {
Ok(MontyObject::None)
} else {
Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath))
};
}
}
}
if parents {
create_overlay_parents(state, relative, ctx)?;
} else if let Some((parent_rel, _)) = relative.rsplit_once('/')
&& !relative_dir_exists(state, parent_rel, ctx)
{
return Err(MountError::not_found(vpath));
}
state.insert(
relative.to_owned(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
);
Ok(MontyObject::None)
}
fn create_overlay_parents(state: &mut OverlayState, relative: &str, ctx: &MountContext<'_>) -> Result<(), MountError> {
let mut current = String::new();
for component in relative.split('/') {
if !current.is_empty() {
current.push('/');
}
current.push_str(component);
match state.get(¤t) {
Some(OverlayEntry::Directory { .. }) => {}
Some(OverlayEntry::File(_) | OverlayEntry::RealFileRef(_)) => {
let current_vpath = format!("{}/{current}", ctx.mount_virtual);
return Err(MountError::io_err(
ErrorKind::NotADirectory,
"Not a directory",
¤t_vpath,
));
}
Some(OverlayEntry::Deleted) => {
state.insert(
current.clone(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
);
}
None => {
let current_vpath = format!("{}/{current}", ctx.mount_virtual);
if let Ok(resolved) =
resolve_path(¤t_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)
{
if resolved.host_path.is_file() {
return Err(MountError::io_err(
ErrorKind::NotADirectory,
"Not a directory",
¤t_vpath,
));
}
if resolved.host_path.is_dir() {
continue;
}
}
state.insert(
current.clone(),
OverlayEntry::Directory {
mtime: current_timestamp(),
},
);
}
}
}
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);
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 => {
let resolved = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)?;
if resolved.host_path.is_file() {
state.insert(relative.to_owned(), OverlayEntry::Deleted);
Ok(MontyObject::None)
} else if resolved.host_path.is_dir() {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
} else {
Err(MountError::not_found(vpath))
}
}
}
}
fn rmdir(
state: &mut OverlayState,
relative: &str,
ctx: &MountContext<'_>,
vpath: &str,
) -> Result<MontyObject, MountError> {
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);
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 => {
let resolved = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)?;
if !resolved.host_path.is_dir() {
return Err(MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath));
}
if real_directory_has_visible_children(state, relative, &resolved.host_path, 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);
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,
host_path: &Path,
vpath: &str,
) -> Result<bool, MountError> {
let prefix = directory_prefix(relative);
let entries = fs::read_dir(host_path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
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)) => 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 resolved = resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)?;
stat_fs(&resolved.host_path, 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_path(vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing) {
Ok(resolved) if resolved.host_path.is_dir() => Some(resolved.host_path),
Ok(_) => return Err(MountError::io_err(ErrorKind::NotADirectory, "Not a directory", vpath)),
Err(MountError::Io(err, _)) if err.kind() == ErrorKind::NotFound => {
return Err(MountError::not_found(vpath));
}
Err(err) => return Err(err),
},
};
let prefix = directory_prefix(relative);
let mut seen_names: AHashSet<String> = AHashSet::new();
let mut entries = Vec::new();
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();
seen_names.insert(child_name.clone());
if !matches!(entry, OverlayEntry::Deleted) {
entries.push(MontyObject::Path(format_child_path(vpath, &child_name)));
}
}
if let Some(host_dir) = host_dir_to_merge
&& let Ok(names) = list_visible_real_dir_entry_names(&host_dir, ctx.mount_host, vpath)
{
for name in names {
if !seen_names.contains(&name) {
entries.push(MontyObject::Path(format_child_path(vpath, &name)));
}
}
}
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)?;
ensure_parent_exists(state, &dst_rel, ctx, dst_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 => resolve_path(src_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Lstat)
.is_ok_and(|r| r.host_path.is_dir()),
};
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)?;
}
let entry = if let Some(entry) = state.remove(&src_rel) {
entry
} else {
let resolved = resolve_path(src_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Lstat)?;
if resolved.host_path.is_symlink() {
reject_escaping_symlink(&resolved.host_path, ctx.mount_host, src_vpath)?;
OverlayFileRef::from_lstat(&resolved.host_path)
.map(OverlayEntry::RealFileRef)
.ok_or_else(|| MountError::not_found(src_vpath))?
} else if resolved.host_path.is_file() {
OverlayFileRef::from_host_path(&resolved.host_path)
.map(OverlayEntry::RealFileRef)
.ok_or_else(|| MountError::not_found(src_vpath))?
} else if resolved.host_path.is_dir() {
OverlayEntry::Directory {
mtime: dir_mtime(&resolved.host_path),
}
} else {
return Err(MountError::not_found(src_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 mut descendants: Vec<(String, OverlayEntry)> = Vec::new();
let mut tombstone_keys: Vec<String> = Vec::new();
if src_is_dir {
let src_prefix = format!("{src_rel}/");
let dst_prefix = format!("{dst_rel}/");
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 suffix = &key[src_prefix.len()..];
if let Some(child) = state.remove(&key) {
descendants.push((format!("{dst_prefix}{suffix}"), child));
tombstone_keys.push(key);
}
}
if let Ok(resolved) = resolve_path(src_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)
&& let Ok(real_children) = collect_real_descendants(&resolved.host_path, &src_prefix, state, &handled_keys)
{
for (old_rel, child_entry) in real_children {
let suffix = old_rel.strip_prefix(&src_prefix).unwrap_or(&old_rel);
descendants.push((format!("{dst_prefix}{suffix}"), child_entry));
tombstone_keys.push(old_rel);
}
}
}
state.insert(src_rel, OverlayEntry::Deleted);
state.insert(dst_rel, entry);
for key in tombstone_keys {
state.insert(key, OverlayEntry::Deleted);
}
for (key, child) in descendants {
state.insert(key, child);
}
Ok(MontyObject::None)
}
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 => {
match resolve_path(dst_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing) {
Ok(resolved) if resolved.host_path.is_dir() => Some(true),
Ok(resolved) if resolved.host_path.exists() => 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_path(dst_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing) {
Ok(resolved) if resolved.host_path.is_dir() => 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(resolved) = resolve_path(dst_vpath, ctx.mount_virtual, ctx.mount_host, ResolveMode::Existing)
&& real_directory_has_visible_children(state, dst_rel, &resolved.host_path, dst_vpath)?
{
return Err(MountError::io_err(
ErrorKind::DirectoryNotEmpty,
"Directory not empty",
dst_vpath,
));
}
Ok(())
}
fn collect_real_descendants(
host_dir: &Path,
prefix: &str,
state: &OverlayState,
already_handled: &AHashSet<String>,
) -> io::Result<Vec<(String, OverlayEntry)>> {
let mut result = Vec::new();
let mut dirs = vec![(host_dir.to_path_buf(), prefix.to_owned())];
while let Some((dir, rel_prefix)) = dirs.pop() {
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
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()?;
if file_type.is_symlink() {
continue;
}
if file_type.is_file() {
if let Some(file_ref) = OverlayFileRef::from_host_path(&entry.path()) {
result.push((rel_key, OverlayEntry::RealFileRef(file_ref)));
}
} else if file_type.is_dir() {
result.push((
rel_key.clone(),
OverlayEntry::Directory {
mtime: dir_mtime(&entry.path()),
},
));
dirs.push((entry.path(), format!("{rel_key}/")));
}
}
}
Ok(result)
}
fn resolve_real_path_state(
vpath: &str,
ctx: &MountContext<'_>,
mode: ResolveMode,
) -> Result<RealPathState, MountError> {
match resolve_path(vpath, ctx.mount_virtual, ctx.mount_host, mode) {
Ok(resolved) => Ok(RealPathState::Present(resolved.host_path)),
Err(MountError::Io(_, _)) => Ok(RealPathState::Missing),
Err(err) => Err(err),
}
}
enum RealPathState {
Present(PathBuf),
Missing,
}
fn directory_prefix(relative: &str) -> String {
if relative.is_empty() {
String::new()
} else {
format!("{relative}/")
}
}