mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
|
df56cd58e7 | ||
|
66e3b58cd7 | ||
|
fb33d1812d | ||
|
0ddaaaefb5 | ||
|
2e5b3bb5eb | ||
|
6645631c46 | ||
|
120048ae95 | ||
|
2d8e58b028 |
19 changed files with 734 additions and 154 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -8,6 +8,20 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for non-UTF8 strings in arguments to `process.exec` and `process.spawn`
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved cross-platform compatibility and correctness for values in `process.args` and `process.env`, especially on Windows
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed various crashes during require that had the error `cannot mutably borrow app data container`
|
||||
|
||||
## `0.9.2` - April 30th, 2025
|
||||
|
||||
### Changed
|
||||
|
|
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1764,6 +1764,7 @@ dependencies = [
|
|||
"async-tungstenite",
|
||||
"blocking",
|
||||
"bstr",
|
||||
"form_urlencoded",
|
||||
"futures",
|
||||
"futures-lite",
|
||||
"futures-rustls",
|
||||
|
@ -1798,7 +1799,6 @@ dependencies = [
|
|||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler",
|
||||
"os_str_bytes",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
|
@ -1880,6 +1880,7 @@ dependencies = [
|
|||
"console",
|
||||
"dunce",
|
||||
"mlua",
|
||||
"os_str_bytes",
|
||||
"parking_lot",
|
||||
"path-clean",
|
||||
"pathdiff",
|
||||
|
|
|
@ -24,6 +24,7 @@ async-net = "2.0"
|
|||
async-tungstenite = "0.29"
|
||||
blocking = "1.6"
|
||||
bstr = "1.9"
|
||||
form_urlencoded = "1.2"
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
futures-lite = "2.6"
|
||||
futures-rustls = "0.26"
|
||||
|
|
|
@ -47,7 +47,7 @@ pub async fn send_request(mut request: Request, lua: Lua) -> LuaResult<Response>
|
|||
.uri()
|
||||
.to_string()
|
||||
.parse::<Url>()
|
||||
.expect("uri is valid");
|
||||
.into_lua_err()?;
|
||||
|
||||
// Some headers are required by most if not
|
||||
// all servers, make sure those are present...
|
||||
|
|
|
@ -110,15 +110,18 @@ impl Request {
|
|||
*/
|
||||
pub fn query(&self) -> HashMap<String, Vec<String>> {
|
||||
let uri = self.inner.uri();
|
||||
let url = uri.to_string().parse::<Url>().expect("uri is valid");
|
||||
|
||||
let mut result = HashMap::<String, Vec<String>>::new();
|
||||
for (key, value) in url.query_pairs() {
|
||||
result
|
||||
.entry(key.into_owned())
|
||||
.or_default()
|
||||
.push(value.into_owned());
|
||||
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
result
|
||||
.entry(key.to_string())
|
||||
.or_default()
|
||||
.push(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
|
|||
|
||||
directories = "6.0"
|
||||
pin-project = "1.0"
|
||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||
|
||||
bstr = "1.9"
|
||||
bytes = "1.6.0"
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::{
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
env::consts::{ARCH, OS},
|
||||
path::MAIN_SEPARATOR,
|
||||
process::Stdio,
|
||||
};
|
||||
|
@ -12,9 +9,11 @@ use std::{
|
|||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::Functions;
|
||||
|
||||
use os_str_bytes::RawOsString;
|
||||
|
||||
use lune_utils::{path::get_current_dir, TableBuilder};
|
||||
use lune_utils::{
|
||||
path::get_current_dir,
|
||||
process::{ProcessArgs, ProcessEnv},
|
||||
TableBuilder,
|
||||
};
|
||||
|
||||
mod create;
|
||||
mod exec;
|
||||
|
@ -58,25 +57,15 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
"little"
|
||||
})?;
|
||||
|
||||
// Create readonly args array
|
||||
let args_vec = lua
|
||||
.app_data_ref::<Vec<String>>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
|
||||
// Extract stored userdatas for args + env, the runtime struct should always provide this
|
||||
let process_args = lua
|
||||
.app_data_ref::<ProcessArgs>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing process args in Lua app data"))?
|
||||
.clone();
|
||||
let process_env = lua
|
||||
.app_data_ref::<ProcessEnv>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing process env in Lua app data"))?
|
||||
.clone();
|
||||
let args_tab = TableBuilder::new(lua.clone())?
|
||||
.with_sequential_values(args_vec)?
|
||||
.build_readonly()?;
|
||||
|
||||
// Create proxied table for env that gets & sets real env vars
|
||||
let env_tab = TableBuilder::new(lua.clone())?
|
||||
.with_metatable(
|
||||
TableBuilder::new(lua.clone())?
|
||||
.with_function(LuaMetaMethod::Index.name(), process_env_get)?
|
||||
.with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
|
||||
.with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
|
||||
.build_readonly()?,
|
||||
)?
|
||||
.build_readonly()?;
|
||||
|
||||
// Create our process exit function, the scheduler crate provides this
|
||||
let fns = Functions::new(lua.clone())?;
|
||||
|
@ -87,73 +76,18 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
.with_value("os", os)?
|
||||
.with_value("arch", arch)?
|
||||
.with_value("endianness", endianness)?
|
||||
.with_value("args", args_tab)?
|
||||
.with_value("args", process_args)?
|
||||
.with_value("cwd", cwd_str)?
|
||||
.with_value("env", env_tab)?
|
||||
.with_value("env", process_env)?
|
||||
.with_value("exit", process_exit)?
|
||||
.with_async_function("exec", process_exec)?
|
||||
.with_function("create", process_create)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
fn process_env_get(lua: &Lua, (_, key): (LuaValue, String)) -> LuaResult<LuaValue> {
|
||||
match env::var_os(key) {
|
||||
Some(value) => {
|
||||
let raw_value = RawOsString::new(value);
|
||||
Ok(LuaValue::String(
|
||||
lua.create_string(raw_value.to_raw_bytes())?,
|
||||
))
|
||||
}
|
||||
None => Ok(LuaValue::Nil),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_env_set(_: &Lua, (_, key, value): (LuaValue, String, Option<String>)) -> LuaResult<()> {
|
||||
// Make sure key is valid, otherwise set_var will panic
|
||||
if key.is_empty() {
|
||||
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
|
||||
} else if key.contains('=') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Key must not contain the equals character '='".to_string(),
|
||||
))
|
||||
} else if key.contains('\0') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Key must not contain the NUL character".to_string(),
|
||||
))
|
||||
} else if let Some(value) = value {
|
||||
// Make sure value is valid, otherwise set_var will panic
|
||||
if value.contains('\0') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Value must not contain the NUL character".to_string(),
|
||||
))
|
||||
} else {
|
||||
env::set_var(&key, &value);
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
env::remove_var(&key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn process_env_iter(lua: &Lua, (_, ()): (LuaValue, ())) -> LuaResult<LuaFunction> {
|
||||
let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
|
||||
lua.create_function_mut(move |lua, (): ()| match vars.next() {
|
||||
Some((key, value)) => {
|
||||
let raw_key = RawOsString::new(key);
|
||||
let raw_value = RawOsString::new(value);
|
||||
Ok((
|
||||
LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
|
||||
LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
|
||||
))
|
||||
}
|
||||
None => Ok((LuaValue::Nil, LuaValue::Nil)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_exec(
|
||||
lua: Lua,
|
||||
(program, args, mut options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
(program, args, mut options): (String, ProcessArgs, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaTable> {
|
||||
let stdin = options.stdio.stdin.take();
|
||||
let stdout = options.stdio.stdout;
|
||||
|
@ -171,7 +105,7 @@ async fn process_exec(
|
|||
|
||||
fn process_create(
|
||||
lua: &Lua,
|
||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
(program, args, options): (String, ProcessArgs, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaValue> {
|
||||
let child = options
|
||||
.into_command(program, args)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
env::{self},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use lune_utils::process::ProcessArgs;
|
||||
use mlua::prelude::*;
|
||||
|
||||
use async_process::Command;
|
||||
|
@ -129,31 +131,24 @@ impl FromLua for ProcessSpawnOptions {
|
|||
}
|
||||
|
||||
impl ProcessSpawnOptions {
|
||||
pub fn into_command(self, program: impl Into<String>, args: Option<Vec<String>>) -> Command {
|
||||
let mut program = program.into();
|
||||
pub fn into_command(self, program: impl Into<OsString>, args: ProcessArgs) -> Command {
|
||||
let mut program: OsString = program.into();
|
||||
let mut args = args.into_iter().collect::<Vec<_>>();
|
||||
|
||||
// Run a shell using the command param if wanted
|
||||
let pargs = match self.shell {
|
||||
None => args,
|
||||
Some(shell) => {
|
||||
let shell_args = match args {
|
||||
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
|
||||
None => vec!["-c".to_string(), program.to_string()],
|
||||
};
|
||||
program = shell.to_string();
|
||||
Some(shell_args)
|
||||
if let Some(shell) = self.shell {
|
||||
let mut shell_command = program.clone();
|
||||
for arg in args {
|
||||
shell_command.push(" ");
|
||||
shell_command.push(arg);
|
||||
}
|
||||
};
|
||||
args = vec![OsString::from("-c"), shell_command];
|
||||
program = shell.into();
|
||||
}
|
||||
|
||||
// Create command with the wanted options
|
||||
let mut cmd = match pargs {
|
||||
None => Command::new(program),
|
||||
Some(args) => {
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
cmd
|
||||
}
|
||||
};
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
|
||||
// Set dir to run in and env variables
|
||||
if let Some(cwd) = self.cwd {
|
||||
|
|
|
@ -35,7 +35,18 @@ pub fn create(lua: Lua) -> LuaResult<LuaValue> {
|
|||
3. The lua chunk we are require-ing from
|
||||
*/
|
||||
|
||||
let require_fn = lua.create_async_function(require)?;
|
||||
let require_fn = lua.create_async_function(|lua, (source, path)| {
|
||||
// NOTE: We need to make sure that the app data reference does not
|
||||
// live through the entire require call, to prevent panicking from
|
||||
// being unable to borrow other app data in the main body of scripts
|
||||
let context = {
|
||||
let context = lua
|
||||
.app_data_ref::<RequireContext>()
|
||||
.expect("Failed to get RequireContext from app data");
|
||||
context.clone()
|
||||
};
|
||||
require(lua, context, source, path)
|
||||
})?;
|
||||
let get_source_fn = lua.create_function(move |lua, (): ()| match lua.inspect_stack(2) {
|
||||
None => Err(LuaError::runtime(
|
||||
"Failed to get stack info for require source",
|
||||
|
@ -60,7 +71,12 @@ pub fn create(lua: Lua) -> LuaResult<LuaValue> {
|
|||
.into_lua(&lua)
|
||||
}
|
||||
|
||||
async fn require(lua: Lua, (source, path): (LuaString, LuaString)) -> LuaResult<LuaMultiValue> {
|
||||
async fn require(
|
||||
lua: Lua,
|
||||
context: RequireContext,
|
||||
source: LuaString,
|
||||
path: LuaString,
|
||||
) -> LuaResult<LuaMultiValue> {
|
||||
let source = source
|
||||
.to_str()
|
||||
.into_lua_err()
|
||||
|
@ -73,11 +89,6 @@ async fn require(lua: Lua, (source, path): (LuaString, LuaString)) -> LuaResult<
|
|||
.context("Failed to parse require path as string")?
|
||||
.to_string();
|
||||
|
||||
let context = lua
|
||||
.app_data_ref::<RequireContext>()
|
||||
.expect("Failed to get RequireContext from app data")
|
||||
.clone();
|
||||
|
||||
if let Some(builtin_name) = path.strip_prefix("@lune/").map(str::to_ascii_lowercase) {
|
||||
library::require(lua, &context, &builtin_name)
|
||||
} else if let Some(self_path) = path.strip_prefix("@self/") {
|
||||
|
|
|
@ -17,6 +17,7 @@ mlua = { version = "0.10.3", features = ["luau", "async"] }
|
|||
|
||||
console = "0.15"
|
||||
dunce = "1.0"
|
||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||
path-clean = "1.0"
|
||||
pathdiff = "0.2"
|
||||
parking_lot = "0.12.3"
|
||||
|
|
|
@ -4,8 +4,13 @@ mod table_builder;
|
|||
mod version_string;
|
||||
|
||||
pub mod fmt;
|
||||
pub mod jit;
|
||||
pub mod path;
|
||||
pub mod process;
|
||||
|
||||
pub use self::table_builder::TableBuilder;
|
||||
pub use self::version_string::get_version_string;
|
||||
|
||||
// TODO: Remove this in the next major semver
|
||||
pub mod jit {
|
||||
pub use super::process::ProcessJitEnablement as JitEnablement;
|
||||
}
|
||||
|
|
252
crates/lune-utils/src/process/args.rs
Normal file
252
crates/lune-utils/src/process/args.rs
Normal file
|
@ -0,0 +1,252 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
use std::{
|
||||
env::args_os,
|
||||
ffi::OsString,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use os_str_bytes::OsStringBytes;
|
||||
|
||||
// Inner (shared) struct
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProcessArgsInner {
|
||||
values: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl FromIterator<OsString> for ProcessArgsInner {
|
||||
fn from_iter<T: IntoIterator<Item = OsString>>(iter: T) -> Self {
|
||||
Self {
|
||||
values: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A struct that can be easily shared, stored in Lua app data,
|
||||
and that also guarantees the values are valid OS strings
|
||||
that can be used for process arguments.
|
||||
|
||||
Usable directly from Lua, implementing both `FromLua` and `LuaUserData`.
|
||||
|
||||
Also provides convenience methods for working with the arguments
|
||||
as either `OsString` or `Vec<u8>`, where using the latter implicitly
|
||||
converts to an `OsString` and fails if the conversion is not possible.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessArgs {
|
||||
inner: Arc<Mutex<ProcessArgsInner>>,
|
||||
}
|
||||
|
||||
impl ProcessArgs {
|
||||
#[must_use]
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(ProcessArgsInner::default())),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(args_os().collect())),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.is_empty()
|
||||
}
|
||||
|
||||
// OS strings
|
||||
|
||||
#[must_use]
|
||||
pub fn all(&self) -> Vec<OsString> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, index: usize) -> Option<OsString> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.get(index).cloned()
|
||||
}
|
||||
|
||||
pub fn set(&self, index: usize, val: impl Into<OsString>) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
if let Some(arg) = inner.values.get_mut(index) {
|
||||
*arg = val.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&self, val: impl Into<OsString>) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.push(val.into());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pop(&self) -> Option<OsString> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.pop()
|
||||
}
|
||||
|
||||
pub fn insert(&self, index: usize, val: impl Into<OsString>) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
if index <= inner.values.len() {
|
||||
inner.values.insert(index, val.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn remove(&self, index: usize) -> Option<OsString> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
if index < inner.values.len() {
|
||||
Some(inner.values.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes wrappers
|
||||
|
||||
#[must_use]
|
||||
pub fn all_bytes(&self) -> Vec<Vec<u8>> {
|
||||
self.all()
|
||||
.into_iter()
|
||||
.filter_map(OsString::into_io_vec)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_bytes(&self, index: usize) -> Option<Vec<u8>> {
|
||||
let val = self.get(index)?;
|
||||
val.into_io_vec()
|
||||
}
|
||||
|
||||
pub fn set_bytes(&self, index: usize, val: impl Into<Vec<u8>>) {
|
||||
if let Some(val_os) = OsString::from_io_vec(val.into()) {
|
||||
self.set(index, val_os);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_bytes(&self, val: impl Into<Vec<u8>>) {
|
||||
if let Some(val_os) = OsString::from_io_vec(val.into()) {
|
||||
self.push(val_os);
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pop_bytes(&self) -> Option<Vec<u8>> {
|
||||
self.pop().and_then(OsString::into_io_vec)
|
||||
}
|
||||
|
||||
pub fn insert_bytes(&self, index: usize, val: impl Into<Vec<u8>>) {
|
||||
if let Some(val_os) = OsString::from_io_vec(val.into()) {
|
||||
self.insert(index, val_os);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_bytes(&self, index: usize) -> Option<Vec<u8>> {
|
||||
self.remove(index).and_then(OsString::into_io_vec)
|
||||
}
|
||||
}
|
||||
|
||||
// Iterator implementations
|
||||
|
||||
impl IntoIterator for ProcessArgs {
|
||||
type Item = OsString;
|
||||
type IntoIter = std::vec::IntoIter<OsString>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<OsString>> FromIterator<S> for ProcessArgs {
|
||||
fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(iter.into_iter().map(Into::into).collect())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<OsString>> Extend<S> for ProcessArgs {
|
||||
fn extend<T: IntoIterator<Item = S>>(&mut self, iter: T) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.extend(iter.into_iter().map(Into::into));
|
||||
}
|
||||
}
|
||||
|
||||
// Lua implementations
|
||||
|
||||
impl FromLua for ProcessArgs {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::Nil = value {
|
||||
Ok(Self::from_iter([] as [OsString; 0]))
|
||||
} else if let LuaValue::Boolean(true) = value {
|
||||
Ok(Self::current())
|
||||
} else if let Some(u) = value.as_userdata().and_then(|u| u.borrow::<Self>().ok()) {
|
||||
Ok(u.clone())
|
||||
} else if let LuaValue::Table(arr) = value {
|
||||
let mut args = Vec::new();
|
||||
for pair in arr.pairs::<LuaValue, LuaValue>() {
|
||||
let val_res = pair.map(|p| p.1.clone());
|
||||
let val = super::lua_value_to_os_string(val_res, "ProcessArgs")?;
|
||||
|
||||
super::validate_os_value(&val)?;
|
||||
|
||||
args.push(val);
|
||||
}
|
||||
Ok(Self::from_iter(args))
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: String::from("ProcessArgs"),
|
||||
message: Some(format!(
|
||||
"Invalid type for process args - expected table or nil, got '{}'",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ProcessArgs {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, (): ()| Ok(this.len()));
|
||||
methods.add_meta_method(LuaMetaMethod::Index, |_, this, index: usize| {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(this.get(index - 1))
|
||||
}
|
||||
});
|
||||
methods.add_meta_method(LuaMetaMethod::NewIndex, |_, _, (): ()| {
|
||||
Err::<(), _>(LuaError::runtime("ProcessArgs is read-only"))
|
||||
});
|
||||
methods.add_meta_method(LuaMetaMethod::Iter, |lua, this, (): ()| {
|
||||
let mut vars = this
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(OsStringBytes::into_io_vec)
|
||||
.enumerate();
|
||||
lua.create_function_mut(move |lua, (): ()| match vars.next() {
|
||||
None => Ok((LuaValue::Nil, LuaValue::Nil)),
|
||||
Some((index, value)) => Ok((
|
||||
LuaValue::Integer(index as i32),
|
||||
LuaValue::String(lua.create_string(value)?),
|
||||
)),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
254
crates/lune-utils/src/process/env.rs
Normal file
254
crates/lune-utils/src/process/env.rs
Normal file
|
@ -0,0 +1,254 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env::vars_os,
|
||||
ffi::{OsStr, OsString},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use os_str_bytes::{OsStrBytes, OsStringBytes};
|
||||
|
||||
// Inner (shared) struct
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProcessEnvInner {
|
||||
values: BTreeMap<OsString, OsString>,
|
||||
}
|
||||
|
||||
impl FromIterator<(OsString, OsString)> for ProcessEnvInner {
|
||||
fn from_iter<T: IntoIterator<Item = (OsString, OsString)>>(iter: T) -> Self {
|
||||
Self {
|
||||
values: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A struct that can be easily shared, stored in Lua app data,
|
||||
and that also guarantees the pairs are valid OS strings
|
||||
that can be used for process environment variables.
|
||||
|
||||
Usable directly from Lua, implementing both `FromLua` and `LuaUserData`.
|
||||
|
||||
Also provides convenience methods for working with the variables
|
||||
as either `OsString` or `Vec<u8>`, where using the latter implicitly
|
||||
converts to an `OsString` and fails if the conversion is not possible.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessEnv {
|
||||
inner: Arc<Mutex<ProcessEnvInner>>,
|
||||
}
|
||||
|
||||
impl ProcessEnv {
|
||||
#[must_use]
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(ProcessEnvInner::default())),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(vars_os().collect())),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.is_empty()
|
||||
}
|
||||
|
||||
// OS strings
|
||||
|
||||
#[must_use]
|
||||
pub fn get_all(&self) -> Vec<(OsString, OsString)> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.clone().into_iter().collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_value(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
|
||||
let key = key.as_ref();
|
||||
|
||||
super::validate_os_key(key).ok()?;
|
||||
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn set_value(&self, key: impl Into<OsString>, val: impl Into<OsString>) {
|
||||
let key = key.into();
|
||||
let val = val.into();
|
||||
|
||||
if super::validate_os_pair((&key, &val)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.insert(key, val);
|
||||
}
|
||||
|
||||
pub fn remove_value(&self, key: impl AsRef<OsStr>) {
|
||||
let key = key.as_ref();
|
||||
|
||||
if super::validate_os_key(key).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.remove(key);
|
||||
}
|
||||
|
||||
// Bytes wrappers
|
||||
|
||||
#[must_use]
|
||||
pub fn get_all_bytes(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
self.get_all()
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| Some((k.into_io_vec()?, v.into_io_vec()?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_value_bytes(&self, key: impl AsRef<[u8]>) -> Option<Vec<u8>> {
|
||||
let key = OsStr::from_io_bytes(key.as_ref())?;
|
||||
let val = self.get_value(key)?;
|
||||
val.into_io_vec()
|
||||
}
|
||||
|
||||
pub fn set_value_bytes(&self, key: impl AsRef<[u8]>, val: impl Into<Vec<u8>>) {
|
||||
let key = OsStr::from_io_bytes(key.as_ref());
|
||||
let val = OsString::from_io_vec(val.into());
|
||||
if let (Some(key), Some(val)) = (key, val) {
|
||||
self.set_value(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_value_bytes(&self, key: impl AsRef<[u8]>) {
|
||||
let key = OsStr::from_io_bytes(key.as_ref());
|
||||
if let Some(key) = key {
|
||||
self.remove_value(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterator implementations
|
||||
|
||||
impl IntoIterator for ProcessEnv {
|
||||
type Item = (OsString, OsString);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<OsString, OsString>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.values.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<OsString>, V: Into<OsString>> FromIterator<(K, V)> for ProcessEnv {
|
||||
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(
|
||||
iter.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.filter(|(k, v)| super::validate_os_pair((k, v)).is_ok())
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<OsString>, V: Into<OsString>> Extend<(K, V)> for ProcessEnv {
|
||||
fn extend<T: IntoIterator<Item = (K, V)>>(&mut self, iter: T) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.values.extend(
|
||||
iter.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.filter(|(k, v)| super::validate_os_pair((k, v)).is_ok()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Lua implementations
|
||||
|
||||
impl FromLua for ProcessEnv {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::Nil = value {
|
||||
Ok(Self::from_iter([] as [(OsString, OsString); 0]))
|
||||
} else if let LuaValue::Boolean(true) = value {
|
||||
Ok(Self::current())
|
||||
} else if let Some(u) = value.as_userdata().and_then(|u| u.borrow::<Self>().ok()) {
|
||||
Ok(u.clone())
|
||||
} else if let LuaValue::Table(arr) = value {
|
||||
let mut args = Vec::new();
|
||||
for pair in arr.pairs::<LuaValue, LuaValue>() {
|
||||
let (key_res, val_res) = match pair {
|
||||
Ok((key, val)) => (Ok(key), Ok(val)),
|
||||
Err(err) => (Err(err.clone()), Err(err)),
|
||||
};
|
||||
|
||||
let key = super::lua_value_to_os_string(key_res, "ProcessEnv")?;
|
||||
let val = super::lua_value_to_os_string(val_res, "ProcessEnv")?;
|
||||
|
||||
super::validate_os_pair((&key, &val))?;
|
||||
|
||||
args.push((key, val));
|
||||
}
|
||||
Ok(Self::from_iter(args))
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: String::from("ProcessEnv"),
|
||||
message: Some(format!(
|
||||
"Invalid type for process env - expected table or nil, got '{}'",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ProcessEnv {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, (): ()| Ok(this.len()));
|
||||
methods.add_meta_method(LuaMetaMethod::Index, |_, this, key: LuaValue| {
|
||||
let key = super::lua_value_to_os_string(Ok(key), "OsString")?;
|
||||
Ok(this.get_value(key))
|
||||
});
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::NewIndex,
|
||||
|_, this, (key, val): (LuaValue, Option<LuaValue>)| {
|
||||
let key = super::lua_value_to_os_string(Ok(key), "OsString")?;
|
||||
if let Some(val) = val {
|
||||
let val = super::lua_value_to_os_string(Ok(val), "OsString")?;
|
||||
this.set_value(key, val);
|
||||
} else {
|
||||
this.remove_value(key);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
methods.add_meta_method(LuaMetaMethod::Iter, |lua, this, (): ()| {
|
||||
let mut vars = this
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|(key, val)| Some((key.into_io_vec()?, val.into_io_vec()?)));
|
||||
lua.create_function_mut(move |lua, (): ()| match vars.next() {
|
||||
None => Ok((LuaValue::Nil, LuaValue::Nil)),
|
||||
Some((key, val)) => Ok((
|
||||
LuaValue::String(lua.create_string(key)?),
|
||||
LuaValue::String(lua.create_string(val)?),
|
||||
)),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,29 +1,31 @@
|
|||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct JitEnablement(bool);
|
||||
pub struct ProcessJitEnablement {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl JitEnablement {
|
||||
impl ProcessJitEnablement {
|
||||
#[must_use]
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self(enabled)
|
||||
Self { enabled }
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, enabled: bool) {
|
||||
self.0 = enabled;
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn enabled(self) -> bool {
|
||||
self.0
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JitEnablement> for bool {
|
||||
fn from(val: JitEnablement) -> Self {
|
||||
impl From<ProcessJitEnablement> for bool {
|
||||
fn from(val: ProcessJitEnablement) -> Self {
|
||||
val.enabled()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for JitEnablement {
|
||||
impl From<bool> for ProcessJitEnablement {
|
||||
fn from(val: bool) -> Self {
|
||||
Self::new(val)
|
||||
}
|
78
crates/lune-utils/src/process/mod.rs
Normal file
78
crates/lune-utils/src/process/mod.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use os_str_bytes::{OsStrBytes, OsStringBytes};
|
||||
|
||||
mod args;
|
||||
mod env;
|
||||
mod jit;
|
||||
|
||||
pub use self::args::ProcessArgs;
|
||||
pub use self::env::ProcessEnv;
|
||||
pub use self::jit::ProcessJitEnablement;
|
||||
|
||||
fn lua_value_to_os_string(res: LuaResult<LuaValue>, to: &'static str) -> LuaResult<OsString> {
|
||||
let (btype, bs) = match res {
|
||||
Ok(LuaValue::String(s)) => ("string", s.as_bytes().to_vec()),
|
||||
Ok(LuaValue::Buffer(b)) => ("buffer", b.to_vec()),
|
||||
res => {
|
||||
let vtype = match res {
|
||||
Ok(v) => v.type_name(),
|
||||
Err(_) => "unknown",
|
||||
};
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: vtype,
|
||||
to: String::from(to),
|
||||
message: Some(format!(
|
||||
"Expected value to be a string or buffer, got '{vtype}'",
|
||||
)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let Some(s) = OsString::from_io_vec(bs) else {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: btype,
|
||||
to: String::from(to),
|
||||
message: Some(String::from("Expected {btype} to contain valid OS bytes")),
|
||||
});
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn validate_os_key(key: &OsStr) -> LuaResult<()> {
|
||||
let Some(key) = key.to_io_bytes() else {
|
||||
return Err(LuaError::runtime("Key must be IO-safe"));
|
||||
};
|
||||
if key.is_empty() {
|
||||
Err(LuaError::runtime("Key must not be empty"))
|
||||
} else if key.contains(&b'=') {
|
||||
Err(LuaError::runtime(
|
||||
"Key must not contain the equals character '='",
|
||||
))
|
||||
} else if key.contains(&b'\0') {
|
||||
Err(LuaError::runtime("Key must not contain the NUL character"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_os_value(val: &OsStr) -> LuaResult<()> {
|
||||
let Some(val) = val.to_io_bytes() else {
|
||||
return Err(LuaError::runtime("Value must be IO-safe"));
|
||||
};
|
||||
if val.contains(&b'\0') {
|
||||
Err(LuaError::runtime(
|
||||
"Value must not contain the NUL character",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_os_pair((key, value): (&OsStr, &OsStr)) -> LuaResult<()> {
|
||||
validate_os_key(key)?;
|
||||
validate_os_value(value)?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use lune_utils::jit::JitEnablement;
|
||||
use lune_utils::process::{ProcessArgs, ProcessEnv, ProcessJitEnablement};
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::{Functions, Scheduler};
|
||||
|
||||
|
@ -58,7 +61,9 @@ impl RuntimeReturnValues {
|
|||
pub struct Runtime {
|
||||
lua: Lua,
|
||||
sched: Scheduler,
|
||||
jit: JitEnablement,
|
||||
args: ProcessArgs,
|
||||
env: ProcessEnv,
|
||||
jit: ProcessJitEnablement,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
|
@ -75,8 +80,6 @@ impl Runtime {
|
|||
pub fn new() -> LuaResult<Self> {
|
||||
let lua = Lua::new();
|
||||
|
||||
lua.set_app_data(Vec::<String>::new());
|
||||
|
||||
let sched = Scheduler::new(lua.clone());
|
||||
let fns = Functions::new(lua.clone()).expect("has scheduler");
|
||||
|
||||
|
@ -126,21 +129,47 @@ impl Runtime {
|
|||
.set(g_table.name(), g_table.create(lua.clone())?)?;
|
||||
}
|
||||
|
||||
let jit = JitEnablement::default();
|
||||
Ok(Self { lua, sched, jit })
|
||||
let args = ProcessArgs::current();
|
||||
let env = ProcessEnv::current();
|
||||
let jit = ProcessJitEnablement::default();
|
||||
|
||||
Ok(Self {
|
||||
lua,
|
||||
sched,
|
||||
args,
|
||||
env,
|
||||
jit,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Sets arguments to give in `process.args` for Lune scripts.
|
||||
|
||||
By default, `std::env::args_os()` is used.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn with_args<A, S>(self, args: A) -> Self
|
||||
pub fn with_args<A, S>(mut self, args: A) -> Self
|
||||
where
|
||||
A: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
self.lua.set_app_data(args);
|
||||
self.args = args.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/**
|
||||
Sets environment values to give in `process.env` for Lune scripts.
|
||||
|
||||
By default, `std::env::vars_os()` is used.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn with_env<E, K, V>(mut self, env: E) -> Self
|
||||
where
|
||||
E: IntoIterator<Item = (K, V)>,
|
||||
K: Into<OsString>,
|
||||
V: Into<OsString>,
|
||||
{
|
||||
self.env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -148,7 +177,10 @@ impl Runtime {
|
|||
Enables or disables JIT compilation.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn with_jit(mut self, jit_status: impl Into<JitEnablement>) -> Self {
|
||||
pub fn with_jit<J>(mut self, jit_status: J) -> Self
|
||||
where
|
||||
J: Into<ProcessJitEnablement>,
|
||||
{
|
||||
self.jit = jit_status.into();
|
||||
self
|
||||
}
|
||||
|
@ -175,8 +207,12 @@ impl Runtime {
|
|||
eprintln!("{}", RuntimeError::from(e));
|
||||
});
|
||||
|
||||
// Enable / disable the JIT as requested and store the current status as AppData
|
||||
// Store the provided args, environment variables, and jit enablement as AppData
|
||||
self.lua.set_app_data(self.args.clone());
|
||||
self.lua.set_app_data(self.env.clone());
|
||||
self.lua.set_app_data(self.jit);
|
||||
|
||||
// Enable / disable the JIT as requested, before loading anything
|
||||
self.lua.enable_jit(self.jit.enabled());
|
||||
|
||||
// Load our "main" thread
|
||||
|
|
|
@ -33,14 +33,8 @@ macro_rules! create_tests {
|
|||
let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
|
||||
let script = read_to_string(&full_name).await?;
|
||||
let mut lune = Runtime::new()?
|
||||
.with_jit(true)
|
||||
.with_args(
|
||||
ARGS
|
||||
.clone()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
.with_args(ARGS.iter().cloned())
|
||||
.with_jit(true);
|
||||
let script_name = full_name
|
||||
.trim_end_matches(".luau")
|
||||
.trim_end_matches(".lua")
|
||||
|
|
|
@ -12,7 +12,6 @@ local servers = {
|
|||
"https://azure.microsoft.com/en-us/updates/feed/",
|
||||
"https://acme-v02.api.letsencrypt.org/directory",
|
||||
"https://ip-ranges.amazonaws.com/ip-ranges.json",
|
||||
"https://api.github.com/zen",
|
||||
"https://en.wikipedia.org/w/api.php",
|
||||
"https://status.godaddy.com/api/v2/summary.json",
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@ assert(#process.args > 0, "No process arguments found")
|
|||
assert(process.args[1] == "Foo", "Invalid first argument to process")
|
||||
assert(process.args[2] == "Bar", "Invalid second argument to process")
|
||||
|
||||
local success, message = pcall(function()
|
||||
local success, err = pcall(function()
|
||||
process.args[1] = "abc"
|
||||
end)
|
||||
local message = if err == nil then nil else tostring(err)
|
||||
assert(
|
||||
success == false and type(message) == "string" and #message > 0,
|
||||
"Trying to set process arguments should throw an error with a message"
|
||||
|
|
Loading…
Add table
Reference in a new issue