use std::{
ffi::OsStr,
io::{Error as IoError, ErrorKind, Read, Write},
path::{Path, PathBuf},
time::SystemTime,
};
#[cfg(unix)]
use cap_std::fs::OpenOptionsExt;
use cap_std::{
fs::{Dir, File, Metadata, OpenOptions},
time::SystemTime as CapSystemTime,
};
use monty_types::{MontyObject, UnicodeErrorData, dir_stat, file_stat, utf8_error_reason};
#[cfg(unix)]
use rustix::fs::OFlags;
use super::error::MountError;
pub(super) const LISTING_ENTRY_MEMORY_USAGE: u64 = 128;
pub(super) fn as_u64(bytes: usize) -> u64 {
u64::try_from(bytes).unwrap_or(u64::MAX)
}
#[derive(Clone, Copy)]
pub(super) struct MemoryBudget {
pub available: u64,
pub limit: u64,
}
impl MemoryBudget {
pub fn full(limit: u64) -> Self {
Self {
available: limit,
limit,
}
}
pub fn check(self, bytes: u64) -> Result<(), MountError> {
if bytes > self.available {
Err(MountError::MemoryUsageLimitExceeded(self.limit))
} else {
Ok(())
}
}
pub fn shrink(self, bytes: u64) -> Result<Self, MountError> {
match self.available.checked_sub(bytes) {
Some(available) => Ok(Self { available, ..self }),
None => Err(MountError::MemoryUsageLimitExceeded(self.limit)),
}
}
pub fn halved(self) -> Self {
Self {
available: self.available / 2,
..self
}
}
}
pub(super) struct MountContext<'a> {
pub mount_virtual: &'a str,
pub mount_dir: &'a Dir,
pub write_bytes_used: &'a mut u64,
pub write_bytes_limit: Option<u64>,
pub memory_usage_limit: u64,
}
pub(super) fn host_read_text(
dir: &Dir,
rel: &str,
vpath: &str,
budget: MemoryBudget,
) -> Result<MontyObject, MountError> {
let bytes = read_file_limited(dir, rel, vpath, budget)?;
let content = bytes_to_utf8(bytes)?;
Ok(MontyObject::String(content))
}
pub(super) fn host_read_bytes(
dir: &Dir,
rel: &str,
vpath: &str,
budget: MemoryBudget,
) -> Result<MontyObject, MountError> {
Ok(MontyObject::Bytes(read_file_limited(dir, rel, vpath, budget)?))
}
fn read_file_limited(dir: &Dir, rel: &str, vpath: &str, budget: MemoryBudget) -> Result<Vec<u8>, MountError> {
reject_non_regular(dir, rel, vpath)?;
let file = open_regular(dir, rel, vpath, OpenOptions::new().read(true))?;
let meta_len = file.metadata().map_err(|err| map_io(err, vpath))?.len();
budget.check(meta_len)?;
let mut content = Vec::with_capacity(usize::try_from(meta_len).unwrap_or(0));
file.take(budget.available.saturating_add(1))
.read_to_end(&mut content)
.map_err(|err| map_io(err, vpath))?;
budget.check(as_u64(content.len()))?;
Ok(content)
}
pub(super) fn host_write_text(dir: &Dir, rel: &str, content: &str, vpath: &str) -> Result<MontyObject, MountError> {
write_bytes_to_file(dir, rel, content.as_bytes(), vpath)?;
Ok(MontyObject::Int(
i64::try_from(content.chars().count()).unwrap_or(i64::MAX),
))
}
pub(super) fn host_write_bytes(dir: &Dir, rel: &str, content: &[u8], vpath: &str) -> Result<MontyObject, MountError> {
write_bytes_to_file(dir, rel, content, vpath)?;
Ok(MontyObject::Int(i64::try_from(content.len()).unwrap_or(i64::MAX)))
}
fn write_bytes_to_file(dir: &Dir, rel: &str, content: &[u8], vpath: &str) -> Result<(), MountError> {
reject_non_regular(dir, rel, vpath)?;
let mut file = open_regular(
dir,
rel,
vpath,
OpenOptions::new().write(true).create(true).truncate(true),
)?;
file.write_all(content).map_err(|err| map_io(err, vpath))
}
pub(super) fn host_append_text(dir: &Dir, rel: &str, content: &str, vpath: &str) -> Result<MontyObject, MountError> {
append_bytes_to_file(dir, rel, content.as_bytes(), vpath)?;
Ok(MontyObject::Int(
i64::try_from(content.chars().count()).unwrap_or(i64::MAX),
))
}
pub(super) fn host_append_bytes(dir: &Dir, rel: &str, content: &[u8], vpath: &str) -> Result<MontyObject, MountError> {
append_bytes_to_file(dir, rel, content, vpath)?;
Ok(MontyObject::Int(i64::try_from(content.len()).unwrap_or(i64::MAX)))
}
fn append_bytes_to_file(dir: &Dir, rel: &str, content: &[u8], vpath: &str) -> Result<(), MountError> {
reject_non_regular(dir, rel, vpath)?;
let mut file = open_regular(dir, rel, vpath, OpenOptions::new().create(true).append(true))?;
file.write_all(content).map_err(|err| map_io(err, vpath))
}
fn open_regular(dir: &Dir, rel: &str, vpath: &str, options: &mut OpenOptions) -> Result<File, MountError> {
let file = dir
.open_with(rel, non_blocking(options))
.map_err(|err| map_io(err, vpath))?;
let metadata = file.metadata().map_err(|err| map_io(err, vpath))?;
if metadata.is_file() {
Ok(file)
} else if metadata.is_dir() {
Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath))
} else {
Err(MountError::io_err(
ErrorKind::PermissionDenied,
"Permission denied",
vpath,
))
}
}
#[cfg(unix)]
fn non_blocking(options: &mut OpenOptions) -> &mut OpenOptions {
options.custom_flags(OFlags::NONBLOCK.bits().cast_signed())
}
#[cfg(not(unix))]
fn non_blocking(options: &mut OpenOptions) -> &mut OpenOptions {
options
}
pub(super) fn host_mkdir(
dir: &Dir,
rel: &str,
parents: bool,
exist_ok: bool,
vpath: &str,
) -> Result<MontyObject, MountError> {
let result = if parents {
match dir.metadata(rel) {
Ok(meta) if meta.is_dir() => {
return if exist_ok {
Ok(MontyObject::None)
} else {
Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath))
};
}
Ok(_) => {
return Err(MountError::io_err(ErrorKind::AlreadyExists, "File exists", vpath));
}
Err(_) => {} }
dir.create_dir_all(rel)
} else {
dir.create_dir(rel)
};
match result {
Ok(()) => Ok(MontyObject::None),
Err(err) if err.kind() == ErrorKind::AlreadyExists && exist_ok && host_is_dir(dir, rel) => {
Ok(MontyObject::None)
}
Err(err) => Err(map_io(err, vpath)),
}
}
pub(super) fn host_unlink(dir: &Dir, rel: &str, vpath: &str) -> Result<MontyObject, MountError> {
dir.remove_file(rel).map_err(|err| map_io(err, vpath))?;
Ok(MontyObject::None)
}
pub(super) fn host_rmdir(dir: &Dir, rel: &str, vpath: &str) -> Result<MontyObject, MountError> {
dir.remove_dir(rel).map_err(|err| map_io(err, vpath))?;
Ok(MontyObject::None)
}
pub(super) fn host_stat(dir: &Dir, rel: &str, vpath: &str) -> Result<MontyObject, MountError> {
let metadata = dir.metadata(rel).map_err(|err| map_io(err, vpath))?;
let mtime = mtime_secs(&metadata);
let size = i64::try_from(metadata.len()).unwrap_or(i64::MAX);
if metadata.is_dir() {
Ok(dir_stat(0o755, mtime))
} else {
Ok(file_stat(0o644, size, mtime))
}
}
pub(super) fn host_iterdir(dir: &Dir, rel: &str, vpath: &str, budget: MemoryBudget) -> Result<MontyObject, MountError> {
let names = host_list_visible_dir_entry_names(dir, rel, vpath, budget.halved())?;
let mut memory_usage = names.iter().fold(0_u64, |usage, name| {
usage
.saturating_add(as_u64(name.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE)
});
let mut result = Vec::new();
for name in names {
let path = format_child_path(vpath, &name);
memory_usage = memory_usage
.saturating_add(as_u64(path.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE);
budget.check(memory_usage)?;
result.push(MontyObject::Path(path));
}
Ok(MontyObject::List(result))
}
pub(super) fn check_write_limit(bytes: usize, ctx: &MountContext<'_>) -> Result<(), MountError> {
if let Some(limit) = ctx.write_bytes_limit {
let bytes = u64::try_from(bytes).unwrap_or(u64::MAX);
if (*ctx.write_bytes_used).saturating_add(bytes) > limit {
return Err(MountError::WriteLimitExceeded(limit));
}
}
Ok(())
}
pub(super) fn commit_write_bytes(bytes: usize, ctx: &mut MountContext<'_>) {
if ctx.write_bytes_limit.is_some() {
*ctx.write_bytes_used = (*ctx.write_bytes_used).saturating_add(u64::try_from(bytes).unwrap_or(u64::MAX));
}
}
pub(super) fn host_list_visible_dir_entry_names(
dir: &Dir,
rel: &str,
vpath: &str,
budget: MemoryBudget,
) -> Result<Vec<String>, MountError> {
dir.metadata(rel).map_err(|err| map_io(err, vpath))?;
let read_dir = dir.read_dir(rel).map_err(|err| map_io(err, vpath))?;
let mut names = Vec::new();
let mut memory_usage = 0_u64;
for entry in read_dir {
let entry = entry.map_err(|err| map_io(err, vpath))?;
let file_type = entry.file_type().map_err(|err| map_io(err, vpath))?;
if file_type.is_symlink() {
let child = join_mount_relative_os(rel, &entry.file_name());
if dir.metadata(&child).is_err() {
continue;
}
}
let name = entry.file_name().to_string_lossy().to_string();
memory_usage = memory_usage
.saturating_add(as_u64(name.len()))
.saturating_add(LISTING_ENTRY_MEMORY_USAGE);
budget.check(memory_usage)?;
names.push(name);
}
Ok(names)
}
pub(super) fn bytes_to_utf8(bytes: Vec<u8>) -> Result<String, MountError> {
String::from_utf8(bytes).map_err(|err| {
let utf8_error = err.utf8_error();
let start = utf8_error.valid_up_to();
let end = utf8_error.error_len().map_or(err.as_bytes().len(), |len| start + len);
let reason = utf8_error_reason(err.as_bytes()[start], utf8_error.error_len());
MountError::InvalidUtf8 {
start,
end,
first_byte: err.as_bytes()[start],
reason,
data: UnicodeErrorData::decode("utf-8", err.as_bytes(), start, end, reason),
}
})
}
pub(super) fn current_timestamp() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |duration| duration.as_secs_f64())
}
pub(super) fn mtime_secs(metadata: &Metadata) -> f64 {
metadata
.modified()
.map_or(SystemTime::UNIX_EPOCH, CapSystemTime::into_std)
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |duration| duration.as_secs_f64())
}
pub(super) fn host_dir_mtime(dir: &Dir, rel: &str) -> f64 {
dir.metadata(rel)
.and_then(|metadata| metadata.modified())
.map_or_else(|_| SystemTime::now(), CapSystemTime::into_std)
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |duration| duration.as_secs_f64())
}
pub(super) fn join_mount_relative_os(rel: &str, child: &OsStr) -> PathBuf {
if rel.is_empty() || rel == "." {
PathBuf::from(child)
} else {
Path::new(rel).join(child)
}
}
pub(super) fn join_mount_relative(rel: &str, child: &str) -> String {
if rel.is_empty() || rel == "." {
child.to_owned()
} else {
format!("{rel}/{child}")
}
}
pub(super) fn host_is_dir(dir: &Dir, rel: &str) -> bool {
dir.metadata(rel).is_ok_and(|meta| meta.is_dir())
}
pub(super) fn host_is_file(dir: &Dir, rel: &str) -> bool {
dir.metadata(rel).is_ok_and(|meta| meta.is_file())
}
pub(super) fn map_io(err: IoError, vpath: &str) -> MountError {
if err.kind() == ErrorKind::PermissionDenied && err.raw_os_error().is_none() {
MountError::PathEscape {
virtual_path: vpath.to_owned(),
}
} else {
MountError::Io(err, vpath.to_owned())
}
}
pub(super) fn reject_non_regular(dir: &Dir, rel: &str, vpath: &str) -> Result<(), MountError> {
match dir.metadata(rel) {
Ok(meta) if meta.is_dir() => Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath)),
Ok(meta) if !meta.is_file() => Err(MountError::io_err(
ErrorKind::PermissionDenied,
"Permission denied",
vpath,
)),
_ => Ok(()),
}
}
pub(super) fn format_child_path(parent: &str, child: &str) -> String {
if parent.ends_with('/') {
format!("{parent}{child}")
} else {
format!("{parent}/{child}")
}
}