mod child_process;
mod io;
mod shell;
mod sync_process;
use rong::{
HostError, IntoJSValue, JSArray, JSContext, JSContextService, JSObject, JSResult, JSValue,
};
use std::env;
use std::sync::Arc;
use std::time::Duration;
pub trait ProcessAuthority: Send + Sync + 'static {
fn authorize(&self) -> Result<(), String>;
}
#[derive(Clone)]
struct ProcessAuthorityService(Arc<dyn ProcessAuthority>);
impl JSContextService for ProcessAuthorityService {}
pub(crate) fn process_authority(ctx: &JSContext) -> Option<Arc<dyn ProcessAuthority>> {
ctx.get_service::<ProcessAuthorityService>()
.map(|service| Arc::clone(&service.0))
}
pub(crate) fn authorize_process(ctx: &JSContext) -> JSResult<()> {
authorize_installed_process(process_authority(ctx))
}
fn authorize_installed_process(authority: Option<Arc<dyn ProcessAuthority>>) -> JSResult<()> {
match authority {
Some(authority) => authorize_process_with(authority.as_ref()),
None => Ok(()),
}
}
pub(crate) fn authorize_process_with(authority: &dyn ProcessAuthority) -> JSResult<()> {
authority
.authorize()
.map_err(|message| HostError::new(rong::error::E_PERMISSION_DENIED, message).into())
}
pub(crate) async fn wait_for_process_revocation(authority: Arc<dyn ProcessAuthority>) {
loop {
if authority.authorize().is_err() {
return;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
fn create_env_object(ctx: &JSContext) -> JSResult<JSObject> {
let env_obj = JSObject::new(ctx);
for (key, value) in env::vars() {
env_obj.set(key.as_str(), value)?;
}
Ok(env_obj)
}
fn create_string_array(
ctx: &JSContext,
values: impl IntoIterator<Item = String>,
) -> JSResult<JSValue> {
let array = JSArray::new(ctx)?;
for value in values {
array.push(value)?;
}
Ok(array.into_js_value(ctx))
}
fn install_command_apis(ctx: &JSContext) -> JSResult<()> {
let rong = ctx.host_namespace();
rong.set("env", create_env_object(ctx)?)?;
rong.set("argv", create_string_array(ctx, env::args())?)?;
rong.set("args", create_string_array(ctx, env::args().skip(2))?)?;
rong_buffer::init(ctx)?;
rong_encoding::init(ctx)?;
rong_abort::init(ctx)?;
rong_stream::init(ctx)?;
io::init(ctx)?;
child_process::init(ctx)?;
sync_process::init(ctx)?;
shell::init(ctx)?;
Ok(())
}
pub fn init(ctx: &JSContext) -> JSResult<()> {
install_command_apis(ctx)
}
pub fn init_with_authority(ctx: &JSContext, authority: Arc<dyn ProcessAuthority>) -> JSResult<()> {
if ctx.get_service::<ProcessAuthorityService>().is_some() {
return Err(HostError::new(
rong::error::E_ALREADY_EXISTS,
"process authority is already sealed for this context",
)
.into());
}
authority
.authorize()
.map_err(|message| HostError::new(rong::error::E_PERMISSION_DENIED, message))?;
ctx.set_service(ProcessAuthorityService(authority));
install_command_apis(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use rong::Source;
use rong_test::*;
use std::sync::atomic::{AtomicBool, Ordering};
struct RevocableAuthority(AtomicBool);
impl ProcessAuthority for RevocableAuthority {
fn authorize(&self) -> Result<(), String> {
self.0
.load(Ordering::SeqCst)
.then_some(())
.ok_or_else(|| "revoked".to_string())
}
}
fn run_unit_suite(unit: &str) {
let unit = unit.to_string();
async_run!(|ctx: JSContext| async move {
rong_assert::init(&ctx)?;
rong_console::init(&ctx)?;
init(&ctx)?;
let passed = UnitJSRunner::load_script(&ctx, &unit).await?.run().await?;
assert!(passed);
Ok(())
});
}
fn run_stdio_suite(mode: &str) {
let mode = mode.to_string();
async_run!(|ctx: JSContext| async move {
rong_assert::init(&ctx)?;
rong_console::init(&ctx)?;
let stdio = io::install_captured_stdio(&ctx, b"stdin-payload".to_vec());
init(&ctx)?;
ctx.global().set("__stdioMode", mode.clone())?;
let passed = UnitJSRunner::load_script(&ctx, "stdio.js")
.await?
.run()
.await?;
assert!(passed);
if mode == "text" {
assert_eq!(
String::from_utf8(stdio.stdout_bytes()).unwrap(),
"hello-out"
);
assert_eq!(String::from_utf8(stdio.stderr_bytes()).unwrap(), "warn-err");
}
Ok(())
});
}
#[test]
fn test_command_namespace() {
for unit in ["spawn.js", "shell.js"] {
run_unit_suite(unit);
}
run_stdio_suite("text");
run_stdio_suite("bytes");
}
#[tokio::test]
async fn revocation_waiter_observes_authority_change() {
let authority = Arc::new(RevocableAuthority(AtomicBool::new(true)));
let task = tokio::spawn(wait_for_process_revocation(authority.clone()));
authority.0.store(false, Ordering::SeqCst);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("revocation waiter must finish")
.expect("waiter task");
}
#[test]
fn allow_and_stale_authorities_are_checked_on_every_operation() {
let authority = RevocableAuthority(AtomicBool::new(true));
assert!(authorize_process_with(&authority).is_ok());
authority.0.store(false, Ordering::SeqCst);
let error = authorize_process_with(&authority).expect_err("stale authority must fail");
assert!(error.to_string().contains("revoked"));
}
#[test]
fn missing_authority_leaves_process_unrestricted() {
authorize_installed_process(None).expect("init() embeddings stay unrestricted");
}
#[test]
fn sealed_authority_cannot_be_replaced() {
async_run!(|ctx: JSContext| async move {
let first = Arc::new(RevocableAuthority(AtomicBool::new(true)));
init_with_authority(&ctx, first)?;
let second = Arc::new(RevocableAuthority(AtomicBool::new(true)));
let error = init_with_authority(&ctx, second)
.expect_err("a second installer must not replace the sealed authority");
assert!(error.to_string().contains("already sealed"));
Ok(())
});
}
#[test]
fn sealed_denial_rejects_spawn() {
async_run!(|ctx: JSContext| async move {
let authority = Arc::new(RevocableAuthority(AtomicBool::new(true)));
init_with_authority(&ctx, authority.clone())?;
authority.0.store(false, Ordering::SeqCst);
let message: String = ctx.eval(Source::from_bytes(
r#"
(() => {
try {
Rong.spawn(['true']);
return 'did-not-throw';
} catch (e) {
return (e && e.message) ? String(e.message) : String(e);
}
})()
"#,
))?;
assert_ne!(message, "did-not-throw");
assert!(
message.contains("revoked") || message.contains("PERMISSION"),
"{message}"
);
Ok(())
});
}
}