mod env_policy;
mod http;
mod llm_policy;
#[cfg(feature = "sandbox")]
mod sandbox;
pub use env_policy::*;
pub use http::*;
pub use llm_policy::*;
#[cfg(feature = "sandbox")]
pub use sandbox::Sandboxed;
use std::io;
#[cfg(any(feature = "fs", feature = "hash"))]
use std::io::Read;
use std::path::{Path, PathBuf};
#[cfg(feature = "sandbox")]
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathOp {
Read,
Write,
Delete,
List,
}
impl std::fmt::Display for PathOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathOp::Read => f.write_str("read"),
PathOp::Write => f.write_str("write"),
PathOp::Delete => f.write_str("delete"),
PathOp::List => f.write_str("list"),
}
}
}
#[derive(Debug, Clone)]
pub struct PolicyError(String);
impl PolicyError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
pub fn message(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PolicyError {}
impl From<String> for PolicyError {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for PolicyError {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
pub struct FsAccess(pub(crate) FsAccessInner);
impl std::fmt::Debug for FsAccess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
FsAccessInner::Direct(p) => f.debug_tuple("FsAccess::Direct").field(p).finish(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { relative, .. } => {
f.debug_tuple("FsAccess::Capped").field(relative).finish()
}
}
}
}
pub(crate) enum FsAccessInner {
Direct(PathBuf),
#[cfg(feature = "sandbox")]
Capped {
dir: Arc<cap_std::fs::Dir>,
relative: PathBuf,
},
}
impl FsAccess {
pub fn direct(path: impl Into<PathBuf>) -> Self {
Self(FsAccessInner::Direct(path.into()))
}
#[cfg(feature = "fs")]
pub(crate) fn file_size(&self) -> io::Result<u64> {
match &self.0 {
FsAccessInner::Direct(p) => Ok(std::fs::metadata(p)?.len()),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => Ok(dir.metadata(relative)?.len()),
}
}
pub(crate) fn read_to_string(&self) -> io::Result<String> {
match &self.0 {
FsAccessInner::Direct(p) => std::fs::read_to_string(p),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => dir.read_to_string(relative),
}
}
#[cfg(feature = "fs")]
pub(crate) fn read_bytes(&self) -> io::Result<Vec<u8>> {
match &self.0 {
FsAccessInner::Direct(p) => std::fs::read(p),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => dir.read(relative),
}
}
pub(crate) fn write(&self, content: impl AsRef<[u8]>) -> io::Result<()> {
match &self.0 {
FsAccessInner::Direct(p) => std::fs::write(p, content),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => dir.write(relative, content),
}
}
#[cfg(any(feature = "fs", test))]
pub(crate) fn exists(&self) -> bool {
match &self.0 {
FsAccessInner::Direct(p) => p.exists(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => dir.exists(relative),
}
}
#[cfg(any(feature = "fs", test))]
pub(crate) fn is_dir(&self) -> bool {
match &self.0 {
FsAccessInner::Direct(p) => p.is_dir(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => {
dir.metadata(relative).map(|m| m.is_dir()).unwrap_or(false)
}
}
}
#[cfg(feature = "fs")]
pub(crate) fn is_file(&self) -> bool {
match &self.0 {
FsAccessInner::Direct(p) => p.is_file(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => {
dir.metadata(relative).map(|m| m.is_file()).unwrap_or(false)
}
}
}
#[cfg(feature = "fs")]
pub(crate) fn create_dir_all(&self) -> io::Result<()> {
match &self.0 {
FsAccessInner::Direct(p) => std::fs::create_dir_all(p),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => dir.create_dir_all(relative),
}
}
#[cfg(any(feature = "fs", test))]
pub(crate) fn remove(&self) -> io::Result<()> {
match &self.0 {
FsAccessInner::Direct(p) => match std::fs::remove_file(p) {
Ok(()) => Ok(()),
Err(e) if is_unlink_dir_error(&e) => std::fs::remove_dir_all(p).map_err(|_| e),
Err(e) => Err(e),
},
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => match dir.remove_file(relative) {
Ok(()) => Ok(()),
Err(e) if is_unlink_dir_error(&e) => dir.remove_dir_all(relative).map_err(|_| e),
Err(e) => Err(e),
},
}
}
#[cfg(any(feature = "fs", feature = "hash"))]
pub(crate) fn open_read(&self) -> io::Result<Box<dyn Read>> {
match &self.0 {
FsAccessInner::Direct(p) => {
let f = std::fs::File::open(p)?;
Ok(Box::new(io::BufReader::new(f)))
}
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => {
let f = dir.open(relative)?;
Ok(Box::new(io::BufReader::new(f)))
}
}
}
pub(crate) fn canonicalize(&self) -> io::Result<PathBuf> {
match &self.0 {
FsAccessInner::Direct(p) => p.canonicalize(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { .. } => Err(io::Error::new(
io::ErrorKind::Unsupported,
"canonicalize is not available in sandboxed mode",
)),
}
}
#[cfg(feature = "fs")]
pub(crate) fn walk_files_filtered(
&self,
display_prefix: &Path,
filter: &dyn Fn(&str) -> bool,
max_depth: usize,
max_entries: usize,
) -> io::Result<Vec<String>> {
let mut results = Vec::new();
match &self.0 {
FsAccessInner::Direct(p) => {
for entry in walkdir::WalkDir::new(p).max_depth(max_depth) {
match entry {
Ok(e) if e.file_type().is_file() => {
let path_str = e.path().to_string_lossy();
if filter(&path_str) {
if results.len() >= max_entries {
return Err(io::Error::other(format!(
"entry limit exceeded ({max_entries})"
)));
}
results.push(path_str.into_owned());
}
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
}
}
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { dir, relative } => {
let walk_root = dir.open_dir(relative)?;
sandbox::walk_capped_filtered(
&walk_root,
display_prefix,
filter,
0,
max_depth,
max_entries,
&mut results,
)?;
}
}
Ok(results)
}
#[cfg(feature = "fs")]
pub(crate) fn walk_files(
&self,
display_prefix: &Path,
max_depth: usize,
max_entries: usize,
) -> io::Result<Vec<String>> {
self.walk_files_filtered(display_prefix, &|_| true, max_depth, max_entries)
}
#[cfg(feature = "fs")]
pub(crate) fn copy_to(&self, dst: &FsAccess) -> io::Result<u64> {
match (&self.0, &dst.0) {
(FsAccessInner::Direct(src), FsAccessInner::Direct(d)) => std::fs::copy(src, d),
#[cfg(feature = "sandbox")]
_ => {
let content = self.read_bytes()?;
let len = content.len() as u64;
dst.write(&content)?;
Ok(len)
}
}
}
#[cfg(test)]
pub(crate) fn display(&self) -> String {
match &self.0 {
FsAccessInner::Direct(p) => p.to_string_lossy().to_string(),
#[cfg(feature = "sandbox")]
FsAccessInner::Capped { relative, .. } => relative.to_string_lossy().to_string(),
}
}
}
#[cfg(any(feature = "fs", test))]
fn is_unlink_dir_error(e: &io::Error) -> bool {
matches!(
e.kind(),
io::ErrorKind::IsADirectory | io::ErrorKind::PermissionDenied
)
}
pub trait PathPolicy: Send + Sync + 'static {
fn policy_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
fn resolve(&self, path: &Path, op: PathOp) -> Result<FsAccess, PolicyError>;
}
#[derive(Debug)]
pub struct Unrestricted;
impl PathPolicy for Unrestricted {
fn resolve(&self, path: &Path, _op: PathOp) -> Result<FsAccess, PolicyError> {
Ok(FsAccess::direct(path))
}
}
#[cfg(test)]
mod tests;