#![cfg(feature = "rhai-runtime")]
use std::path::PathBuf;
use std::sync::Arc;
use rhai::Engine;
use uni_plugin::{Capability, CapabilitySet, normalize_capability_path};
use crate::host_fn_impls::rt_err;
use crate::host_fns::{RhaiHostFnRegistry, RhaiHostFnSpec};
use crate::loader::RhaiLoader;
pub fn register(loader: &mut RhaiLoader) {
let placeholder = Capability::Filesystem {
read: vec!["**".into()],
write: vec!["**".into()],
};
loader.host_fns_mut().register(RhaiHostFnSpec::gated(
"uni.fs.read",
placeholder.clone(),
"Read a UTF-8 file from the host filesystem.",
register_fs_read,
));
loader.host_fns_mut().register(RhaiHostFnSpec::gated(
"uni.fs.write",
placeholder,
"Write a UTF-8 string to a host filesystem path.",
register_fs_write,
));
}
fn register_fs_read(engine: &mut Engine, caps: &CapabilitySet) {
let caps = caps.clone();
engine.register_fn(
"uni_fs_read",
move |path: &str| -> Result<String, Box<rhai::EvalAltResult>> {
let resolved = resolve_read_path(&caps, path)?;
std::fs::read_to_string(&resolved)
.map_err(|e| rt_err(format!("uni.fs.read({path}): {e}")))
},
);
}
fn register_fs_write(engine: &mut Engine, caps: &CapabilitySet) {
let caps = caps.clone();
engine.register_fn(
"uni_fs_write",
move |path: &str, data: &str| -> Result<(), Box<rhai::EvalAltResult>> {
let resolved = resolve_write_path(&caps, path)?;
std::fs::write(&resolved, data)
.map_err(|e| rt_err(format!("uni.fs.write({path}): {e}")))
},
);
}
fn resolve_read_path(
caps: &CapabilitySet,
path: &str,
) -> Result<PathBuf, Box<rhai::EvalAltResult>> {
let norm = normalize_capability_path(path).ok_or_else(|| {
rt_err(format!(
"uni.fs.read: path `{path}` is not an absolute, traversal-free path"
))
})?;
if !caps
.iter()
.any(|c| c.filesystem_read_allows(&norm.to_string_lossy()))
{
return Err(rt_err(format!(
"uni.fs.read: path `{path}` not in granted Filesystem read allow-list"
)));
}
let canonical =
std::fs::canonicalize(&norm).map_err(|e| rt_err(format!("uni.fs.read({path}): {e}")))?;
if !caps
.iter()
.any(|c| c.filesystem_read_allows(&canonical.to_string_lossy()))
{
return Err(rt_err(format!(
"uni.fs.read: path `{path}` resolves outside the granted Filesystem read allow-list"
)));
}
Ok(canonical)
}
fn resolve_write_path(
caps: &CapabilitySet,
path: &str,
) -> Result<PathBuf, Box<rhai::EvalAltResult>> {
let norm = normalize_capability_path(path).ok_or_else(|| {
rt_err(format!(
"uni.fs.write: path `{path}` is not an absolute, traversal-free path"
))
})?;
if !caps
.iter()
.any(|c| c.filesystem_write_allows(&norm.to_string_lossy()))
{
return Err(rt_err(format!(
"uni.fs.write: path `{path}` not in granted Filesystem write allow-list"
)));
}
let resolved = if norm.exists() {
std::fs::canonicalize(&norm).map_err(|e| rt_err(format!("uni.fs.write({path}): {e}")))?
} else {
let parent = norm.parent().ok_or_else(|| {
rt_err(format!(
"uni.fs.write: path `{path}` has no parent directory"
))
})?;
let file_name = norm
.file_name()
.ok_or_else(|| rt_err(format!("uni.fs.write: path `{path}` has no file name")))?;
let canonical_parent = std::fs::canonicalize(parent)
.map_err(|e| rt_err(format!("uni.fs.write({path}): parent: {e}")))?;
canonical_parent.join(file_name)
};
if !caps
.iter()
.any(|c| c.filesystem_write_allows(&resolved.to_string_lossy()))
{
return Err(rt_err(format!(
"uni.fs.write: path `{path}` resolves outside the granted Filesystem write allow-list"
)));
}
Ok(resolved)
}
#[doc(hidden)]
pub fn _register_for_test(reg: &mut RhaiHostFnRegistry) {
let cap = Capability::Filesystem {
read: vec!["**".into()],
write: vec![],
};
reg.register(RhaiHostFnSpec {
name: "uni.fs.read".into(),
required_capability: Some(cap),
docs: String::new(),
register: Arc::new(register_fs_read),
});
}