use std::path::{Path, PathBuf};
use std::sync::Arc;
use rquickjs::{Ctx, Error, Module, Result, loader::Loader, loader::Resolver, module::Declared};
use crate::fs::PathSandbox;
#[derive(Debug, Clone)]
pub struct SandboxResolver {
sandbox: Arc<PathSandbox>,
}
impl SandboxResolver {
#[must_use]
pub fn new(sandbox: Arc<PathSandbox>) -> Self {
Self { sandbox }
}
fn join_relative(&self, base: &str, name: &str) -> PathBuf {
let base_dir: PathBuf = if base.is_empty() {
self.sandbox.root().to_path_buf()
} else {
Path::new(base)
.parent()
.map_or_else(|| self.sandbox.root().to_path_buf(), PathBuf::from)
};
base_dir.join(name)
}
}
impl Resolver for SandboxResolver {
fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> Result<String> {
if !(name.starts_with("./") || name.starts_with("../") || name.starts_with('/')) {
return Err(Error::new_loading_message(
name,
"bare module specifiers are not supported inside the sandbox",
));
}
let joined = self.join_relative(base, name);
let rel = joined
.strip_prefix(self.sandbox.root())
.unwrap_or(&joined)
.to_string_lossy()
.into_owned();
let resolved = self
.sandbox
.resolve_read(&rel)
.map_err(|e| Error::new_loading_message(name, e.message.clone()))?;
Ok(resolved.to_string_lossy().into_owned())
}
}
#[derive(Debug, Clone)]
pub struct SandboxLoader {
sandbox: Arc<PathSandbox>,
}
impl SandboxLoader {
#[must_use]
pub fn new(sandbox: Arc<PathSandbox>) -> Self {
Self { sandbox }
}
}
impl Loader for SandboxLoader {
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result<Module<'js, Declared>> {
let path = Path::new(name);
if !path.starts_with(self.sandbox.root()) {
return Err(Error::new_loading_message(name, "path escapes script_root"));
}
let allowed_ext = matches!(path.extension().and_then(|e| e.to_str()), Some("js" | "mjs"));
if !allowed_ext {
return Err(Error::new_loading_message(
name,
"only .js and .mjs modules are supported",
));
}
let source = std::fs::read(path).map_err(|e| Error::new_loading_message(name, e.to_string()))?;
Module::declare(ctx.clone(), name, source)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mk_sandbox() -> (tempfile::TempDir, Arc<PathSandbox>) {
let tmp = tempfile::tempdir().expect("tempdir");
let sb = Arc::new(PathSandbox::new(tmp.path()).expect("sandbox"));
(tmp, sb)
}
#[test]
fn resolver_rejects_bare_specifiers() {
let (_tmp, sb) = mk_sandbox();
let mut r = SandboxResolver::new(sb);
let rt = rquickjs::Runtime::new().expect("runtime");
let cx = rquickjs::Context::full(&rt).expect("context");
cx.with(|ctx| {
let err = r.resolve(&ctx, "", "lodash").unwrap_err();
assert!(err.to_string().contains("bare module"));
});
}
#[test]
fn resolver_rejects_traversal() {
let (_tmp, sb) = mk_sandbox();
let mut r = SandboxResolver::new(sb);
let rt = rquickjs::Runtime::new().expect("runtime");
let cx = rquickjs::Context::full(&rt).expect("context");
cx.with(|ctx| {
let err = r.resolve(&ctx, "", "../escape.js").unwrap_err();
assert!(err.is_loading());
});
}
#[test]
fn resolver_accepts_valid_relative() {
let (tmp, sb) = mk_sandbox();
std::fs::write(tmp.path().join("helper.js"), b"export const x = 1;").unwrap();
let mut r = SandboxResolver::new(sb);
let rt = rquickjs::Runtime::new().expect("runtime");
let cx = rquickjs::Context::full(&rt).expect("context");
cx.with(|ctx| {
let resolved = r.resolve(&ctx, "", "./helper.js").expect("resolve");
assert!(resolved.ends_with("helper.js"));
});
}
}