use std::path::{Path, PathBuf};
pub struct FileContent {
pub content: String,
pub resolved_path: PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum InitError {
#[error("root directory not found: {}", path.display())]
RootNotFound { path: PathBuf },
#[error("I/O error on {}: {source}", path.display())]
Io {
path: PathBuf,
source: std::io::Error,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ReadError {
#[error("path traversal detected: {}", attempted.display())]
Traversal { attempted: PathBuf },
#[error("I/O error on {}: {source}", path.display())]
Io {
path: PathBuf,
source: std::io::Error,
},
}
pub trait SandboxedFs: Send + Sync {
fn read(&self, relative: &Path) -> Result<Option<FileContent>, ReadError>;
}
pub struct FsSandbox {
root: PathBuf,
}
impl FsSandbox {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let raw = root.into();
let canonical = match raw.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(InitError::RootNotFound { path: raw });
}
Err(e) => {
return Err(InitError::Io {
path: raw,
source: e,
});
}
};
Ok(Self { root: canonical })
}
}
impl SandboxedFs for FsSandbox {
fn read(&self, relative: &Path) -> Result<Option<FileContent>, ReadError> {
let path = self.root.join(relative);
let canonical = match path.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(ReadError::Io { path, source: e });
}
};
if !canonical.starts_with(&self.root) {
return Err(ReadError::Traversal {
attempted: canonical,
});
}
match std::fs::read_to_string(&canonical) {
Ok(content) => Ok(Some(FileContent {
content,
resolved_path: canonical,
})),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ReadError::Io {
path: canonical,
source: e,
}),
}
}
}
pub struct SymlinkAwareSandbox {
root: PathBuf,
allowed_targets: Vec<PathBuf>,
}
impl SymlinkAwareSandbox {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let raw = root.into();
let canonical = match raw.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(InitError::RootNotFound { path: raw });
}
Err(e) => {
return Err(InitError::Io {
path: raw,
source: e,
});
}
};
let mut allowed_targets = Vec::new();
if let Ok(entries) = std::fs::read_dir(&canonical) {
for entry in entries.flatten() {
let meta = match entry.path().symlink_metadata() {
Ok(m) => m,
Err(_) => continue,
};
if meta.file_type().is_symlink() {
if let Ok(target) = entry.path().canonicalize() {
allowed_targets.push(target);
}
}
}
}
Ok(Self {
root: canonical,
allowed_targets,
})
}
}
impl SandboxedFs for SymlinkAwareSandbox {
fn read(&self, relative: &Path) -> Result<Option<FileContent>, ReadError> {
let path = self.root.join(relative);
let canonical = match path.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(ReadError::Io { path, source: e });
}
};
if canonical.starts_with(&self.root) {
return read_file(&canonical);
}
for target in &self.allowed_targets {
if canonical.starts_with(target) {
return read_file(&canonical);
}
}
Err(ReadError::Traversal {
attempted: canonical,
})
}
}
fn read_file(canonical: &Path) -> Result<Option<FileContent>, ReadError> {
match std::fs::read_to_string(canonical) {
Ok(content) => Ok(Some(FileContent {
content,
resolved_path: canonical.to_path_buf(),
})),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ReadError::Io {
path: canonical.to_path_buf(),
source: e,
}),
}
}
#[cfg(feature = "sandbox-cap-std")]
pub struct CapSandbox {
dir: cap_std::fs::Dir,
}
#[cfg(feature = "sandbox-cap-std")]
impl CapSandbox {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let raw = root.into();
let dir = match cap_std::fs::Dir::open_ambient_dir(&raw, cap_std::ambient_authority()) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(InitError::RootNotFound { path: raw });
}
Err(e) => {
return Err(InitError::Io {
path: raw,
source: e,
});
}
};
Ok(Self { dir })
}
}
#[cfg(feature = "sandbox-cap-std")]
impl SandboxedFs for CapSandbox {
fn read(&self, relative: &Path) -> Result<Option<FileContent>, ReadError> {
match self.dir.read_to_string(relative) {
Ok(content) => Ok(Some(FileContent {
content,
resolved_path: relative.to_path_buf(),
})),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ReadError::Io {
path: relative.to_path_buf(),
source: e,
}),
}
}
}