use std::{
fs::{self, OpenOptions},
io::{ErrorKind, Write},
path::Path,
time::SystemTime,
};
use super::error::MountError;
use crate::{MontyObject, UnicodeErrorData, codecs, dir_stat, file_stat};
pub(super) struct MountContext<'a> {
pub mount_virtual: &'a str,
pub mount_host: &'a Path,
pub write_bytes_used: &'a mut u64,
pub write_bytes_limit: Option<u64>,
}
pub(super) fn read_text_fs(path: &Path, vpath: &str) -> Result<MontyObject, MountError> {
reject_directory(path, vpath)?;
let bytes = fs::read(path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
let content = bytes_to_utf8(bytes)?;
Ok(MontyObject::String(content))
}
pub(super) fn read_bytes_fs(path: &Path, vpath: &str) -> Result<MontyObject, MountError> {
reject_directory(path, vpath)?;
let content = fs::read(path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
Ok(MontyObject::Bytes(content))
}
pub(super) fn write_text_fs(path: &Path, content: &str, vpath: &str) -> Result<MontyObject, MountError> {
reject_directory(path, vpath)?;
fs::write(path, content).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
Ok(MontyObject::Int(
i64::try_from(content.chars().count()).unwrap_or(i64::MAX),
))
}
pub(super) fn write_bytes_fs(path: &Path, content: &[u8], vpath: &str) -> Result<MontyObject, MountError> {
reject_directory(path, vpath)?;
fs::write(path, content).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
Ok(MontyObject::Int(i64::try_from(content.len()).unwrap_or(i64::MAX)))
}
pub(super) fn append_text_fs(path: &Path, content: &str, vpath: &str) -> Result<MontyObject, MountError> {
append_bytes_to_file(path, content.as_bytes(), vpath)?;
Ok(MontyObject::Int(
i64::try_from(content.chars().count()).unwrap_or(i64::MAX),
))
}
pub(super) fn append_bytes_fs(path: &Path, content: &[u8], vpath: &str) -> Result<MontyObject, MountError> {
append_bytes_to_file(path, content, vpath)?;
Ok(MontyObject::Int(i64::try_from(content.len()).unwrap_or(i64::MAX)))
}
fn append_bytes_to_file(path: &Path, content: &[u8], vpath: &str) -> Result<(), MountError> {
reject_directory(path, vpath)?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|err| MountError::Io(err, vpath.to_owned()))?;
file.write_all(content)
.map_err(|err| MountError::Io(err, vpath.to_owned()))
}
pub(super) fn mkdir_fs(path: &Path, parents: bool, exist_ok: bool, vpath: &str) -> Result<MontyObject, MountError> {
let result = if parents {
match path.symlink_metadata() {
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(_) => {} }
fs::create_dir_all(path)
} else {
fs::create_dir(path)
};
match result {
Ok(()) => Ok(MontyObject::None),
Err(err) if err.kind() == ErrorKind::AlreadyExists && exist_ok && path.is_dir() => Ok(MontyObject::None),
Err(err) => Err(MountError::Io(err, vpath.to_owned())),
}
}
pub(super) fn unlink_fs(path: &Path, vpath: &str) -> Result<MontyObject, MountError> {
fs::remove_file(path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
Ok(MontyObject::None)
}
pub(super) fn rmdir_fs(path: &Path, vpath: &str) -> Result<MontyObject, MountError> {
fs::remove_dir(path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
Ok(MontyObject::None)
}
pub(super) fn stat_fs(path: &Path, vpath: &str) -> Result<MontyObject, MountError> {
let metadata = fs::metadata(path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
let mtime = metadata
.modified()
.unwrap_or(SystemTime::UNIX_EPOCH)
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |duration| duration.as_secs_f64());
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 iterdir_fs(host_path: &Path, vpath: &str, mount_host_path: &Path) -> Result<MontyObject, MountError> {
let mut result = Vec::new();
for name in list_visible_real_dir_entry_names(host_path, mount_host_path, vpath)? {
result.push(MontyObject::Path(format_child_path(vpath, &name)));
}
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 list_visible_real_dir_entry_names(
host_path: &Path,
mount_host_path: &Path,
vpath: &str,
) -> Result<Vec<String>, MountError> {
let read_dir = fs::read_dir(host_path).map_err(|err| MountError::Io(err, vpath.to_owned()))?;
let mut names = Vec::new();
for entry in read_dir {
let entry = entry.map_err(|err| MountError::Io(err, vpath.to_owned()))?;
let file_type = entry.file_type().map_err(|err| MountError::Io(err, vpath.to_owned()))?;
if file_type.is_symlink() {
match fs::canonicalize(entry.path()) {
Ok(canonical) if !canonical.starts_with(mount_host_path) => continue,
Err(_) => continue,
_ => {}
}
}
names.push(entry.file_name().to_string_lossy().to_string());
}
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 = codecs::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 dir_mtime(path: &Path) -> f64 {
fs::metadata(path)
.and_then(|metadata| metadata.modified())
.unwrap_or_else(|_| SystemTime::now())
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |duration| duration.as_secs_f64())
}
pub(super) fn reject_directory(path: &Path, vpath: &str) -> Result<(), MountError> {
if path.is_dir() {
return Err(MountError::io_err(ErrorKind::IsADirectory, "Is a directory", vpath));
}
Ok(())
}
pub(super) fn format_child_path(parent: &str, child: &str) -> String {
if parent.ends_with('/') {
format!("{parent}{child}")
} else {
format!("{parent}/{child}")
}
}