//! Capability-sandboxed host files. Every system call runs on the blocking pool.
//! Writable files currently open a second append descriptor by path. A concurrent rename
//! between those opens can make append refer to a different file (review F9).
use crate::*;
use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt};
use cap_std::fs::{Dir, FileExt, OpenOptions};
use std::{io::Write, path::Path, sync::Mutex};
/// A host directory bound to one process tracker.
#[derive(Clone)]
pub struct Host {
dir: Arc
,
process: Process,
}
impl Host {
/// Open the sandbox root. Call this outside an asynchronous worker.
pub fn new(path: impl AsRef, process: Process) -> Result {
Ok(Self {
dir: Arc::new(Dir::open_ambient_dir(path, cap_std::ambient_authority())?),
process,
})
}
/// Share a sandbox with a different process tracker.
pub fn for_process(&self, process: Process) -> Self {
Self {
dir: self.dir.clone(),
process,
}
}
}
fn rel(path: &str) -> String {
let p = path.trim_start_matches('/');
if p.is_empty() { ".".into() } else { p.into() }
}
fn stat(m: cap_std::fs::Metadata) -> Stat {
Stat {
directory: m.is_dir(),
symlink: m.file_type().is_symlink(),
size: m.len(),
accessed: m.accessed().ok().map(|t| t.into_std()),
modified: m.modified().ok().map(|t| t.into_std()),
}
}
struct Handle {
file: Arc>,
append: Arc>,
process: Process,
}
#[async_trait]
impl Layer for Host {
async fn stat(&self, path: &str, follow: bool) -> Result {
let (d, p) = (self.dir.clone(), rel(path));
self.process
.blocking(move || {
if follow {
d.metadata(p)
} else {
d.symlink_metadata(p)
}
.map(stat)
})
.await
}
async fn readlink(&self, path: &str) -> Result {
let (d, p) = (self.dir.clone(), rel(path));
self.process
.blocking(move || d.read_link(p).map(|p| p.to_string_lossy().into_owned()))
.await
}
async fn list(&self, path: &str) -> Result> {
let (d, p) = (self.dir.clone(), rel(path));
self.process
.blocking(move || {
let mut entries = d
.read_dir(p)?
.map(|e| {
let e = e?;
Ok(Entry {
name: e.file_name().to_string_lossy().into_owned(),
directory: e.file_type()?.is_dir(),
symlink: e.file_type()?.is_symlink(),
})
})
.collect::>>()?;
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
})
.await
}
async fn open(&self, path: &str, o: Open) -> Result> {
let (d, p) = (self.dir.clone(), rel(path));
let (file, append) = self
.process
.blocking(move || {
let mut options = OpenOptions::new();
options
.read(true)
.write(o.write)
.create(o.create)
.create_new(o.create && o.exclusive)
.truncate(o.truncate);
options.follow(if o.follow {
FollowSymlinks::Yes
} else {
FollowSymlinks::No
});
let f = d.open_with(&p, &options)?;
let directory = f.metadata()?.is_dir();
if o.directory && !directory {
return Err(error(io::ErrorKind::NotADirectory));
}
let append = if o.write && !directory {
{
let mut append = OpenOptions::new();
append.append(true).follow(if o.follow {
FollowSymlinks::Yes
} else {
FollowSymlinks::No
});
d.open_with(&p, &append)?
}
} else {
f.try_clone()?
};
Ok((f, append))
})
.await?;
Ok(Arc::new(Handle {
file: Arc::new(Mutex::new(file)),
append: Arc::new(Mutex::new(append)),
process: self.process.clone(),
}))
}
async fn mkdir(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.create_dir(p)).await
}
async fn remove_file(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.remove_file(p)).await
}
async fn remove_dir(&self, path: &str) -> Result<()> {
let (d, p) = (self.dir.clone(), rel(path));
self.process.blocking(move || d.remove_dir(p)).await
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
let (d, a, b) = (self.dir.clone(), rel(from), rel(to));
self.process.blocking(move || d.rename(a, &d, b)).await
}
}
#[async_trait]
impl File for Handle {
async fn stat(&self) -> Result {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().metadata().map(stat))
.await
}
async fn read(&self, offset: u64, length: usize) -> Result> {
let f = self.file.clone();
self.process
.blocking(move || {
let f = f.lock().unwrap();
let mut bytes = Vec::new();
bytes
.try_reserve(length)
.map_err(|_| error(io::ErrorKind::FileTooLarge))?;
bytes.resize(length, 0);
let n = f.read_at(&mut bytes, offset)?;
bytes.truncate(n);
Ok(bytes)
})
.await
}
async fn write(&self, offset: u64, bytes: Vec) -> Result {
let f = self.file.clone();
self.process
.blocking(move || {
let f = f.lock().unwrap();
f.write_at(&bytes, offset)
})
.await
}
async fn append(&self, bytes: Vec) -> Result {
let f = self.append.clone();
self.process
.blocking(move || f.lock().unwrap().write(&bytes))
.await
}
async fn set_size(&self, size: u64) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().set_len(size))
.await
}
async fn sync(&self) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || f.lock().unwrap().sync_all())
.await
}
async fn set_times(
&self,
accessed: Option,
modified: Option,
) -> Result<()> {
let f = self.file.clone();
self.process
.blocking(move || {
let f = f.lock().unwrap().try_clone()?.into_std();
let mut times = std::fs::FileTimes::new();
if let Some(t) = accessed {
times = times.set_accessed(t);
}
if let Some(t) = modified {
times = times.set_modified(t);
}
f.set_times(times)
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Seek, SeekFrom};
/// Positional reads and writes do not change the descriptor's cursor.
#[tokio::test]
async fn positional_io_keeps_cursor() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("file"), b"abcd").unwrap();
let host = Host::new(temp.path(), Process::default()).unwrap();
let file = host
.dir
.open_with("file", OpenOptions::new().read(true).write(true))
.unwrap();
let append = file.try_clone().unwrap();
let file = Arc::new(Mutex::new(file));
file.lock().unwrap().seek(SeekFrom::Start(3)).unwrap();
let handle = Handle {
file: file.clone(),
append: Arc::new(Mutex::new(append)),
process: Process::default(),
};
assert_eq!(handle.read(0, 1).await.unwrap(), b"a");
handle.write(1, b"Z".to_vec()).await.unwrap();
assert_eq!(file.lock().unwrap().stream_position().unwrap(), 3);
}
}