Compare commits

...

8 commits
v0.9.2 ... main

Author SHA1 Message Date
Filip Tibell
df56cd58e7
Github does not like requests from github to github, remove it from https test 2025-05-02 22:33:28 +02:00
Filip Tibell
66e3b58cd7
Url and uri are not the same 2025-05-02 21:57:13 +02:00
Filip Tibell
fb33d1812d
Remove old unused app data 2025-05-02 12:31:58 +02:00
Filip Tibell
0ddaaaefb5
Update changelog 2025-05-02 12:29:46 +02:00
Filip Tibell
2e5b3bb5eb
Fix panicking during require because of long lived require context borrow 2025-05-02 12:27:20 +02:00
Filip Tibell
6645631c46
Properly store process args and env as part of runtime initialization instead of in std-process 2025-05-02 12:18:53 +02:00
Filip Tibell
120048ae95
Update changelog 2025-05-01 21:12:54 +02:00
Filip Tibell
2d8e58b028
Revamp handling of process args and env with fully featured newtypes 2025-05-01 21:08:58 +02:00
19 changed files with 734 additions and 154 deletions

View file

@ -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
View file

@ -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",

View file

@ -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"

View file

@ -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...

View file

@ -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
}

View file

@ -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"

View file

@ -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)

View file

@ -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 {

View file

@ -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/") {

View file

@ -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"

View file

@ -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;
}

View 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)?),
)),
})
});
}
}

View 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)?),
)),
})
});
}
}

View file

@ -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)
}

View 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(())
}

View file

@ -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

View file

@ -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")

View file

@ -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",
}

View file

@ -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"