use std::{
fs,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use super::{
common::MountContext,
dispatch::{self, FsRequest},
error::MountError,
mount_mode::MountMode,
path_security::normalize_virtual_path,
};
use crate::{MontyObject, os::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);
}
#[must_use]
pub fn into_mounts(self) -> Vec<Mount> {
self.mounts
}
pub fn take_shared_mounts(slots: &[Arc<Mutex<Option<Mount>>>]) -> Result<Self, String> {
let mut taken: Vec<Mount> = Vec::with_capacity(slots.len());
for (i, shared) in slots.iter().enumerate() {
let Ok(mut guard) = shared.lock() else {
rollback_taken_mounts(taken, &slots[..i]);
return Err(format!("mount {i} lock is poisoned"));
};
let Some(mount) = guard.take() else {
drop(guard); rollback_taken_mounts(taken, &slots[..i]);
return Err(format!("mount {i} is already in use by another run"));
};
taken.push(mount);
}
let mut table = Self::new();
for mount in taken {
table.push_mount(mount);
}
Ok(table)
}
pub fn put_back_shared_mounts(self, slots: &[Arc<Mutex<Option<Mount>>>]) {
for (shared, mount) in slots.iter().zip(self.into_mounts()) {
if let Ok(mut slot) = shared.lock() {
debug_assert!(slot.is_none(), "mount slot should be empty during put_back");
*slot = Some(mount);
}
}
}
pub fn handle_os_call(&mut self, call: &OsFunctionCall) -> Option<Result<MontyObject, MountError>> {
if !call.is_filesystem() {
return None;
}
let request = dispatch::fs_request_from_call(call);
match self.route_request(request) {
Some(Ok(index)) => Some(self.mounts[index].execute(request)),
Some(Err(err)) => Some(Err(err)),
None => None,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.mounts.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.mounts.len()
}
fn route_request(&self, request: FsRequest<'_>) -> Option<Result<usize, MountError>> {
let src_mount_index = self.find_mount_index(request.primary_path())?;
if let Some(dst_path) = request.rename_destination() {
let dst_mount_index = self.find_mount_index(dst_path)?;
if src_mount_index != dst_mount_index {
return Some(Err(MountError::CrossMountRename {
src: request.primary_path().to_owned(),
dst: dst_path.to_owned(),
}));
}
}
Some(Ok(src_mount_index))
}
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))
}
}
fn rollback_taken_mounts(taken: Vec<Mount>, slots: &[Arc<Mutex<Option<Mount>>>]) {
for (shared, mount) in slots.iter().zip(taken) {
if let Ok(mut slot) = shared.lock() {
*slot = Some(mount);
}
}
}
#[derive(Debug)]
pub struct Mount {
virtual_path: String,
host_path: PathBuf,
mode: MountMode,
write_bytes_used: u64,
write_bytes_limit: Option<u64>,
}
impl Mount {
pub fn new(
virtual_path: &str,
host_path: impl AsRef<Path>,
mode: MountMode,
write_bytes_limit: Option<u64>,
) -> 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 canonical_host = fs::canonicalize(host_path).map_err(|e| {
MountError::InvalidMount(format!("cannot canonicalize host path '{}': {e}", host_path.display()))
})?;
if !canonical_host.is_dir() {
return Err(MountError::InvalidMount(format!(
"host path is not a directory: '{}'",
host_path.display()
)));
}
Ok(Self {
virtual_path: normalized_virtual,
host_path: canonical_host,
mode,
write_bytes_used: 0,
write_bytes_limit,
})
}
#[must_use]
pub fn virtual_path(&self) -> &str {
&self.virtual_path
}
#[must_use]
pub fn host_path(&self) -> &Path {
&self.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 write_bytes_used(&self) -> u64 {
self.write_bytes_used
}
fn execute(&mut self, request: FsRequest<'_>) -> Result<MontyObject, MountError> {
let mut ctx = MountContext {
mount_virtual: &self.virtual_path,
mount_host: &self.host_path,
write_bytes_used: &mut self.write_bytes_used,
write_bytes_limit: self.write_bytes_limit,
};
dispatch::execute(request, &mut ctx, &mut self.mode)
}
}
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'/')
}
}