use std::sync::Arc;
use sim_kernel::{
CapabilityName, CapabilitySet, Cx, Dir, ReadPolicy, Result, Shape, Symbol, Value,
};
use sim_lib_core::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
use sim_lib_namespace::{
ModuleIdentity, ModuleInstance, ModuleLoader, ModuleRequest, ModuleResolutionReceipt,
};
use sim_shape::AnyShape;
pub struct JavascriptModulePolicy {
loader: ModuleLoader,
codec: Symbol,
}
impl Default for JavascriptModulePolicy {
fn default() -> Self {
Self::with_codec(Symbol::qualified("codec", "javascript"))
}
}
impl JavascriptModulePolicy {
pub fn with_codec(codec: Symbol) -> Self {
Self {
loader: ModuleLoader::new(),
codec,
}
}
pub fn load(
&self,
cx: &mut Cx,
specifier: impl Into<String>,
admission: JavascriptModuleAdmission,
) -> Result<ModuleInstance> {
self.load_from(cx, None, specifier, admission)
}
pub fn load_from(
&self,
cx: &mut Cx,
importer: Option<ModuleIdentity>,
specifier: impl Into<String>,
admission: JavascriptModuleAdmission,
) -> Result<ModuleInstance> {
self.loader.load(
cx,
ModuleRequest {
root_id: admission.root_id,
root: admission.root,
importer,
specifier: specifier.into(),
codec: self.codec.clone(),
read_policy: admission.read_policy,
requires: admission.requires,
allow: admission.allow,
},
)
}
pub fn dynamic_import(
&self,
cx: &mut Cx,
importer: Option<ModuleIdentity>,
specifier: impl Into<String>,
admission: JavascriptModuleAdmission,
) -> Result<ModuleInstance> {
self.load_from(cx, importer, specifier, admission)
}
pub fn receipts(&self) -> Result<Vec<ModuleResolutionReceipt>> {
self.loader.receipts()
}
}
pub struct JavascriptModuleAdmission {
pub root_id: Symbol,
pub root: Arc<dyn Dir>,
pub read_policy: ReadPolicy,
pub requires: Vec<CapabilityName>,
pub allow: CapabilitySet,
}
pub struct DynamicJavascript {
broker: ReadEvalBroker,
codec: Symbol,
}
impl Default for DynamicJavascript {
fn default() -> Self {
Self::with_codec(Symbol::qualified("codec", "javascript"))
}
}
impl DynamicJavascript {
pub fn with_codec(codec: Symbol) -> Self {
Self {
broker: ReadEvalBroker::new(),
codec,
}
}
pub fn evaluate(
&self,
cx: &mut Cx,
source: impl Into<String>,
admission: JavascriptDynamicAdmission,
) -> Result<Value> {
self.broker.admit(
cx,
ReadEvalRequest {
origin: RequestOrigin::new(Symbol::qualified("javascript", "dynamic-source")),
codec: self.codec.clone(),
source: ReadEvalSource::Text(source.into()),
read_policy: admission.read_policy,
requires: admission.requires,
allow: admission.allow,
expected_shape: admission.expected_shape,
},
)
}
}
pub struct JavascriptDynamicAdmission {
pub read_policy: ReadPolicy,
pub requires: Vec<CapabilityName>,
pub allow: CapabilitySet,
pub expected_shape: Arc<dyn Shape>,
}
impl JavascriptDynamicAdmission {
pub fn new(read_policy: ReadPolicy) -> Self {
Self {
read_policy,
requires: Vec::new(),
allow: CapabilitySet::new(),
expected_shape: Arc::new(AnyShape),
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, sync::RwLock};
use sim_codec_lisp::LispCodecLib;
use sim_kernel::{
ClassId, ClassRef, CodecId, DefaultFactory, EagerPolicy, Error, Object, ObjectCompat,
Table, TrustLevel, read_eval_capability,
};
use sim_lib_namespace::{ModuleResolutionOutcome, module_load_capability};
use super::*;
fn context() -> (Cx, sim_kernel::GrantSeat) {
let (mut cx, seat) = Cx::new_seated(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
cx.load_lib(&LispCodecLib::new(CodecId(71)).unwrap())
.unwrap();
(cx, seat)
}
fn trusted() -> ReadPolicy {
ReadPolicy {
trust: TrustLevel::TrustedSource,
capabilities: CapabilitySet::new().grant(read_eval_capability()),
}
}
#[derive(Default)]
struct MemoryDir(RwLock<BTreeMap<Symbol, Value>>);
impl MemoryDir {
fn source(&self, cx: &mut Cx, name: &str, source: &str) {
self.0.write().unwrap().insert(
Symbol::new(name),
cx.factory().string(source.to_owned()).unwrap(),
);
}
}
impl Object for MemoryDir {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok("javascript-memory-root".to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for MemoryDir {
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
cx.factory()
.class_stub(ClassId(0), Symbol::qualified("test", "JavascriptRoot"))
}
fn as_table_impl(&self) -> Option<&dyn Table> {
Some(self)
}
fn as_dir(&self) -> Option<&dyn Dir> {
Some(self)
}
}
impl Table for MemoryDir {
fn backend_symbol(&self) -> Symbol {
Symbol::qualified("test", "javascript-root")
}
fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
self.0
.read()
.unwrap()
.get(&key)
.cloned()
.map_or_else(|| cx.factory().nil(), Ok)
}
fn set(&self, _cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
self.0.write().unwrap().insert(key, value);
Ok(())
}
fn has(&self, _cx: &mut Cx, key: Symbol) -> Result<bool> {
Ok(self.0.read().unwrap().contains_key(&key))
}
fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
self.0
.write()
.unwrap()
.remove(&key)
.map_or_else(|| cx.factory().nil(), Ok)
}
fn keys(&self, _cx: &mut Cx) -> Result<Vec<Symbol>> {
Ok(self.0.read().unwrap().keys().cloned().collect())
}
fn entries(&self, _cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
Ok(self
.0
.read()
.unwrap()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect())
}
fn len(&self, _cx: &mut Cx) -> Result<usize> {
Ok(self.0.read().unwrap().len())
}
fn clear(&self, _cx: &mut Cx) -> Result<()> {
self.0.write().unwrap().clear();
Ok(())
}
}
impl Dir for MemoryDir {
fn mkdir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Value> {
Err(Error::Eval("nested dirs unsupported".into()))
}
fn opendir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Option<Value>> {
Ok(None)
}
fn rmdir(&self, cx: &mut Cx, _name: Symbol) -> Result<Value> {
cx.factory().nil()
}
fn is_dir(&self, _cx: &mut Cx, _name: Symbol) -> Result<bool> {
Ok(false)
}
}
fn admission(root: Arc<MemoryDir>) -> JavascriptModuleAdmission {
JavascriptModuleAdmission {
root_id: Symbol::new("modules"),
root,
read_policy: trusted(),
requires: vec![module_load_capability()],
allow: CapabilitySet::new(),
}
}
#[test]
fn esm_uses_shared_live_lifecycle_supplied_roots_and_cached_failures() {
let (mut cx, seat) = context();
seat.grant(&mut cx, read_eval_capability()).unwrap();
seat.grant(&mut cx, module_load_capability()).unwrap();
let root = Arc::new(MemoryDir::default());
root.source(&mut cx, "answer.mjs", "42");
root.source(&mut cx, "broken.mjs", "(");
let modules = JavascriptModulePolicy::with_codec(Symbol::qualified("codec", "lisp"));
let first = modules
.load(&mut cx, "answer.mjs", admission(root.clone()))
.unwrap();
let imported = modules
.dynamic_import(&mut cx, None, "answer.mjs", admission(root.clone()))
.unwrap();
assert_eq!(first.identity(), imported.identity());
assert_eq!(
first
.default_export()
.get()
.unwrap()
.object()
.display(&mut cx)
.unwrap(),
"42"
);
assert!(
modules
.load(&mut cx, "broken.mjs", admission(root.clone()))
.is_err()
);
root.source(&mut cx, "broken.mjs", "41");
assert!(
modules
.load(&mut cx, "broken.mjs", admission(root))
.is_err()
);
assert_eq!(
modules
.receipts()
.unwrap()
.iter()
.map(|r| r.outcome)
.collect::<Vec<_>>(),
vec![
ModuleResolutionOutcome::Linked,
ModuleResolutionOutcome::CacheHit,
ModuleResolutionOutcome::Failed,
ModuleResolutionOutcome::Failed,
]
);
}
#[test]
fn dynamic_source_rejects_ambient_authority() {
let (mut cx, _seat) = context();
let dynamic = DynamicJavascript::with_codec(Symbol::qualified("codec", "lisp"));
let denied = dynamic
.evaluate(
&mut cx,
"42",
JavascriptDynamicAdmission::new(ReadPolicy {
trust: TrustLevel::Untrusted,
capabilities: CapabilitySet::new(),
}),
)
.unwrap_err();
assert!(matches!(
denied,
Error::TrustDenied { .. } | Error::CapabilityDenied { .. }
));
}
}