use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
};
use cap_std::{ambient_authority, fs::Dir};
use monty_types::{MontyObject, OsFunctionCall};
use super::{
common::MountContext,
dispatch,
error::MountError,
mount_mode::MountMode,
path_security::{contains_null_byte, normalize_virtual_path, reject_overlong_path},
};
pub const DEFAULT_MEMORY_USAGE_LIMIT: u64 = 100_000_000;
#[derive(Debug)]
pub enum MountCallOutcome {
Handled(Result<MontyObject, MountError>),
NotHandled(OsFunctionCall),
}
#[derive(Debug, Default)]
pub struct MountTable {
mounts: Vec<Mount>,
}
impl MountTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn mount(
&mut self,
virtual_path: &str,
host_path: impl AsRef<Path>,
mode: MountMode,
write_bytes_limit: Option<u64>,
) -> Result<(), MountError> {
let mount = Mount::new(virtual_path, host_path, mode, write_bytes_limit)?;
self.push_mount(mount);
Ok(())
}
pub fn push_mount(&mut self, mount: Mount) {
let insert_at = self
.mounts
.partition_point(|existing| existing.virtual_path().len() > mount.virtual_path().len());
self.mounts.insert(insert_at, mount);
}
pub fn handle_os_call(&mut self, call: OsFunctionCall) -> MountCallOutcome {
if let Some(primary_path) = call.fs_primary_path() {
let rejection = reject_overlong_path(primary_path).err().or_else(|| {
contains_null_byte(primary_path)
.then(|| MountError::EmbeddedNullByte(call.embedded_null_message(false)))
});
if let Some(e) = rejection {
MountCallOutcome::Handled(if call.is_existence_check() {
Ok(MontyObject::Bool(false))
} else {
Err(e)
})
} else {
match self.route_call(primary_path, &call) {
Some(Ok(index)) => MountCallOutcome::Handled(self.mounts[index].execute(call)),
Some(Err(err)) => MountCallOutcome::Handled(Err(err)),
None => MountCallOutcome::NotHandled(call),
}
}
} else {
MountCallOutcome::NotHandled(call)
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.mounts.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.mounts.len()
}
fn route_call(&self, primary_path: &str, call: &OsFunctionCall) -> Option<Result<usize, MountError>> {
let src_mount_index = self.find_mount_index(primary_path);
if let Some(dst_path) = call.rename_destination() {
if let Err(e) = reject_overlong_path(dst_path) {
return Some(Err(e));
}
if contains_null_byte(dst_path) {
return Some(Err(MountError::EmbeddedNullByte(call.embedded_null_message(true))));
}
match (src_mount_index, self.find_mount_index(dst_path)) {
(None, None) => None,
(Some(src), Some(dst)) if src == dst => Some(Ok(src)),
_ => Some(Err(MountError::CrossMountRename {
src: primary_path.to_owned(),
dst: dst_path.to_owned(),
})),
}
} else {
src_mount_index.map(Ok)
}
}
fn find_mount_index(&self, virtual_path: &str) -> Option<usize> {
let normalized = normalize_virtual_path(virtual_path);
self.mounts
.iter()
.position(|mount| path_matches_mount(&normalized, mount.virtual_path()))
}
}
#[derive(Debug)]
pub struct Mount {
root: MountRoot,
mode: MountMode,
write_bytes_used: u64,
write_bytes_limit: Option<u64>,
memory_usage_limit: u64,
}
impl Mount {
pub fn new(
virtual_path: &str,
host_path: impl AsRef<Path>,
mode: MountMode,
write_bytes_limit: Option<u64>,
) -> Result<Self, MountError> {
Ok(Self::with_root(
MountRoot::open(virtual_path, host_path)?,
mode,
write_bytes_limit,
))
}
#[must_use]
pub fn with_root(root: MountRoot, mode: MountMode, write_bytes_limit: Option<u64>) -> Self {
Self {
root,
mode,
write_bytes_used: 0,
write_bytes_limit,
memory_usage_limit: DEFAULT_MEMORY_USAGE_LIMIT,
}
}
#[must_use]
pub fn root(&self) -> &MountRoot {
&self.root
}
#[must_use]
pub fn virtual_path(&self) -> &str {
self.root.virtual_path()
}
#[must_use]
pub fn host_path(&self) -> &Path {
self.root.host_path()
}
#[must_use]
pub fn mode(&self) -> &MountMode {
&self.mode
}
#[must_use]
pub fn write_bytes_limit(&self) -> Option<u64> {
self.write_bytes_limit
}
#[must_use]
pub fn memory_usage_limit(&self) -> u64 {
self.memory_usage_limit
}
#[must_use]
pub fn with_memory_usage_limit(mut self, limit: u64) -> Self {
self.memory_usage_limit = limit;
self
}
#[must_use]
pub fn memory_usage(&self) -> u64 {
match &self.mode {
MountMode::OverlayMemory(state) => state.memory_usage(),
MountMode::ReadWrite | MountMode::ReadOnly => 0,
}
}
#[must_use]
pub fn write_bytes_used(&self) -> u64 {
self.write_bytes_used
}
fn execute(&mut self, call: OsFunctionCall) -> Result<MontyObject, MountError> {
let mut ctx = MountContext {
mount_virtual: &self.root.virtual_path,
mount_dir: &self.root.dir,
write_bytes_used: &mut self.write_bytes_used,
write_bytes_limit: self.write_bytes_limit,
memory_usage_limit: self.memory_usage_limit,
};
dispatch::execute(dispatch::fs_request_from_call(call), &mut ctx, &mut self.mode)
}
}
#[derive(Debug, Clone)]
pub struct MountRoot {
virtual_path: String,
host_path: PathBuf,
dir: Arc<Dir>,
}
impl MountRoot {
pub fn open(virtual_path: &str, host_path: impl AsRef<Path>) -> Result<Self, MountError> {
let host_path = host_path.as_ref();
if !virtual_path.starts_with('/') {
return Err(MountError::InvalidMount(format!(
"virtual path must be absolute, got: '{virtual_path}'"
)));
}
let normalized_virtual = normalize_virtual_path(virtual_path);
let dir = Dir::open_ambient_dir(host_path, ambient_authority())
.map_err(|e| MountError::InvalidMount(format!("cannot open host path '{}': {e}", host_path.display())))?;
let canonical_host = fs::canonicalize(host_path).map_err(|e| {
MountError::InvalidMount(format!("cannot resolve host path '{}': {e}", host_path.display()))
})?;
Ok(Self {
virtual_path: normalized_virtual,
host_path: canonical_host,
dir: Arc::new(dir),
})
}
#[must_use]
pub fn virtual_path(&self) -> &str {
&self.virtual_path
}
#[must_use]
pub fn host_path(&self) -> &Path {
&self.host_path
}
}
fn path_matches_mount(normalized_path: &str, mount_virtual_path: &str) -> bool {
if mount_virtual_path == "/" || normalized_path == mount_virtual_path {
true
} else {
normalized_path.starts_with(mount_virtual_path)
&& normalized_path.as_bytes().get(mount_virtual_path.len()) == Some(&b'/')
}
}