use mlua::{Function, Lua, LuaSerdeExt, Result, Table, UserData, UserDataMethods, Value};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use tokio::runtime::Runtime;
use tokio::task::JoinHandle;
pub fn runtime() -> &'static Runtime {
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime")
})
}
pub fn block_on<F, T>(future: F) -> T
where
F: std::future::Future<Output = T>,
{
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
tokio::task::block_in_place(|| handle.block_on(future))
}
Err(_) => {
runtime().block_on(future)
}
}
}
#[derive(Debug, Clone, Copy, Default)]
enum Method {
#[default]
Get,
Post,
Put,
Delete,
Patch,
Head,
}
impl Method {
fn from_str(s: &str) -> Self {
match s.to_uppercase().as_str() {
"POST" => Method::Post,
"PUT" => Method::Put,
"DELETE" => Method::Delete,
"PATCH" => Method::Patch,
"HEAD" => Method::Head,
_ => Method::Get,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum CacheOption {
#[default]
Disabled,
Auto,
Path(PathBuf),
}
#[derive(Debug, Default, Clone)]
struct FetchOptions {
method: Method,
headers: HashMap<String, String>,
body: Option<String>,
timeout_secs: Option<u64>,
cache: CacheOption,
}
impl FetchOptions {
fn from_lua_table(lua: &Lua, table: &Table) -> Result<Self> {
let mut opts = FetchOptions::default();
if let Ok(method) = table.get::<String>("method") {
opts.method = Method::from_str(&method);
}
if let Ok(headers) = table.get::<Table>("headers") {
for (k, v) in headers.pairs::<String, String>().flatten() {
opts.headers.insert(k, v);
}
}
if let Ok(body) = table.get::<String>("body") {
opts.body = Some(body);
} else if let Ok(body_table) = table.get::<Table>("body") {
if let Ok(json_val) = lua.from_value::<serde_json::Value>(Value::Table(body_table)) {
opts.body = Some(json_val.to_string());
opts.headers
.entry("Content-Type".to_string())
.or_insert_with(|| "application/json".to_string());
}
}
if let Ok(timeout) = table.get::<u64>("timeout") {
opts.timeout_secs = Some(timeout);
}
if let Ok(cache_path) = table.get::<String>("cache") {
opts.cache = CacheOption::Path(PathBuf::from(cache_path));
} else if let Ok(cache_bool) = table.get::<bool>("cache") {
opts.cache = if cache_bool {
CacheOption::Auto
} else {
CacheOption::Disabled
};
}
Ok(opts)
}
}
fn get_cache_path(url: &str, project_root: &Path) -> PathBuf {
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
let mut hasher = XxHash64::with_seed(0);
url.hash(&mut hasher);
let hash = hasher.finish();
let hash_hex = format!("{:016x}", hash);
project_root.join(".rs-web-cache/downloads").join(hash_hex)
}
fn read_cache(cache_path: &Path) -> Option<Vec<u8>> {
std::fs::read(cache_path).ok()
}
fn write_cache(cache_path: &Path, data: &[u8]) -> std::io::Result<()> {
if let Some(parent) = cache_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(cache_path, data)
}
type FetchResult = std::result::Result<FetchResponse, String>;
struct AsyncTask {
handle: Arc<Mutex<Option<JoinHandle<FetchResult>>>>,
result: Arc<Mutex<Option<FetchResult>>>,
completed: Arc<Mutex<bool>>,
}
impl Clone for AsyncTask {
fn clone(&self) -> Self {
Self {
handle: Arc::clone(&self.handle),
result: Arc::clone(&self.result),
completed: Arc::clone(&self.completed),
}
}
}
impl UserData for AsyncTask {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("is_completed", |_, this, _: ()| Ok(*this.completed.lock()));
}
}
#[derive(Clone)]
struct FetchResponse {
status: u16,
headers: HashMap<String, String>,
body: String,
ok: bool,
}
type BytesFetchResult = std::result::Result<BytesFetchResponse, String>;
#[derive(Clone)]
struct BytesFetchResponse {
status: u16,
body: Vec<u8>,
ok: bool,
}
impl BytesFetchResponse {
fn to_lua_table(&self, lua: &Lua) -> Result<Table> {
let table = lua.create_table()?;
table.set("status", self.status)?;
table.set("ok", self.ok)?;
table.set("body", lua.create_string(&self.body)?)?;
Ok(table)
}
}
struct AsyncBytesTask {
handle: Arc<Mutex<Option<JoinHandle<BytesFetchResult>>>>,
result: Arc<Mutex<Option<BytesFetchResult>>>,
completed: Arc<Mutex<bool>>,
}
impl Clone for AsyncBytesTask {
fn clone(&self) -> Self {
Self {
handle: Arc::clone(&self.handle),
result: Arc::clone(&self.result),
completed: Arc::clone(&self.completed),
}
}
}
impl UserData for AsyncBytesTask {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("is_completed", |_, this, _: ()| Ok(*this.completed.lock()));
}
}
type IOResult = std::result::Result<IOResponse, String>;
#[derive(Clone)]
enum IOResponse {
Ok, Bytes(u64), Bool(bool), String(String), Json(serde_json::Value), }
impl IOResponse {
fn to_lua_value(&self, lua: &Lua) -> Result<Value> {
match self {
IOResponse::Ok => Ok(Value::Boolean(true)),
IOResponse::Bytes(n) => Ok(Value::Integer(*n as i64)),
IOResponse::Bool(b) => Ok(Value::Boolean(*b)),
IOResponse::String(s) => Ok(Value::String(lua.create_string(s)?)),
IOResponse::Json(v) => lua.to_value(v),
}
}
}
pub struct AsyncIOTask {
handle: Arc<Mutex<Option<JoinHandle<IOResult>>>>,
result: Arc<Mutex<Option<IOResult>>>,
completed: Arc<Mutex<bool>>,
}
impl AsyncIOTask {
pub fn new(handle: JoinHandle<std::result::Result<(), String>>) -> Self {
let wrapped_handle = runtime().spawn(async move {
match handle.await {
Ok(Ok(())) => Ok(IOResponse::Ok),
Ok(Err(e)) => Err(e),
Err(e) => Err(format!("Task panicked: {}", e)),
}
});
Self {
handle: Arc::new(Mutex::new(Some(wrapped_handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
}
}
pub fn from_string_handle(handle: JoinHandle<std::result::Result<String, String>>) -> Self {
let wrapped_handle = runtime().spawn(async move {
match handle.await {
Ok(Ok(s)) => Ok(IOResponse::String(s)),
Ok(Err(e)) => Err(e),
Err(e) => Err(format!("Task panicked: {}", e)),
}
});
Self {
handle: Arc::new(Mutex::new(Some(wrapped_handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
}
}
pub fn from_json_handle(
handle: JoinHandle<std::result::Result<serde_json::Value, String>>,
) -> Self {
let wrapped_handle = runtime().spawn(async move {
match handle.await {
Ok(Ok(v)) => Ok(IOResponse::Json(v)),
Ok(Err(e)) => Err(e),
Err(e) => Err(format!("Task panicked: {}", e)),
}
});
Self {
handle: Arc::new(Mutex::new(Some(wrapped_handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
}
}
pub fn from_bool_handle(handle: JoinHandle<std::result::Result<bool, String>>) -> Self {
let wrapped_handle = runtime().spawn(async move {
match handle.await {
Ok(Ok(b)) => Ok(IOResponse::Bool(b)),
Ok(Err(e)) => Err(e),
Err(e) => Err(format!("Task panicked: {}", e)),
}
});
Self {
handle: Arc::new(Mutex::new(Some(wrapped_handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
}
}
}
impl Clone for AsyncIOTask {
fn clone(&self) -> Self {
Self {
handle: Arc::clone(&self.handle),
result: Arc::clone(&self.result),
completed: Arc::clone(&self.completed),
}
}
}
impl UserData for AsyncIOTask {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("is_completed", |_, this, _: ()| Ok(*this.completed.lock()));
}
}
type LuaFnResult = std::result::Result<super::portable::LuaPortable, String>;
pub struct AsyncLuaTask {
func_key: Arc<Mutex<Option<mlua::RegistryKey>>>,
result: Arc<Mutex<Option<LuaFnResult>>>,
completed: Arc<Mutex<bool>>,
}
impl Clone for AsyncLuaTask {
fn clone(&self) -> Self {
Self {
func_key: Arc::clone(&self.func_key),
result: Arc::clone(&self.result),
completed: Arc::clone(&self.completed),
}
}
}
impl UserData for AsyncLuaTask {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("is_completed", |_, this, _: ()| Ok(*this.completed.lock()));
}
}
impl FetchResponse {
fn to_lua_table(&self, lua: &Lua) -> Result<Table> {
let table = lua.create_table()?;
table.set("status", self.status)?;
table.set("body", self.body.clone())?;
table.set("ok", self.ok)?;
let headers_table = lua.create_table()?;
for (k, v) in &self.headers {
headers_table.set(k.clone(), v.clone())?;
}
table.set("headers", headers_table)?;
let body_clone = self.body.clone();
let json_fn = lua.create_function(move |lua, _: ()| {
match serde_json::from_str::<serde_json::Value>(&body_clone) {
Ok(v) => lua.to_value(&v),
Err(e) => Err(mlua::Error::RuntimeError(format!(
"Failed to parse JSON: {}",
e
))),
}
})?;
table.set("json", json_fn)?;
Ok(table)
}
}
async fn do_fetch(url: &str, opts: &FetchOptions) -> FetchResult {
let client = reqwest::Client::new();
let mut builder = match opts.method {
Method::Get => client.get(url),
Method::Post => client.post(url),
Method::Put => client.put(url),
Method::Delete => client.delete(url),
Method::Patch => client.patch(url),
Method::Head => client.head(url),
};
for (k, v) in &opts.headers {
builder = builder.header(k, v);
}
if let Some(body) = &opts.body {
builder = builder.body(body.clone());
}
if let Some(timeout) = opts.timeout_secs {
builder = builder.timeout(std::time::Duration::from_secs(timeout));
}
let response = builder.send().await.map_err(|e| e.to_string())?;
let status = response.status().as_u16();
let ok = response.status().is_success();
let mut headers = HashMap::new();
for (k, v) in response.headers() {
if let Ok(v_str) = v.to_str() {
headers.insert(k.to_string(), v_str.to_string());
}
}
let body = response.text().await.map_err(|e| e.to_string())?;
Ok(FetchResponse {
status,
headers,
body,
ok,
})
}
async fn do_fetch_bytes(
url: &str,
opts: &FetchOptions,
) -> std::result::Result<(u16, bool, Vec<u8>), String> {
let client = reqwest::Client::new();
let mut builder = match opts.method {
Method::Get => client.get(url),
Method::Post => client.post(url),
Method::Put => client.put(url),
Method::Delete => client.delete(url),
Method::Patch => client.patch(url),
Method::Head => client.head(url),
};
for (k, v) in &opts.headers {
builder = builder.header(k, v);
}
if let Some(body) = &opts.body {
builder = builder.body(body.clone());
}
if let Some(timeout) = opts.timeout_secs {
builder = builder.timeout(std::time::Duration::from_secs(timeout));
}
let response = builder.send().await.map_err(|e| e.to_string())?;
let status = response.status().as_u16();
let ok = response.status().is_success();
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
Ok((status, ok, bytes.to_vec()))
}
use std::path::Path;
use super::helpers::{is_path_within_root, resolve_path};
use crate::tracker::SharedTracker;
pub fn create_module(
lua: &Lua,
project_root: &Path,
sandbox: bool,
tracker: SharedTracker,
) -> Result<Table> {
let async_module = lua.create_table()?;
let root = project_root.to_path_buf();
let root_clone = root.clone();
let fetch = lua.create_function(move |lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(lua, t)?
} else {
FetchOptions::default()
};
let cache_path = match &opts.cache {
CacheOption::Disabled => None,
CacheOption::Auto => Some(get_cache_path(&url, &root_clone)),
CacheOption::Path(p) => Some(if p.is_absolute() {
p.clone()
} else {
root_clone.join(p)
}),
};
if let Some(ref path) = cache_path
&& let Some(cached) = read_cache(path)
&& let Ok(body) = String::from_utf8(cached)
{
let response = FetchResponse {
status: 200,
headers: HashMap::new(),
body,
ok: true,
};
let table = response.to_lua_table(lua)?;
return Ok(Value::Table(table));
}
let result = block_on(do_fetch(&url, &opts));
match result {
Ok(response) => {
if response.ok
&& let Some(ref path) = cache_path
{
let _ = write_cache(path, response.body.as_bytes());
}
let table = response.to_lua_table(lua)?;
Ok(Value::Table(table))
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e))),
}
})?;
async_module.set("fetch_sync", fetch)?;
let fetch_async = lua.create_function(|_lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(_lua, t)?
} else {
FetchOptions::default()
};
let handle = runtime().spawn(async move { do_fetch(&url, &opts).await });
let task = AsyncTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
};
Ok(task)
})?;
async_module.set("fetch", fetch_async)?;
let fetch_json = lua.create_function(|lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let mut opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(lua, t)?
} else {
FetchOptions::default()
};
opts.headers
.entry("Accept".to_string())
.or_insert_with(|| "application/json".to_string());
let result = block_on(do_fetch(&url, &opts));
match result {
Ok(response) => {
if !response.ok {
return Err(mlua::Error::RuntimeError(format!(
"HTTP {} for {}",
response.status, url
)));
}
match serde_json::from_str::<serde_json::Value>(&response.body) {
Ok(v) => lua.to_value(&v),
Err(e) => Err(mlua::Error::RuntimeError(format!(
"Failed to parse JSON from {}: {}",
url, e
))),
}
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e))),
}
})?;
async_module.set("fetch_json", fetch_json)?;
let root_clone = root.clone();
let fetch_bytes = lua.create_function(move |lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(lua, t)?
} else {
FetchOptions::default()
};
let cache_path = match &opts.cache {
CacheOption::Disabled => None,
CacheOption::Auto => Some(get_cache_path(&url, &root_clone)),
CacheOption::Path(p) => Some(if p.is_absolute() {
p.clone()
} else {
root_clone.join(p)
}),
};
if let Some(ref path) = cache_path
&& let Some(cached) = read_cache(path)
{
let table = lua.create_table()?;
table.set("status", 200u16)?;
table.set("ok", true)?;
table.set("body", lua.create_string(&cached)?)?;
return Ok(Value::Table(table));
}
let result = block_on(do_fetch_bytes(&url, &opts));
match result {
Ok((status, ok, bytes)) => {
if ok && let Some(ref path) = cache_path {
let _ = write_cache(path, &bytes);
}
let table = lua.create_table()?;
table.set("status", status)?;
table.set("ok", ok)?;
table.set("body", lua.create_string(&bytes)?)?;
Ok(Value::Table(table))
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e))),
}
})?;
async_module.set("fetch_bytes_sync", fetch_bytes)?;
let root_clone = root.clone();
let fetch_bytes_async = lua.create_function(move |lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(lua, t)?
} else {
FetchOptions::default()
};
let root_for_cache = root_clone.clone();
let handle = runtime().spawn(async move {
let cache_path = match &opts.cache {
CacheOption::Disabled => None,
CacheOption::Auto => Some(get_cache_path(&url, &root_for_cache)),
CacheOption::Path(p) => Some(if p.is_absolute() {
p.clone()
} else {
root_for_cache.join(p)
}),
};
if let Some(ref path) = cache_path
&& let Some(cached) = read_cache(path)
{
return Ok(BytesFetchResponse {
status: 200,
body: cached,
ok: true,
});
}
match do_fetch_bytes(&url, &opts).await {
Ok((status, ok, bytes)) => {
if ok && let Some(ref path) = cache_path {
let _ = write_cache(path, &bytes);
}
Ok(BytesFetchResponse {
status,
body: bytes,
ok,
})
}
Err(e) => Err(e),
}
});
let task = AsyncBytesTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
};
Ok(task)
})?;
async_module.set("fetch_bytes", fetch_bytes_async)?;
let fetch_all = lua.create_function(|lua, requests: Table| {
let mut urls_and_opts: Vec<(String, FetchOptions)> = Vec::new();
for pair in requests.pairs::<Value, Value>() {
let (_, v) = pair?;
match v {
Value::String(s) => {
urls_and_opts.push((s.to_str()?.to_string(), FetchOptions::default()));
}
Value::Table(t) => {
let url: String = t.get("url").or_else(|_| t.get(1))?;
let opts = if let Ok(opts_table) = t.get::<Table>("options") {
FetchOptions::from_lua_table(lua, &opts_table)?
} else {
FetchOptions::default()
};
urls_and_opts.push((url, opts));
}
_ => {
return Err(mlua::Error::RuntimeError(
"fetch_all: expected string or table".to_string(),
));
}
}
}
let results = block_on(async {
let futures: Vec<_> = urls_and_opts
.iter()
.map(|(url, opts)| do_fetch(url, opts))
.collect();
futures::future::join_all(futures).await
});
let result_table = lua.create_table()?;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(response) => {
let table = response.to_lua_table(lua)?;
result_table.set(i + 1, table)?;
}
Err(e) => {
let err_table = lua.create_table()?;
err_table.set("ok", false)?;
err_table.set("error", e)?;
result_table.set(i + 1, err_table)?;
}
}
}
Ok(Value::Table(result_table))
})?;
async_module.set("fetch_all", fetch_all)?;
let spawn = lua.create_function(|lua, args: (String, Option<Table>)| {
let (url, opts_table) = args;
let opts = if let Some(ref t) = opts_table {
FetchOptions::from_lua_table(lua, t)?
} else {
FetchOptions::default()
};
let handle = runtime().spawn(async move { do_fetch(&url, &opts).await });
let task = AsyncTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
};
Ok(task)
})?;
async_module.set("spawn", spawn)?;
let wrap_fn = lua.create_function(|lua, func: Function| {
let key = lua.create_registry_value(func)?;
Ok(AsyncLuaTask {
func_key: Arc::new(Mutex::new(Some(key))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("wrap", wrap_fn)?;
let await_fn = lua.create_function(|lua, ud: mlua::AnyUserData| {
if let Ok(task) = ud.borrow::<AsyncTask>() {
if *task.completed.lock()
&& let Some(ref result) = *task.result.lock()
{
return match result {
Ok(response) => {
let table = response.to_lua_table(lua)?;
Ok(Value::Table(table))
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e))),
};
}
let handle = task.handle.lock().take();
drop(task);
if let Some(h) = handle {
let result = block_on(h);
let task = ud.borrow::<AsyncTask>()?;
match result {
Ok(fetch_result) => {
*task.completed.lock() = true;
*task.result.lock() = Some(fetch_result.clone());
match fetch_result {
Ok(response) => {
let table = response.to_lua_table(lua)?;
Ok(Value::Table(table))
}
Err(e) => {
Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e)))
}
}
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Task panicked: {}", e))),
}
} else {
Err(mlua::Error::RuntimeError(
"Task handle already consumed".to_string(),
))
}
}
else if let Ok(task) = ud.borrow::<AsyncBytesTask>() {
if *task.completed.lock()
&& let Some(ref result) = *task.result.lock()
{
return match result {
Ok(response) => {
let table = response.to_lua_table(lua)?;
Ok(Value::Table(table))
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e))),
};
}
let handle = task.handle.lock().take();
drop(task);
if let Some(h) = handle {
let result = block_on(h);
let task = ud.borrow::<AsyncBytesTask>()?;
match result {
Ok(fetch_result) => {
*task.completed.lock() = true;
*task.result.lock() = Some(fetch_result.clone());
match fetch_result {
Ok(response) => {
let table = response.to_lua_table(lua)?;
Ok(Value::Table(table))
}
Err(e) => {
Err(mlua::Error::RuntimeError(format!("Fetch failed: {}", e)))
}
}
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Task panicked: {}", e))),
}
} else {
Err(mlua::Error::RuntimeError(
"Task handle already consumed".to_string(),
))
}
}
else if let Ok(task) = ud.borrow::<AsyncIOTask>() {
if *task.completed.lock()
&& let Some(ref result) = *task.result.lock()
{
return match result {
Ok(response) => response.to_lua_value(lua),
Err(e) => Err(mlua::Error::RuntimeError(format!("I/O failed: {}", e))),
};
}
let handle = task.handle.lock().take();
drop(task);
if let Some(h) = handle {
let result = block_on(h);
let task = ud.borrow::<AsyncIOTask>()?;
match result {
Ok(io_result) => {
*task.completed.lock() = true;
*task.result.lock() = Some(io_result.clone());
match io_result {
Ok(response) => response.to_lua_value(lua),
Err(e) => Err(mlua::Error::RuntimeError(format!("I/O failed: {}", e))),
}
}
Err(e) => Err(mlua::Error::RuntimeError(format!("Task panicked: {}", e))),
}
} else {
Err(mlua::Error::RuntimeError(
"Task handle already consumed".to_string(),
))
}
}
else if let Ok(task) = ud.borrow::<AsyncLuaTask>() {
if *task.completed.lock()
&& let Some(ref result) = *task.result.lock()
{
return match result {
Ok(portable) => portable.to_lua(lua),
Err(e) => Err(mlua::Error::RuntimeError(format!(
"Lua function failed: {}",
e
))),
};
}
let func_key = task.func_key.lock().take();
drop(task);
if let Some(key) = func_key {
let func: Function = lua.registry_value(&key)?;
let result: Value = func.call(())?;
let portable_result =
super::portable::LuaPortable::from_lua(&result, lua).map_err(|e| e.to_string());
let task = ud.borrow::<AsyncLuaTask>()?;
*task.completed.lock() = true;
*task.result.lock() = Some(portable_result.clone());
lua.remove_registry_value(key)?;
match portable_result {
Ok(portable) => portable.to_lua(lua),
Err(e) => Err(mlua::Error::RuntimeError(format!(
"Lua function failed: {}",
e
))),
}
} else {
Err(mlua::Error::RuntimeError(
"Lua function already executed".to_string(),
))
}
} else {
Err(mlua::Error::RuntimeError(
"Expected AsyncTask, AsyncBytesTask, AsyncIOTask, or AsyncLuaTask".to_string(),
))
}
})?;
async_module.raw_set("await", await_fn)?;
let await_all = lua.create_function(|lua, tasks_table: Table| {
enum TaskType {
Text(Option<JoinHandle<FetchResult>>, Option<FetchResult>),
Bytes(
Option<JoinHandle<BytesFetchResult>>,
Option<BytesFetchResult>,
),
IO(Option<JoinHandle<IOResult>>, Option<IOResult>),
LuaFn(Option<mlua::RegistryKey>, Option<LuaFnResult>),
}
let mut tasks: Vec<(mlua::AnyUserData, TaskType)> = Vec::new();
for pair in tasks_table.pairs::<i64, mlua::AnyUserData>() {
let (_, ud) = pair?;
if let Ok(task) = ud.borrow::<AsyncTask>() {
let completed_result = if *task.completed.lock() {
task.result.lock().clone()
} else {
None
};
let handle = task.handle.lock().take();
drop(task);
tasks.push((ud, TaskType::Text(handle, completed_result)));
} else if let Ok(task) = ud.borrow::<AsyncBytesTask>() {
let completed_result = if *task.completed.lock() {
task.result.lock().clone()
} else {
None
};
let handle = task.handle.lock().take();
drop(task);
tasks.push((ud, TaskType::Bytes(handle, completed_result)));
} else if let Ok(task) = ud.borrow::<AsyncIOTask>() {
let completed_result = if *task.completed.lock() {
task.result.lock().clone()
} else {
None
};
let handle = task.handle.lock().take();
drop(task);
tasks.push((ud, TaskType::IO(handle, completed_result)));
} else if let Ok(task) = ud.borrow::<AsyncLuaTask>() {
let completed_result = if *task.completed.lock() {
task.result.lock().clone()
} else {
None
};
let func_key = task.func_key.lock().take();
drop(task);
tasks.push((ud, TaskType::LuaFn(func_key, completed_result)));
} else {
return Err(mlua::Error::RuntimeError(
"await_all: expected AsyncTask, AsyncBytesTask, AsyncIOTask, or AsyncLuaTask"
.to_string(),
));
}
}
let async_results: Vec<_> = block_on(async {
let mut results = Vec::with_capacity(tasks.len());
for (_, task_type) in &mut tasks {
match task_type {
TaskType::Text(handle, completed) => {
if let Some(result) = completed.take() {
results.push(TaskType::Text(None, Some(result)));
} else if let Some(h) = handle.take() {
match h.await {
Ok(result) => results.push(TaskType::Text(None, Some(result))),
Err(e) => results.push(TaskType::Text(
None,
Some(Err(format!("Task panicked: {}", e))),
)),
}
} else {
results.push(TaskType::Text(None, None));
}
}
TaskType::Bytes(handle, completed) => {
if let Some(result) = completed.take() {
results.push(TaskType::Bytes(None, Some(result)));
} else if let Some(h) = handle.take() {
match h.await {
Ok(result) => results.push(TaskType::Bytes(None, Some(result))),
Err(e) => results.push(TaskType::Bytes(
None,
Some(Err(format!("Task panicked: {}", e))),
)),
}
} else {
results.push(TaskType::Bytes(None, None));
}
}
TaskType::IO(handle, completed) => {
if let Some(result) = completed.take() {
results.push(TaskType::IO(None, Some(result)));
} else if let Some(h) = handle.take() {
match h.await {
Ok(result) => results.push(TaskType::IO(None, Some(result))),
Err(e) => results.push(TaskType::IO(
None,
Some(Err(format!("Task panicked: {}", e))),
)),
}
} else {
results.push(TaskType::IO(None, None));
}
}
TaskType::LuaFn(key, completed) => {
results.push(TaskType::LuaFn(key.take(), completed.take()));
}
}
}
results
});
let mut final_results = Vec::with_capacity(async_results.len());
for result in async_results {
match result {
TaskType::LuaFn(Some(key), None) => {
let func_result: LuaFnResult = (|| {
let func: Function = lua.registry_value(&key)?;
let result: Value = func.call(())?;
let portable = super::portable::LuaPortable::from_lua(&result, lua)?;
lua.remove_registry_value(key)?;
Ok(portable)
})()
.map_err(|e: mlua::Error| e.to_string());
final_results.push(TaskType::LuaFn(None, Some(func_result)));
}
TaskType::LuaFn(None, Some(completed)) => {
final_results.push(TaskType::LuaFn(None, Some(completed)));
}
TaskType::LuaFn(Some(key), Some(_)) => {
let _ = lua.remove_registry_value(key);
final_results.push(TaskType::LuaFn(None, None));
}
other => final_results.push(other),
}
}
let result_table = lua.create_table()?;
for (i, result) in final_results.into_iter().enumerate() {
match result {
TaskType::Text(_, Some(Ok(response))) => {
let table = response.to_lua_table(lua)?;
result_table.set(i + 1, table)?;
}
TaskType::Bytes(_, Some(Ok(response))) => {
let table = response.to_lua_table(lua)?;
result_table.set(i + 1, table)?;
}
TaskType::IO(_, Some(Ok(response))) => {
let value = response.to_lua_value(lua)?;
result_table.set(i + 1, value)?;
}
TaskType::LuaFn(_, Some(Ok(portable))) => {
let value = portable.to_lua(lua)?;
result_table.set(i + 1, value)?;
}
TaskType::Text(_, Some(Err(e)))
| TaskType::Bytes(_, Some(Err(e)))
| TaskType::IO(_, Some(Err(e)))
| TaskType::LuaFn(_, Some(Err(e))) => {
let err_table = lua.create_table()?;
err_table.set("ok", false)?;
err_table.set("error", e)?;
result_table.set(i + 1, err_table)?;
}
_ => {
result_table.set(i + 1, Value::Nil)?;
}
}
}
Ok(Value::Table(result_table))
})?;
async_module.set("await_all", await_all)?;
let root_clone = root.clone();
let tracker_clone = tracker.clone();
let write_file = lua.create_function(move |_, (path, content): (String, String)| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot write '{}' outside project directory",
path
)));
}
let tracker = tracker_clone.clone();
let content_bytes = content.clone().into_bytes();
let handle = runtime().spawn(async move {
if let Some(parent) = resolved.parent() {
tokio::fs::create_dir_all(parent).await.ok();
}
match tokio::fs::write(&resolved, &content).await {
Ok(()) => {
let canonical = resolved.canonicalize().unwrap_or(resolved);
tracker.record_write_async(canonical, &content_bytes);
Ok(IOResponse::Ok)
}
Err(e) => Err(format!("Failed to write file: {}", e)),
}
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("write_file", write_file)?;
let root_clone = root.clone();
let tracker_clone = tracker.clone();
let write = lua.create_function(move |_, (path, data): (String, mlua::String)| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot write '{}' outside project directory",
path
)));
}
let bytes = data.as_bytes().to_vec();
let tracker = tracker_clone.clone();
let handle = runtime().spawn(async move {
if let Some(parent) = resolved.parent() {
tokio::fs::create_dir_all(parent).await.ok();
}
match tokio::fs::write(&resolved, &bytes).await {
Ok(()) => {
let canonical = resolved.canonicalize().unwrap_or(resolved);
tracker.record_write_async(canonical, &bytes);
Ok(IOResponse::Ok)
}
Err(e) => Err(format!("Failed to write file: {}", e)),
}
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("write", write)?;
let root_clone = root.clone();
let exists = lua.create_function(move |_lua, path: String| {
let resolved = resolve_path(&path, &root_clone);
let handle = runtime()
.spawn(async move { Ok(tokio::fs::try_exists(&resolved).await.unwrap_or(false)) });
Ok(AsyncIOTask::from_bool_handle(handle))
})?;
async_module.set("exists", exists)?;
let root_clone = root.clone();
let sandbox_clone = sandbox;
let read_file = lua.create_function(move |_lua, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox_clone && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
match tokio::fs::read_to_string(&resolved).await {
Ok(content) => Ok(IOResponse::String(content)),
Err(e) => Err(format!("Failed to read file: {}", e)),
}
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("read_file", read_file)?;
let root_clone = root.clone();
let read_files = lua.create_function(move |_lua, paths: Vec<String>| {
let resolved_paths: Vec<PathBuf> =
paths.iter().map(|p| resolve_path(p, &root_clone)).collect();
if sandbox {
for (i, resolved) in resolved_paths.iter().enumerate() {
if !is_path_within_root(resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
paths[i]
)));
}
}
}
let handle = runtime().spawn(async move {
let futures: Vec<_> = resolved_paths
.iter()
.map(tokio::fs::read_to_string)
.collect();
let results = futures::future::join_all(futures).await;
let contents: Vec<serde_json::Value> = results
.into_iter()
.map(|r| match r {
Ok(s) => serde_json::Value::String(s),
Err(_) => serde_json::Value::Null,
})
.collect();
Ok(serde_json::Value::Array(contents))
});
Ok(AsyncIOTask::from_json_handle(handle))
})?;
async_module.set("read_files", read_files)?;
let root_clone = root.clone();
let load_json = lua.create_function(move |_lua, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
let content = tokio::fs::read_to_string(&resolved)
.await
.map_err(|e| format!("Failed to read file: {}", e))?;
serde_json::from_str::<serde_json::Value>(&content)
.map_err(|e| format!("Failed to parse JSON: {}", e))
});
Ok(AsyncIOTask::from_json_handle(handle))
})?;
async_module.set("load_json", load_json)?;
let root_clone = root.clone();
let tracker_clone = tracker.clone();
let copy_file = lua.create_function(move |_, (src, dst): (String, String)| {
let src_resolved = resolve_path(&src, &root_clone);
let dst_resolved = resolve_path(&dst, &root_clone);
if sandbox {
if !is_path_within_root(&src_resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
src
)));
}
if !is_path_within_root(&dst_resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot write '{}' outside project directory",
dst
)));
}
}
let tracker = tracker_clone.clone();
let handle = runtime().spawn(async move {
if let Some(parent) = dst_resolved.parent() {
tokio::fs::create_dir_all(parent).await.ok();
}
let content = tokio::fs::read(&src_resolved).await;
match tokio::fs::copy(&src_resolved, &dst_resolved).await {
Ok(bytes) => {
if let Ok(ref content) = content {
let src_canonical = src_resolved.canonicalize().unwrap_or(src_resolved);
tracker.record_read_async(src_canonical, content);
let dst_canonical = dst_resolved.canonicalize().unwrap_or(dst_resolved);
tracker.record_write_async(dst_canonical, content);
}
Ok(IOResponse::Bytes(bytes))
}
Err(e) => Err(format!("Failed to copy: {}", e)),
}
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("copy_file", copy_file)?;
let root_clone = root.clone();
let rename = lua.create_function(move |_, (src, dst): (String, String)| {
let src_resolved = resolve_path(&src, &root_clone);
let dst_resolved = resolve_path(&dst, &root_clone);
if sandbox {
if !is_path_within_root(&src_resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
src
)));
}
if !is_path_within_root(&dst_resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot write '{}' outside project directory",
dst
)));
}
}
let handle = runtime().spawn(async move {
if let Some(parent) = dst_resolved.parent() {
tokio::fs::create_dir_all(parent).await.ok();
}
tokio::fs::rename(&src_resolved, &dst_resolved)
.await
.map(|()| IOResponse::Ok)
.map_err(|e| format!("Failed to rename: {}", e))
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("rename", rename)?;
let root_clone = root.clone();
let create_dir = lua.create_function(move |_, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot create '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
match tokio::fs::create_dir_all(&resolved).await {
Ok(()) => Ok(IOResponse::Ok),
Err(e) => Err(format!("Failed to create directory: {}", e)),
}
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("create_dir", create_dir)?;
let root_clone = root.clone();
let remove_file = lua.create_function(move |_, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot remove '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
tokio::fs::remove_file(&resolved)
.await
.map(|()| IOResponse::Ok)
.map_err(|e| format!("Failed to remove file: {}", e))
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("remove_file", remove_file)?;
let root_clone = root.clone();
let remove_dir = lua.create_function(move |_, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot remove '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
tokio::fs::remove_dir_all(&resolved)
.await
.map(|()| IOResponse::Ok)
.map_err(|e| format!("Failed to remove directory: {}", e))
});
Ok(AsyncIOTask {
handle: Arc::new(Mutex::new(Some(handle))),
result: Arc::new(Mutex::new(None)),
completed: Arc::new(Mutex::new(false)),
})
})?;
async_module.set("remove_dir", remove_dir)?;
let root_clone = root.clone();
let metadata = lua.create_function(move |_lua, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
let meta = tokio::fs::metadata(&resolved)
.await
.map_err(|e| format!("Failed to get metadata: {}", e))?;
let modified = meta
.modified()
.ok()
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
Ok(serde_json::json!({
"is_file": meta.is_file(),
"is_dir": meta.is_dir(),
"len": meta.len(),
"readonly": meta.permissions().readonly(),
"modified": modified,
}))
});
Ok(AsyncIOTask::from_json_handle(handle))
})?;
async_module.set("metadata", metadata)?;
let root_clone = root.clone();
let sandbox_clone = sandbox;
let read_dir = lua.create_function(move |_lua, path: String| {
let resolved = resolve_path(&path, &root_clone);
let root_for_filter = root_clone.clone();
if sandbox_clone && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
let mut entries = Vec::new();
let mut dir = tokio::fs::read_dir(&resolved)
.await
.map_err(|e| format!("Failed to read directory: {}", e))?;
while let Some(entry) = dir
.next_entry()
.await
.map_err(|e| format!("Failed to read entry: {}", e))?
{
let entry_path = entry.path();
if sandbox_clone && !is_path_within_root(&entry_path, &root_for_filter) {
continue;
}
let file_type = entry
.file_type()
.await
.map_err(|e| format!("Failed to get file type: {}", e))?;
entries.push(serde_json::json!({
"path": entry_path.to_string_lossy(),
"name": entry.file_name().to_string_lossy(),
"is_file": file_type.is_file(),
"is_dir": file_type.is_dir(),
"is_symlink": file_type.is_symlink(),
}));
}
Ok(serde_json::Value::Array(entries))
});
Ok(AsyncIOTask::from_json_handle(handle))
})?;
async_module.set("read_dir", read_dir)?;
let root_clone = root.clone();
let canonicalize = lua.create_function(move |_, path: String| {
let resolved = resolve_path(&path, &root_clone);
if sandbox && !is_path_within_root(&resolved, &root_clone) {
return Err(mlua::Error::RuntimeError(format!(
"Sandbox: cannot access '{}' outside project directory",
path
)));
}
let handle = runtime().spawn(async move {
tokio::fs::canonicalize(&resolved)
.await
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Failed to canonicalize: {}", e))
});
Ok(AsyncIOTask::from_string_handle(handle))
})?;
async_module.set("canonicalize", canonicalize)?;
Ok(async_module)
}