From 2d8e58b0282082a1a756150ddc59068241927763 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 1 May 2025 21:08:58 +0200 Subject: [PATCH] Revamp handling of process args and env with fully featured newtypes --- Cargo.lock | 2 +- crates/lune-std-process/Cargo.toml | 1 - crates/lune-std-process/src/lib.rs | 98 ++------ crates/lune-std-process/src/options/mod.rs | 35 ++- crates/lune-utils/Cargo.toml | 1 + crates/lune-utils/src/lib.rs | 7 +- crates/lune-utils/src/process/args.rs | 252 ++++++++++++++++++++ crates/lune-utils/src/process/env.rs | 254 +++++++++++++++++++++ crates/lune-utils/src/{ => process}/jit.rs | 18 +- crates/lune-utils/src/process/mod.rs | 78 +++++++ tests/process/args.luau | 3 +- 11 files changed, 637 insertions(+), 112 deletions(-) create mode 100644 crates/lune-utils/src/process/args.rs create mode 100644 crates/lune-utils/src/process/env.rs rename crates/lune-utils/src/{ => process}/jit.rs (51%) create mode 100644 crates/lune-utils/src/process/mod.rs diff --git a/Cargo.lock b/Cargo.lock index feb8396..68b85b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1798,7 +1798,6 @@ dependencies = [ "lune-utils", "mlua", "mlua-luau-scheduler", - "os_str_bytes", "pin-project", ] @@ -1880,6 +1879,7 @@ dependencies = [ "console", "dunce", "mlua", + "os_str_bytes", "parking_lot", "path-clean", "pathdiff", diff --git a/crates/lune-std-process/Cargo.toml b/crates/lune-std-process/Cargo.toml index e1b6bb1..99772d6 100644 --- a/crates/lune-std-process/Cargo.toml +++ b/crates/lune-std-process/Cargo.toml @@ -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" diff --git a/crates/lune-std-process/src/lib.rs b/crates/lune-std-process/src/lib.rs index d1564e2..7c31558 100644 --- a/crates/lune-std-process/src/lib.rs +++ b/crates/lune-std-process/src/lib.rs @@ -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,19 @@ pub fn module(lua: Lua) -> LuaResult { "little" })?; - // Create readonly args array + // Find the readonly args array let args_vec = lua .app_data_ref::>() .ok_or_else(|| LuaError::runtime("Missing args vec 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 userdatas for args + env + // TODO: Move this up into the runtime creation instead, + // and set it as app data there to later fetch here + let process_args = ProcessArgs::from_iter(args_vec); + let process_env = ProcessEnv::current(); + lua.set_app_data(process_args.clone()); + lua.set_app_data(process_env.clone()); // Create our process exit function, the scheduler crate provides this let fns = Functions::new(lua.clone())?; @@ -87,73 +80,18 @@ pub fn module(lua: Lua) -> LuaResult { .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 { - 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)) -> 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 { - let mut vars = env::vars_os().collect::>().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>, ProcessSpawnOptions), + (program, args, mut options): (String, ProcessArgs, ProcessSpawnOptions), ) -> LuaResult { let stdin = options.stdio.stdin.take(); let stdout = options.stdio.stdout; @@ -171,7 +109,7 @@ async fn process_exec( fn process_create( lua: &Lua, - (program, args, options): (String, Option>, ProcessSpawnOptions), + (program, args, options): (String, ProcessArgs, ProcessSpawnOptions), ) -> LuaResult { let child = options .into_command(program, args) diff --git a/crates/lune-std-process/src/options/mod.rs b/crates/lune-std-process/src/options/mod.rs index f19cf26..1dab470 100644 --- a/crates/lune-std-process/src/options/mod.rs +++ b/crates/lune-std-process/src/options/mod.rs @@ -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, args: Option>) -> Command { - let mut program = program.into(); + pub fn into_command(self, program: impl Into, args: ProcessArgs) -> Command { + let mut program: OsString = program.into(); + let mut args = args.into_iter().collect::>(); // 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 { diff --git a/crates/lune-utils/Cargo.toml b/crates/lune-utils/Cargo.toml index 8e0344e..e901a88 100644 --- a/crates/lune-utils/Cargo.toml +++ b/crates/lune-utils/Cargo.toml @@ -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" diff --git a/crates/lune-utils/src/lib.rs b/crates/lune-utils/src/lib.rs index 828426f..fb420ed 100644 --- a/crates/lune-utils/src/lib.rs +++ b/crates/lune-utils/src/lib.rs @@ -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; +} diff --git a/crates/lune-utils/src/process/args.rs b/crates/lune-utils/src/process/args.rs new file mode 100644 index 0000000..4152d2c --- /dev/null +++ b/crates/lune-utils/src/process/args.rs @@ -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, +} + +impl FromIterator for ProcessArgsInner { + fn from_iter>(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`, 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>, +} + +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 { + let inner = self.inner.lock().unwrap(); + inner.values.clone() + } + + #[must_use] + pub fn get(&self, index: usize) -> Option { + let inner = self.inner.lock().unwrap(); + inner.values.get(index).cloned() + } + + pub fn set(&self, index: usize, val: impl Into) { + 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) { + let mut inner = self.inner.lock().unwrap(); + inner.values.push(val.into()); + } + + #[must_use] + pub fn pop(&self) -> Option { + let mut inner = self.inner.lock().unwrap(); + inner.values.pop() + } + + pub fn insert(&self, index: usize, val: impl Into) { + 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 { + 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> { + self.all() + .into_iter() + .filter_map(OsString::into_io_vec) + .collect() + } + + #[must_use] + pub fn get_bytes(&self, index: usize) -> Option> { + let val = self.get(index)?; + val.into_io_vec() + } + + pub fn set_bytes(&self, index: usize, val: impl Into>) { + if let Some(val_os) = OsString::from_io_vec(val.into()) { + self.set(index, val_os); + } + } + + pub fn push_bytes(&self, val: impl Into>) { + if let Some(val_os) = OsString::from_io_vec(val.into()) { + self.push(val_os); + } + } + + #[must_use] + pub fn pop_bytes(&self) -> Option> { + self.pop().and_then(OsString::into_io_vec) + } + + pub fn insert_bytes(&self, index: usize, val: impl Into>) { + if let Some(val_os) = OsString::from_io_vec(val.into()) { + self.insert(index, val_os); + } + } + + pub fn remove_bytes(&self, index: usize) -> Option> { + self.remove(index).and_then(OsString::into_io_vec) + } +} + +// Iterator implementations + +impl IntoIterator for ProcessArgs { + type Item = OsString; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let inner = self.inner.lock().unwrap(); + inner.values.clone().into_iter() + } +} + +impl> FromIterator for ProcessArgs { + fn from_iter>(iter: T) -> Self { + Self { + inner: Arc::new(Mutex::new(iter.into_iter().map(Into::into).collect())), + } + } +} + +impl> Extend for ProcessArgs { + fn extend>(&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 { + 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::().ok()) { + Ok(u.clone()) + } else if let LuaValue::Table(arr) = value { + let mut args = Vec::new(); + for pair in arr.pairs::() { + 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>(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)?), + )), + }) + }); + } +} diff --git a/crates/lune-utils/src/process/env.rs b/crates/lune-utils/src/process/env.rs new file mode 100644 index 0000000..8258b49 --- /dev/null +++ b/crates/lune-utils/src/process/env.rs @@ -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, +} + +impl FromIterator<(OsString, OsString)> for ProcessEnvInner { + fn from_iter>(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`, 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>, +} + +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) -> Option { + 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, val: impl Into) { + 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) { + 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, Vec)> { + 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> { + 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>) { + 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; + + fn into_iter(self) -> Self::IntoIter { + let inner = self.inner.lock().unwrap(); + inner.values.clone().into_iter() + } +} + +impl, V: Into> FromIterator<(K, V)> for ProcessEnv { + fn from_iter>(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, V: Into> Extend<(K, V)> for ProcessEnv { + fn extend>(&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 { + 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::().ok()) { + Ok(u.clone()) + } else if let LuaValue::Table(arr) = value { + let mut args = Vec::new(); + for pair in arr.pairs::() { + 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>(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)| { + 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)?), + )), + }) + }); + } +} diff --git a/crates/lune-utils/src/jit.rs b/crates/lune-utils/src/process/jit.rs similarity index 51% rename from crates/lune-utils/src/jit.rs rename to crates/lune-utils/src/process/jit.rs index e5bb80d..27cb1f3 100644 --- a/crates/lune-utils/src/jit.rs +++ b/crates/lune-utils/src/process/jit.rs @@ -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 for bool { - fn from(val: JitEnablement) -> Self { +impl From for bool { + fn from(val: ProcessJitEnablement) -> Self { val.enabled() } } -impl From for JitEnablement { +impl From for ProcessJitEnablement { fn from(val: bool) -> Self { Self::new(val) } diff --git a/crates/lune-utils/src/process/mod.rs b/crates/lune-utils/src/process/mod.rs new file mode 100644 index 0000000..a2ca849 --- /dev/null +++ b/crates/lune-utils/src/process/mod.rs @@ -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, to: &'static str) -> LuaResult { + 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(()) +} diff --git a/tests/process/args.luau b/tests/process/args.luau index 6a8bb45..1084989 100644 --- a/tests/process/args.luau +++ b/tests/process/args.luau @@ -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"