diff --git a/Cargo.lock b/Cargo.lock index eca852d..0fd5b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,17 +205,6 @@ version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -759,12 +748,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" -[[package]] -name = "either" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1367,15 +1350,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -1484,56 +1458,27 @@ name = "lune" version = "0.8.3" dependencies = [ "anyhow", - "async-compression", - "async-trait", - "blocking", - "bstr", - "chrono", - "chrono_lc", "clap", "console", "dialoguer", "directories", - "dunce", "env_logger", "futures-util", - "glam", - "http 1.1.0", - "http-body-util", - "hyper 1.3.1", - "hyper-tungstenite", - "hyper-util", "include_dir", - "itertools", - "lz4_flex", + "lune-roblox", + "lune-std", + "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.2", + "mlua-luau-scheduler", "once_cell", - "os_str_bytes", - "path-clean", - "pathdiff", - "pin-project", - "rand", - "rbx_binary", - "rbx_cookie", - "rbx_dom_weak", - "rbx_reflection", - "rbx_reflection_database", - "rbx_xml", - "regex", "reqwest", "rustyline", - "self_cell", "serde", "serde_json", - "serde_yaml", "thiserror", "tokio", - "tokio-tungstenite", - "toml", "tracing", "tracing-subscriber", - "urlencoding", "zip_next", ] @@ -1570,7 +1515,7 @@ dependencies = [ "lune-std-task", "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "serde", "serde_json", "tokio", @@ -1620,7 +1565,7 @@ dependencies = [ "lune-std-serde", "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "reqwest", "tokio", "tokio-tungstenite", @@ -1634,7 +1579,7 @@ dependencies = [ "directories", "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "os_str_bytes", "pin-project", "tokio", @@ -1657,7 +1602,7 @@ dependencies = [ "lune-roblox", "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "once_cell", "rbx_cookie", ] @@ -1685,7 +1630,7 @@ dependencies = [ "dialoguer", "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "tokio", ] @@ -1695,7 +1640,7 @@ version = "0.1.0" dependencies = [ "lune-utils", "mlua", - "mlua-luau-scheduler 0.0.1", + "mlua-luau-scheduler", "tokio", ] @@ -1827,23 +1772,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "mlua-luau-scheduler" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13eabdbc57fa38cf0b604d98ce3431573c79a964aac56e09c16c240d36cb1bf" -dependencies = [ - "async-executor", - "blocking", - "concurrent-queue", - "derive_more", - "event-listener 4.0.3", - "futures-lite", - "mlua", - "rustc-hash", - "tracing", -] - [[package]] name = "mlua-sys" version = "0.5.2" @@ -3074,7 +3002,6 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "tracing", "windows-sys 0.48.0", ] diff --git a/crates/lune-std-net/Cargo.toml b/crates/lune-std-net/Cargo.toml index ded2a71..24616ba 100644 --- a/crates/lune-std-net/Cargo.toml +++ b/crates/lune-std-net/Cargo.toml @@ -27,7 +27,11 @@ reqwest = { version = "0.11", default-features = false, features = [ tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } urlencoding = "2.1" -tokio = { version = "1", default-features = false, features = ["sync", "net"] } +tokio = { version = "1", default-features = false, features = [ + "sync", + "net", + "macros", +] } lune-utils = { version = "0.1.0", path = "../lune-utils" } lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" } diff --git a/crates/lune/Cargo.toml b/crates/lune/Cargo.toml index d8bf591..0c4b3b2 100644 --- a/crates/lune/Cargo.toml +++ b/crates/lune/Cargo.toml @@ -18,7 +18,31 @@ name = "lune" path = "src/lib.rs" [features] -default = ["cli", "roblox"] +default = [ + "datetime", + "fs", + "luau", + "net", + "process", + "regex", + "roblox", + "serde", + "stdio", + "task", + "cli", +] + +datetime = ["lune-std/datetime"] +fs = ["lune-std/fs"] +luau = ["lune-std/luau"] +net = ["lune-std/net"] +process = ["lune-std/process"] +regex = ["lune-std/regex"] +roblox = ["lune-std/roblox", "dep:lune-roblox"] +serde = ["lune-std/serde"] +stdio = ["lune-std/stdio"] +task = ["lune-std/task"] + cli = [ "dep:anyhow", "dep:env_logger", @@ -27,113 +51,39 @@ cli = [ "dep:rustyline", "dep:zip_next", ] -roblox = [ - "dep:glam", - "dep:rand", - "dep:rbx_cookie", - "dep:rbx_binary", - "dep:rbx_dom_weak", - "dep:rbx_reflection", - "dep:rbx_reflection_database", - "dep:rbx_xml", -] [lints] workspace = true -# All of the dependencies for Lune. -# -# Dependencies are categorized as following: -# -# 1. General dependencies with no specific features set -# 2. Large / core dependencies that have many different crates and / or features set -# 3. Dependencies for specific features of Lune, eg. the CLI or massive Roblox builtin library -# [dependencies] +mlua = { version = "0.9.7", features = ["luau"] } +mlua-luau-scheduler = "0.0.1" + console = "0.15" +dialoguer = "0.11" directories = "5.0" futures-util = "0.3" once_cell = "1.17" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" thiserror = "1.0" -async-trait = "0.1" -dialoguer = "0.11" -dunce = "1.0" -lz4_flex = "0.11" -path-clean = "1.0" -pathdiff = "0.2" -pin-project = "1.0" -urlencoding = "2.1" -bstr = "1.9" -regex = "1.10" -self_cell = "1.0" -### RUNTIME - -blocking = "1.5" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tokio = { version = "1.24", features = ["full", "tracing"] } -os_str_bytes = { version = "7.0", features = ["conversions"] } - -mlua-luau-scheduler = { version = "0.0.2" } -mlua = { version = "0.9.7", features = [ - "luau", - "luau-jit", - "async", - "serialize", -] } - -### SERDE - -async-compression = { version = "0.4", features = [ - "tokio", - "brotli", - "deflate", - "gzip", - "zlib", -] } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } -serde_yaml = "0.9" -toml = { version = "0.8", features = ["preserve_order"] } - -### NET - -hyper = { version = "1.1", features = ["full"] } -hyper-util = { version = "0.1", features = ["full"] } -http = "1.0" -http-body-util = { version = "0.1" } -hyper-tungstenite = { version = "0.13" } - +tokio = { version = "1", features = ["full"] } reqwest = { version = "0.11", default-features = false, features = [ "rustls-tls", ] } -tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } - -### DATETIME -chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc -chrono_lc = "0.1" +lune-std = { version = "0.1.0", path = "../lune-std" } +lune-utils = { version = "0.1.0", path = "../lune-utils" } +lune-roblox = { optional = true, version = "0.1.0", path = "../lune-roblox" } ### CLI anyhow = { optional = true, version = "1.0" } env_logger = { optional = true, version = "0.11" } -itertools = "0.12" clap = { optional = true, version = "4.1", features = ["derive"] } include_dir = { optional = true, version = "0.7", features = ["glob"] } rustyline = { optional = true, version = "14.0" } zip_next = { optional = true, version = "1.1" } - -### ROBLOX - -glam = { optional = true, version = "0.27" } -rand = { optional = true, version = "0.8" } - -rbx_cookie = { optional = true, version = "0.1.4", default-features = false } - -rbx_binary = { optional = true, version = "0.7.3" } -rbx_dom_weak = { optional = true, version = "2.6.0" } -rbx_reflection = { optional = true, version = "4.4.0" } -rbx_reflection_database = { optional = true, version = "0.2.9" } -rbx_xml = { optional = true, version = "0.13.2" } diff --git a/crates/lune/src/cli/utils/files.rs b/crates/lune/src/cli/utils/files.rs index 9b9b1ca..2e02bb8 100644 --- a/crates/lune/src/cli/utils/files.rs +++ b/crates/lune/src/cli/utils/files.rs @@ -6,7 +6,6 @@ use std::{ use anyhow::{anyhow, bail, Result}; use console::style; use directories::UserDirs; -use itertools::Itertools; use once_cell::sync::Lazy; const LUNE_COMMENT_PREFIX: &str = "-->"; @@ -180,10 +179,9 @@ pub fn parse_lune_description_from_file(contents: &str) -> Option { }); let unindented_lines = comment_lines .iter() - .map(|line| &line[shortest_indent..]) - // Replace newlines with a single space inbetween instead - .interleave(std::iter::repeat(" ").take(comment_lines.len() - 1)) - .collect(); + .map(|line| line[shortest_indent..].to_string()) + .collect::>() + .join(" "); Some(unindented_lines) } } diff --git a/crates/lune/src/lib.rs b/crates/lune/src/lib.rs index eaaa157..4369179 100644 --- a/crates/lune/src/lib.rs +++ b/crates/lune/src/lib.rs @@ -1,9 +1,11 @@ -mod lune; +#![allow(clippy::cargo_common_metadata)] + +mod rt; #[cfg(feature = "roblox")] -pub mod roblox; +pub use lune_roblox as roblox; #[cfg(test)] mod tests; -pub use crate::lune::{Runtime, RuntimeError}; +pub use crate::rt::{Runtime, RuntimeError}; diff --git a/crates/lune/src/lune/builtins/datetime/error.rs b/crates/lune/src/lune/builtins/datetime/error.rs deleted file mode 100644 index b79fb40..0000000 --- a/crates/lune/src/lune/builtins/datetime/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use mlua::prelude::*; - -use thiserror::Error; - -pub type DateTimeResult = Result; - -#[derive(Debug, Clone, Error)] -pub enum DateTimeError { - #[error("invalid date")] - InvalidDate, - #[error("invalid time")] - InvalidTime, - #[error("ambiguous date or time")] - Ambiguous, - #[error("date or time is outside allowed range")] - OutOfRangeUnspecified, - #[error("{name} must be within range {min} -> {max}, got {value}")] - OutOfRange { - name: &'static str, - value: String, - min: String, - max: String, - }, - #[error(transparent)] - ParseError(#[from] chrono::ParseError), -} - -impl From for LuaError { - fn from(value: DateTimeError) -> Self { - LuaError::runtime(value.to_string()) - } -} diff --git a/crates/lune/src/lune/builtins/datetime/mod.rs b/crates/lune/src/lune/builtins/datetime/mod.rs deleted file mode 100644 index 87a07db..0000000 --- a/crates/lune/src/lune/builtins/datetime/mod.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::cmp::Ordering; - -use mlua::prelude::*; - -use chrono::prelude::*; -use chrono::DateTime as ChronoDateTime; -use chrono_lc::LocaleDate; - -use crate::lune::util::TableBuilder; - -mod error; -mod values; - -use error::*; -use values::*; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_function("fromIsoDate", |_, iso_date: String| { - Ok(DateTime::from_iso_date(iso_date)?) - })? - .with_function("fromLocalTime", |_, values| { - Ok(DateTime::from_local_time(&values)?) - })? - .with_function("fromUniversalTime", |_, values| { - Ok(DateTime::from_universal_time(&values)?) - })? - .with_function("fromUnixTimestamp", |_, timestamp| { - Ok(DateTime::from_unix_timestamp_float(timestamp)?) - })? - .with_function("now", |_, ()| Ok(DateTime::now()))? - .build_readonly() -} - -const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; -const DEFAULT_LOCALE: &str = "en"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct DateTime { - // NOTE: We store this as the UTC time zone since it is the most commonly - // used and getting the generics right for TimeZone is somewhat tricky, - // but none of the method implementations below should rely on this tz - inner: ChronoDateTime, -} - -impl DateTime { - /** - Creates a new `DateTime` struct representing the current moment in time. - - See [`chrono::DateTime::now`] for additional details. - */ - pub fn now() -> Self { - Self { inner: Utc::now() } - } - - /** - Creates a new `DateTime` struct from the given `unix_timestamp`, - which is a float of seconds passed since the UNIX epoch. - - This is somewhat unconventional, but fits our Luau interface and dynamic types quite well. - To use this method the same way you would use a more traditional `from_unix_timestamp` - that takes a `u64` of seconds or similar type, casting the value is sufficient: - - ```rust ignore - DateTime::from_unix_timestamp_float(123456789u64 as f64) - ``` - - See [`chrono::DateTime::from_timestamp`] for additional details. - */ - pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult { - let whole = unix_timestamp.trunc() as i64; - let fract = unix_timestamp.fract(); - let nanos = (fract * 1_000_000_000f64) - .round() - .clamp(u32::MIN as f64, u32::MAX as f64) as u32; - let inner = ChronoDateTime::::from_timestamp(whole, nanos) - .ok_or(DateTimeError::OutOfRangeUnspecified)?; - Ok(Self { inner }) - } - - /** - Transforms individual date & time values into a new - `DateTime` struct, using the universal (UTC) time zone. - - See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`] - for additional details and cases where this constructor may return an error. - */ - pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult { - let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day) - .ok_or(DateTimeError::InvalidDate)?; - - let time = NaiveTime::from_hms_milli_opt( - values.hour, - values.minute, - values.second, - values.millisecond, - ) - .ok_or(DateTimeError::InvalidTime)?; - - let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time)); - - Ok(Self { inner }) - } - - /** - Transforms individual date & time values into a new - `DateTime` struct, using the current local time zone. - - See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`] - for additional details and cases where this constructor may return an error. - */ - pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult { - let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day) - .ok_or(DateTimeError::InvalidDate)?; - - let time = NaiveTime::from_hms_milli_opt( - values.hour, - values.minute, - values.second, - values.millisecond, - ) - .ok_or(DateTimeError::InvalidTime)?; - - let inner = Local - .from_local_datetime(&NaiveDateTime::new(date, time)) - .single() - .ok_or(DateTimeError::Ambiguous)? - .with_timezone(&Utc); - - Ok(Self { inner }) - } - - /** - Formats the `DateTime` using the universal (UTC) time - zone, the given format string, and the given locale. - - `format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively. - - See [`chrono_lc::DateTime::formatl`] for additional details. - */ - pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String { - self.inner - .with_timezone(&Local) - .formatl( - format.unwrap_or(DEFAULT_FORMAT), - locale.unwrap_or(DEFAULT_LOCALE), - ) - .to_string() - } - - /** - Formats the `DateTime` using the universal (UTC) time - zone, the given format string, and the given locale. - - `format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively. - - See [`chrono_lc::DateTime::formatl`] for additional details. - */ - pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String { - self.inner - .with_timezone(&Utc) - .formatl( - format.unwrap_or(DEFAULT_FORMAT), - locale.unwrap_or(DEFAULT_LOCALE), - ) - .to_string() - } - - /** - Parses a time string in the ISO 8601 format, such as - `1996-12-19T16:39:57-08:00`, into a new `DateTime` struct. - - See [`chrono::DateTime::parse_from_rfc3339`] for additional details. - */ - pub fn from_iso_date(iso_date: impl AsRef) -> DateTimeResult { - let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc); - Ok(Self { inner }) - } - - /** - Extracts individual date & time values from this - `DateTime`, using the current local time zone. - */ - pub fn to_local_time(self) -> DateTimeValues { - DateTimeValues::from(self.inner.with_timezone(&Local)) - } - - /** - Extracts individual date & time values from this - `DateTime`, using the universal (UTC) time zone. - */ - pub fn to_universal_time(self) -> DateTimeValues { - DateTimeValues::from(self.inner.with_timezone(&Utc)) - } - - /** - Formats a time string in the ISO 8601 format, such as `1996-12-19T16:39:57-08:00`. - - See [`chrono::DateTime::to_rfc3339`] for additional details. - */ - pub fn to_iso_date(self) -> String { - self.inner.to_rfc3339() - } -} - -impl LuaUserData for DateTime { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp())); - fields.add_field_method_get("unixTimestampMillis", |_, this| { - Ok(this.inner.timestamp_millis()) - }); - } - - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - // Metamethods to compare DateTime as instants in time - methods.add_meta_method( - LuaMetaMethod::Eq, - |_, this: &Self, other: LuaUserDataRef| Ok(this.eq(&other)), - ); - methods.add_meta_method( - LuaMetaMethod::Lt, - |_, this: &Self, other: LuaUserDataRef| { - Ok(matches!(this.cmp(&other), Ordering::Less)) - }, - ); - methods.add_meta_method( - LuaMetaMethod::Le, - |_, this: &Self, other: LuaUserDataRef| { - Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal)) - }, - ); - // Normal methods - methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date())); - methods.add_method( - "formatUniversalTime", - |_, this, (format, locale): (Option, Option)| { - Ok(this.format_string_universal(format.as_deref(), locale.as_deref())) - }, - ); - methods.add_method( - "formatLocalTime", - |_, this, (format, locale): (Option, Option)| { - Ok(this.format_string_local(format.as_deref(), locale.as_deref())) - }, - ); - methods.add_method("toUniversalTime", |_, this: &Self, ()| { - Ok(this.to_universal_time()) - }); - methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time())); - } -} diff --git a/crates/lune/src/lune/builtins/datetime/values.rs b/crates/lune/src/lune/builtins/datetime/values.rs deleted file mode 100644 index 7bca230..0000000 --- a/crates/lune/src/lune/builtins/datetime/values.rs +++ /dev/null @@ -1,170 +0,0 @@ -use mlua::prelude::*; - -use chrono::prelude::*; - -use crate::lune::util::TableBuilder; - -use super::error::{DateTimeError, DateTimeResult}; - -#[derive(Debug, Clone, Copy)] -pub struct DateTimeValues { - pub year: i32, - pub month: u32, - pub day: u32, - pub hour: u32, - pub minute: u32, - pub second: u32, - pub millisecond: u32, -} - -impl DateTimeValues { - /** - Verifies that all of the date & time values are within allowed ranges: - - | Name | Range | - |---------------|----------------| - | `year` | `1400 -> 9999` | - | `month` | `1 -> 12` | - | `day` | `1 -> 31` | - | `hour` | `0 -> 23` | - | `minute` | `0 -> 59` | - | `second` | `0 -> 60` | - | `millisecond` | `0 -> 999` | - */ - pub fn verify(self) -> DateTimeResult { - verify_in_range("year", self.year, 1400, 9999)?; - verify_in_range("month", self.month, 1, 12)?; - verify_in_range("day", self.day, 1, 31)?; - verify_in_range("hour", self.hour, 0, 23)?; - verify_in_range("minute", self.minute, 0, 59)?; - verify_in_range("second", self.second, 0, 60)?; - verify_in_range("millisecond", self.millisecond, 0, 999)?; - Ok(self) - } -} - -fn verify_in_range(name: &'static str, value: T, min: T, max: T) -> DateTimeResult -where - T: PartialOrd + std::fmt::Display, -{ - assert!(max > min); - if value < min || value > max { - Err(DateTimeError::OutOfRange { - name, - min: min.to_string(), - max: max.to_string(), - value: value.to_string(), - }) - } else { - Ok(value) - } -} - -/** - Conversion methods between DateTimeValues and plain lua tables - - Note that the IntoLua implementation here uses a read-only table, - since we generally want to convert into lua when we know we have - a fixed point in time, and we guarantee that it doesn't change -*/ - -impl FromLua<'_> for DateTimeValues { - fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - if !value.is_table() { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "DateTimeValues", - message: Some("value must be a table".to_string()), - }); - }; - - let value = value.as_table().unwrap(); - let values = Self { - year: value.get("year")?, - month: value.get("month")?, - day: value.get("day")?, - hour: value.get("hour")?, - minute: value.get("minute")?, - second: value.get("second")?, - millisecond: value.get("millisecond").unwrap_or(0), - }; - - match values.verify() { - Ok(dt) => Ok(dt), - Err(e) => Err(LuaError::FromLuaConversionError { - from: "table", - to: "DateTimeValues", - message: Some(e.to_string()), - }), - } - } -} - -impl IntoLua<'_> for DateTimeValues { - fn into_lua(self, lua: &Lua) -> LuaResult { - let tab = TableBuilder::new(lua)? - .with_value("year", self.year)? - .with_values(vec![ - ("month", self.month), - ("day", self.day), - ("hour", self.hour), - ("minute", self.minute), - ("second", self.second), - ("millisecond", self.millisecond), - ])? - .build_readonly()?; - Ok(LuaValue::Table(tab)) - } -} - -/** - Conversion methods between chrono's timezone-aware DateTime to - and from our non-timezone-aware DateTimeValues values struct -*/ - -impl From> for DateTimeValues { - fn from(value: DateTime) -> Self { - Self { - year: value.year(), - month: value.month(), - day: value.day(), - hour: value.hour(), - minute: value.minute(), - second: value.second(), - millisecond: value.timestamp_subsec_millis(), - } - } -} - -impl TryFrom for DateTime { - type Error = DateTimeError; - fn try_from(value: DateTimeValues) -> Result { - Utc.with_ymd_and_hms( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - ) - .single() - .ok_or(DateTimeError::Ambiguous) - } -} - -impl TryFrom for DateTime { - type Error = DateTimeError; - fn try_from(value: DateTimeValues) -> Result { - Local - .with_ymd_and_hms( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - ) - .single() - .ok_or(DateTimeError::Ambiguous) - } -} diff --git a/crates/lune/src/lune/builtins/fs/copy.rs b/crates/lune/src/lune/builtins/fs/copy.rs deleted file mode 100644 index bcad01f..0000000 --- a/crates/lune/src/lune/builtins/fs/copy.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::collections::VecDeque; -use std::io::ErrorKind; -use std::path::{Path, PathBuf}; - -use mlua::prelude::*; -use tokio::fs; - -use super::options::FsWriteOptions; - -pub struct CopyContents { - // Vec<(relative depth, path)> - pub dirs: Vec<(usize, PathBuf)>, - pub files: Vec<(usize, PathBuf)>, -} - -async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult { - let mut dirs = Vec::new(); - let mut files = Vec::new(); - - let mut queue = VecDeque::new(); - - let normalized_root = fs::canonicalize(&root).await.map_err(|e| { - LuaError::RuntimeError(format!("Failed to canonicalize root directory path\n{e}")) - })?; - - // Push initial children of the root path into the queue - let mut entries = fs::read_dir(&normalized_root).await?; - while let Some(entry) = entries.next_entry().await? { - queue.push_back((1, entry.path())); - } - - // Go through the current queue, pushing to it - // when we find any new descendant directories - // FUTURE: Try to do async reading here concurrently to speed it up a bit - while let Some((current_depth, current_path)) = queue.pop_front() { - let meta = fs::metadata(¤t_path).await?; - if meta.is_symlink() { - return Err(LuaError::RuntimeError(format!( - "Symlinks are not yet supported, encountered at path '{}'", - current_path.display() - ))); - } else if meta.is_dir() { - // FUTURE: Add an option in FsWriteOptions for max depth and limit it here - let mut entries = fs::read_dir(¤t_path).await?; - while let Some(entry) = entries.next_entry().await? { - queue.push_back((current_depth + 1, entry.path())); - } - dirs.push((current_depth, current_path)); - } else { - files.push((current_depth, current_path)); - } - } - - // Ensure that all directory and file paths are relative to the root path - // SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe - for (_, dir) in dirs.iter_mut() { - *dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf() - } - for (_, file) in files.iter_mut() { - *file = file.strip_prefix(&normalized_root).unwrap().to_path_buf() - } - - // FUTURE: Deduplicate paths such that these directories: - // - foo/ - // - foo/bar/ - // - foo/bar/baz/ - // turn into a single foo/bar/baz/ and let create_dir_all do the heavy lifting - - Ok(CopyContents { dirs, files }) -} - -async fn ensure_no_dir_exists(path: impl AsRef) -> LuaResult<()> { - let path = path.as_ref(); - match fs::metadata(&path).await { - Ok(meta) if meta.is_dir() => Err(LuaError::RuntimeError(format!( - "A directory already exists at the path '{}'", - path.display() - ))), - _ => Ok(()), - } -} - -async fn ensure_no_file_exists(path: impl AsRef) -> LuaResult<()> { - let path = path.as_ref(); - match fs::metadata(&path).await { - Ok(meta) if meta.is_file() => Err(LuaError::RuntimeError(format!( - "A file already exists at the path '{}'", - path.display() - ))), - _ => Ok(()), - } -} - -pub async fn copy( - source: impl AsRef, - target: impl AsRef, - options: FsWriteOptions, -) -> LuaResult<()> { - let source = source.as_ref(); - let target = target.as_ref(); - - // Check if we got a file or directory - we will handle them differently below - let (is_dir, is_file) = match fs::metadata(&source).await { - Ok(meta) => (meta.is_dir(), meta.is_file()), - Err(e) if e.kind() == ErrorKind::NotFound => { - return Err(LuaError::RuntimeError(format!( - "No file or directory exists at the path '{}'", - source.display() - ))) - } - Err(e) => return Err(e.into()), - }; - if !is_file && !is_dir { - return Err(LuaError::RuntimeError(format!( - "The given path '{}' is not a file or a directory", - source.display() - ))); - } - - // Perform copying: - // - // 1. If we are not allowed to overwrite, make sure nothing exists at the target path - // 2. If we are allowed to overwrite, remove any previous entry at the path - // 3. Write all directories first - // 4. Write all files - - if !options.overwrite { - if is_file { - ensure_no_file_exists(target).await?; - } else if is_dir { - ensure_no_dir_exists(target).await?; - } - } - - if is_file { - fs::copy(source, target).await?; - } else if is_dir { - let contents = get_contents_at(source.to_path_buf(), options).await?; - - if options.overwrite { - let (is_dir, is_file) = match fs::metadata(&target).await { - Ok(meta) => (meta.is_dir(), meta.is_file()), - Err(e) if e.kind() == ErrorKind::NotFound => (false, false), - Err(e) => return Err(e.into()), - }; - if is_dir { - fs::remove_dir_all(target).await?; - } else if is_file { - fs::remove_file(target).await?; - } - } - - fs::create_dir_all(target).await?; - - // FUTURE: Write dirs / files concurrently - // to potentially speed these operations up - for (_, dir) in &contents.dirs { - fs::create_dir_all(target.join(dir)).await?; - } - for (_, file) in &contents.files { - fs::copy(source.join(file), target.join(file)).await?; - } - } - - Ok(()) -} diff --git a/crates/lune/src/lune/builtins/fs/metadata.rs b/crates/lune/src/lune/builtins/fs/metadata.rs deleted file mode 100644 index 93bdbe6..0000000 --- a/crates/lune/src/lune/builtins/fs/metadata.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{ - fmt, - fs::{FileType as StdFileType, Metadata as StdMetadata, Permissions as StdPermissions}, - io::Result as IoResult, - str::FromStr, - time::SystemTime, -}; - -use mlua::prelude::*; - -use crate::lune::builtins::datetime::DateTime; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FsMetadataKind { - None, - File, - Dir, - Symlink, -} - -impl fmt::Display for FsMetadataKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::None => "none", - Self::File => "file", - Self::Dir => "dir", - Self::Symlink => "symlink", - } - ) - } -} - -impl FromStr for FsMetadataKind { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.trim().to_ascii_lowercase().as_ref() { - "none" => Ok(Self::None), - "file" => Ok(Self::File), - "dir" => Ok(Self::Dir), - "symlink" => Ok(Self::Symlink), - _ => Err("Invalid metadata kind"), - } - } -} - -impl From for FsMetadataKind { - fn from(value: StdFileType) -> Self { - if value.is_file() { - Self::File - } else if value.is_dir() { - Self::Dir - } else if value.is_symlink() { - Self::Symlink - } else { - panic!("Encountered unknown filesystem filetype") - } - } -} - -impl<'lua> IntoLua<'lua> for FsMetadataKind { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - if self == Self::None { - Ok(LuaValue::Nil) - } else { - self.to_string().into_lua(lua) - } - } -} - -#[derive(Debug, Clone)] -pub struct FsPermissions { - pub(crate) read_only: bool, -} - -impl From for FsPermissions { - fn from(value: StdPermissions) -> Self { - Self { - read_only: value.readonly(), - } - } -} - -impl<'lua> IntoLua<'lua> for FsPermissions { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - let tab = lua.create_table_with_capacity(0, 1)?; - tab.set("readOnly", self.read_only)?; - tab.set_readonly(true); - Ok(LuaValue::Table(tab)) - } -} - -#[derive(Debug, Clone)] -pub struct FsMetadata { - pub(crate) kind: FsMetadataKind, - pub(crate) exists: bool, - pub(crate) created_at: Option, - pub(crate) modified_at: Option, - pub(crate) accessed_at: Option, - pub(crate) permissions: Option, -} - -impl FsMetadata { - pub fn not_found() -> Self { - Self { - kind: FsMetadataKind::None, - exists: false, - created_at: None, - modified_at: None, - accessed_at: None, - permissions: None, - } - } -} - -impl<'lua> IntoLua<'lua> for FsMetadata { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - let tab = lua.create_table_with_capacity(0, 6)?; - tab.set("kind", self.kind)?; - tab.set("exists", self.exists)?; - tab.set("createdAt", self.created_at)?; - tab.set("modifiedAt", self.modified_at)?; - tab.set("accessedAt", self.accessed_at)?; - tab.set("permissions", self.permissions)?; - tab.set_readonly(true); - Ok(LuaValue::Table(tab)) - } -} - -impl From for FsMetadata { - fn from(value: StdMetadata) -> Self { - Self { - kind: value.file_type().into(), - exists: true, - created_at: system_time_to_timestamp(value.created()), - modified_at: system_time_to_timestamp(value.modified()), - accessed_at: system_time_to_timestamp(value.accessed()), - permissions: Some(FsPermissions::from(value.permissions())), - } - } -} - -fn system_time_to_timestamp(res: IoResult) -> Option { - match res { - Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) { - Ok(d) => DateTime::from_unix_timestamp_float(d.as_secs_f64()).ok(), - Err(_) => None, - }, - Err(_) => None, - } -} diff --git a/crates/lune/src/lune/builtins/fs/mod.rs b/crates/lune/src/lune/builtins/fs/mod.rs deleted file mode 100644 index 0db1f7f..0000000 --- a/crates/lune/src/lune/builtins/fs/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::io::ErrorKind as IoErrorKind; -use std::path::{PathBuf, MAIN_SEPARATOR}; - -use bstr::{BString, ByteSlice}; -use mlua::prelude::*; -use tokio::fs; - -use crate::lune::util::TableBuilder; - -mod copy; -mod metadata; -mod options; - -use copy::copy; -use metadata::FsMetadata; -use options::FsWriteOptions; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_async_function("readFile", fs_read_file)? - .with_async_function("readDir", fs_read_dir)? - .with_async_function("writeFile", fs_write_file)? - .with_async_function("writeDir", fs_write_dir)? - .with_async_function("removeFile", fs_remove_file)? - .with_async_function("removeDir", fs_remove_dir)? - .with_async_function("metadata", fs_metadata)? - .with_async_function("isFile", fs_is_file)? - .with_async_function("isDir", fs_is_dir)? - .with_async_function("move", fs_move)? - .with_async_function("copy", fs_copy)? - .build_readonly() -} - -async fn fs_read_file(lua: &Lua, path: String) -> LuaResult { - let bytes = fs::read(&path).await.into_lua_err()?; - - lua.create_string(bytes) -} - -async fn fs_read_dir(_: &Lua, path: String) -> LuaResult> { - let mut dir_strings = Vec::new(); - let mut dir = fs::read_dir(&path).await.into_lua_err()?; - while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? { - if let Some(dir_path_str) = dir_entry.path().to_str() { - dir_strings.push(dir_path_str.to_owned()); - } else { - return Err(LuaError::RuntimeError(format!( - "File path could not be converted into a string: '{}'", - dir_entry.path().display() - ))); - } - } - let mut dir_string_prefix = path; - if !dir_string_prefix.ends_with(MAIN_SEPARATOR) { - dir_string_prefix.push(MAIN_SEPARATOR); - } - let dir_strings_no_prefix = dir_strings - .iter() - .map(|inner_path| { - inner_path - .trim() - .trim_start_matches(&dir_string_prefix) - .to_owned() - }) - .collect::>(); - Ok(dir_strings_no_prefix) -} - -async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> { - fs::write(&path, contents.as_bytes()).await.into_lua_err() -} - -async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> { - fs::create_dir_all(&path).await.into_lua_err() -} - -async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> { - fs::remove_file(&path).await.into_lua_err() -} - -async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> { - fs::remove_dir_all(&path).await.into_lua_err() -} - -async fn fs_metadata(_: &Lua, path: String) -> LuaResult { - match fs::metadata(path).await { - Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()), - Ok(meta) => Ok(FsMetadata::from(meta)), - Err(e) => Err(e.into()), - } -} - -async fn fs_is_file(_: &Lua, path: String) -> LuaResult { - match fs::metadata(path).await { - Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false), - Ok(meta) => Ok(meta.is_file()), - Err(e) => Err(e.into()), - } -} - -async fn fs_is_dir(_: &Lua, path: String) -> LuaResult { - match fs::metadata(path).await { - Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false), - Ok(meta) => Ok(meta.is_dir()), - Err(e) => Err(e.into()), - } -} - -async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> { - let path_from = PathBuf::from(from); - if !path_from.exists() { - return Err(LuaError::RuntimeError(format!( - "No file or directory exists at the path '{}'", - path_from.display() - ))); - } - let path_to = PathBuf::from(to); - if !options.overwrite && path_to.exists() { - return Err(LuaError::RuntimeError(format!( - "A file or directory already exists at the path '{}'", - path_to.display() - ))); - } - fs::rename(path_from, path_to).await.into_lua_err()?; - Ok(()) -} - -async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> { - copy(from, to, options).await -} diff --git a/crates/lune/src/lune/builtins/fs/options.rs b/crates/lune/src/lune/builtins/fs/options.rs deleted file mode 100644 index d33c8f4..0000000 --- a/crates/lune/src/lune/builtins/fs/options.rs +++ /dev/null @@ -1,31 +0,0 @@ -use mlua::prelude::*; - -#[derive(Debug, Clone, Copy)] -pub struct FsWriteOptions { - pub(crate) overwrite: bool, -} - -impl<'lua> FromLua<'lua> for FsWriteOptions { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - Ok(match value { - LuaValue::Nil => Self { overwrite: false }, - LuaValue::Boolean(b) => Self { overwrite: b }, - LuaValue::Table(t) => { - let overwrite: Option = t.get("overwrite")?; - Self { - overwrite: overwrite.unwrap_or(false), - } - } - _ => { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "FsWriteOptions", - message: Some(format!( - "Invalid write options - expected boolean or table, got {}", - value.type_name() - )), - }) - } - }) - } -} diff --git a/crates/lune/src/lune/builtins/luau/mod.rs b/crates/lune/src/lune/builtins/luau/mod.rs deleted file mode 100644 index 89ec972..0000000 --- a/crates/lune/src/lune/builtins/luau/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -use mlua::prelude::*; - -use crate::lune::util::TableBuilder; - -mod options; -use options::{LuauCompileOptions, LuauLoadOptions}; - -const BYTECODE_ERROR_BYTE: u8 = 0; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_function("compile", compile_source)? - .with_function("load", load_source)? - .build_readonly() -} - -fn compile_source<'lua>( - lua: &'lua Lua, - (source, options): (LuaString<'lua>, LuauCompileOptions), -) -> LuaResult> { - let bytecode = options.into_compiler().compile(source); - - match bytecode.first() { - Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError( - String::from_utf8_lossy(&bytecode).into_owned(), - )), - Some(_) => lua.create_string(bytecode), - None => panic!("Compiling resulted in empty bytecode"), - } -} - -fn load_source<'lua>( - lua: &'lua Lua, - (source, options): (LuaString<'lua>, LuauLoadOptions), -) -> LuaResult> { - let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name); - - if let Some(environment) = options.environment { - let environment_with_globals = lua.create_table()?; - - if let Some(meta) = environment.get_metatable() { - environment_with_globals.set_metatable(Some(meta)); - } - - for pair in lua.globals().pairs() { - let (key, value): (LuaValue, LuaValue) = pair?; - environment_with_globals.set(key, value)?; - } - - for pair in environment.pairs() { - let (key, value): (LuaValue, LuaValue) = pair?; - environment_with_globals.set(key, value)?; - } - - chunk = chunk.set_environment(environment_with_globals); - } - - chunk.into_function() -} diff --git a/crates/lune/src/lune/builtins/luau/options.rs b/crates/lune/src/lune/builtins/luau/options.rs deleted file mode 100644 index b34df9d..0000000 --- a/crates/lune/src/lune/builtins/luau/options.rs +++ /dev/null @@ -1,123 +0,0 @@ -use mlua::prelude::*; -use mlua::Compiler as LuaCompiler; - -const DEFAULT_DEBUG_NAME: &str = "luau.load(...)"; - -pub struct LuauCompileOptions { - pub(crate) optimization_level: u8, - pub(crate) coverage_level: u8, - pub(crate) debug_level: u8, -} - -impl LuauCompileOptions { - pub fn into_compiler(self) -> LuaCompiler { - LuaCompiler::default() - .set_optimization_level(self.optimization_level) - .set_coverage_level(self.coverage_level) - .set_debug_level(self.debug_level) - } -} - -impl Default for LuauCompileOptions { - fn default() -> Self { - // NOTE: This is the same as LuaCompiler::default() values, but they are - // not accessible from outside of mlua so we need to recreate them here. - Self { - optimization_level: 1, - coverage_level: 0, - debug_level: 1, - } - } -} - -impl<'lua> FromLua<'lua> for LuauCompileOptions { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - Ok(match value { - LuaValue::Nil => Self::default(), - LuaValue::Table(t) => { - let mut options = Self::default(); - - let get_and_check = |name: &'static str| -> LuaResult> { - match t.get(name)? { - Some(n @ (0..=2)) => Ok(Some(n)), - Some(n) => Err(LuaError::runtime(format!( - "'{name}' must be one of: 0, 1, or 2 - got {n}" - ))), - None => Ok(None), - } - }; - - if let Some(optimization_level) = get_and_check("optimizationLevel")? { - options.optimization_level = optimization_level; - } - if let Some(coverage_level) = get_and_check("coverageLevel")? { - options.coverage_level = coverage_level; - } - if let Some(debug_level) = get_and_check("debugLevel")? { - options.debug_level = debug_level; - } - - options - } - _ => { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "CompileOptions", - message: Some(format!( - "Invalid compile options - expected table, got {}", - value.type_name() - )), - }) - } - }) - } -} - -pub struct LuauLoadOptions<'lua> { - pub(crate) debug_name: String, - pub(crate) environment: Option>, -} - -impl Default for LuauLoadOptions<'_> { - fn default() -> Self { - Self { - debug_name: DEFAULT_DEBUG_NAME.to_string(), - environment: None, - } - } -} - -impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - Ok(match value { - LuaValue::Nil => Self::default(), - LuaValue::Table(t) => { - let mut options = Self::default(); - - if let Some(debug_name) = t.get("debugName")? { - options.debug_name = debug_name; - } - - if let Some(environment) = t.get("environment")? { - options.environment = Some(environment); - } - - options - } - LuaValue::String(s) => Self { - debug_name: s.to_string_lossy().to_string(), - environment: None, - }, - _ => { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "LoadOptions", - message: Some(format!( - "Invalid load options - expected string or table, got {}", - value.type_name() - )), - }) - } - }) - } -} diff --git a/crates/lune/src/lune/builtins/mod.rs b/crates/lune/src/lune/builtins/mod.rs deleted file mode 100644 index 8006a80..0000000 --- a/crates/lune/src/lune/builtins/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::str::FromStr; - -use mlua::prelude::*; - -mod datetime; -mod fs; -mod luau; -mod net; -mod process; -mod regex; -mod serde; -mod stdio; -mod task; - -#[cfg(feature = "roblox")] -mod roblox; - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum LuneBuiltin { - DateTime, - Fs, - Luau, - Net, - Task, - Process, - Regex, - Serde, - Stdio, - #[cfg(feature = "roblox")] - Roblox, -} - -impl LuneBuiltin { - pub fn name(&self) -> &'static str { - match self { - Self::DateTime => "datetime", - Self::Fs => "fs", - Self::Luau => "luau", - Self::Net => "net", - Self::Task => "task", - Self::Process => "process", - Self::Regex => "regex", - Self::Serde => "serde", - Self::Stdio => "stdio", - #[cfg(feature = "roblox")] - Self::Roblox => "roblox", - } - } - - pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult> { - let res = match self { - Self::DateTime => datetime::create(lua), - Self::Fs => fs::create(lua), - Self::Luau => luau::create(lua), - Self::Net => net::create(lua), - Self::Task => task::create(lua), - Self::Process => process::create(lua), - Self::Regex => regex::create(lua), - Self::Serde => serde::create(lua), - Self::Stdio => stdio::create(lua), - #[cfg(feature = "roblox")] - Self::Roblox => roblox::create(lua), - }; - match res { - Ok(v) => v.into_lua_multi(lua), - Err(e) => Err(e.context(format!( - "Failed to create builtin library '{}'", - self.name() - ))), - } - } -} - -impl FromStr for LuneBuiltin { - type Err = String; - fn from_str(s: &str) -> Result { - match s.trim().to_ascii_lowercase().as_str() { - "datetime" => Ok(Self::DateTime), - "fs" => Ok(Self::Fs), - "luau" => Ok(Self::Luau), - "net" => Ok(Self::Net), - "task" => Ok(Self::Task), - "process" => Ok(Self::Process), - "regex" => Ok(Self::Regex), - "serde" => Ok(Self::Serde), - "stdio" => Ok(Self::Stdio), - #[cfg(feature = "roblox")] - "roblox" => Ok(Self::Roblox), - _ => Err(format!("Unknown builtin library '{s}'")), - } - } -} diff --git a/crates/lune/src/lune/builtins/net/client.rs b/crates/lune/src/lune/builtins/net/client.rs deleted file mode 100644 index 5eb2527..0000000 --- a/crates/lune/src/lune/builtins/net/client.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::str::FromStr; - -use mlua::prelude::*; - -use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING}; - -use crate::lune::{ - builtins::serde::compress_decompress::{decompress, CompressDecompressFormat}, - util::TableBuilder, -}; - -use super::{config::RequestConfig, util::header_map_to_table}; - -const REGISTRY_KEY: &str = "NetClient"; - -pub struct NetClientBuilder { - builder: reqwest::ClientBuilder, -} - -impl NetClientBuilder { - pub fn new() -> NetClientBuilder { - Self { - builder: reqwest::ClientBuilder::new(), - } - } - - pub fn headers(mut self, headers: &[(K, V)]) -> LuaResult - where - K: AsRef, - V: AsRef<[u8]>, - { - let mut map = HeaderMap::new(); - for (key, val) in headers { - let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?; - let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?; - map.insert(hkey, hval); - } - self.builder = self.builder.default_headers(map); - Ok(self) - } - - pub fn build(self) -> LuaResult { - let client = self.builder.build().into_lua_err()?; - Ok(NetClient { inner: client }) - } -} - -#[derive(Debug, Clone)] -pub struct NetClient { - inner: reqwest::Client, -} - -impl NetClient { - pub fn from_registry(lua: &Lua) -> Self { - lua.named_registry_value(REGISTRY_KEY) - .expect("Failed to get NetClient from lua registry") - } - - pub fn into_registry(self, lua: &Lua) { - lua.set_named_registry_value(REGISTRY_KEY, self) - .expect("Failed to store NetClient in lua registry"); - } - - pub async fn request(&self, config: RequestConfig) -> LuaResult { - // Create and send the request - let mut request = self.inner.request(config.method, config.url); - for (query, values) in config.query { - request = request.query( - &values - .iter() - .map(|v| (query.as_str(), v)) - .collect::>(), - ); - } - for (header, values) in config.headers { - for value in values { - request = request.header(header.as_str(), value); - } - } - let res = request - .body(config.body.unwrap_or_default()) - .send() - .await - .into_lua_err()?; - - // Extract status, headers - let res_status = res.status().as_u16(); - let res_status_text = res.status().canonical_reason(); - let res_headers = res.headers().clone(); - - // Read response bytes - let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec(); - let mut res_decompressed = false; - - // Check for extra options, decompression - if config.options.decompress { - let decompress_format = res_headers - .iter() - .find(|(name, _)| { - name.as_str() - .eq_ignore_ascii_case(CONTENT_ENCODING.as_str()) - }) - .and_then(|(_, value)| value.to_str().ok()) - .and_then(CompressDecompressFormat::detect_from_header_str); - if let Some(format) = decompress_format { - res_bytes = decompress(format, res_bytes).await?; - res_decompressed = true; - } - } - - Ok(NetClientResponse { - ok: (200..300).contains(&res_status), - status_code: res_status, - status_message: res_status_text.unwrap_or_default().to_string(), - headers: res_headers, - body: res_bytes, - body_decompressed: res_decompressed, - }) - } -} - -impl LuaUserData for NetClient {} - -impl FromLua<'_> for NetClient { - fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - if let LuaValue::UserData(ud) = value { - if let Ok(ctx) = ud.borrow::() { - return Ok(ctx.clone()); - } - } - unreachable!("NetClient should only be used from registry") - } -} - -impl From<&Lua> for NetClient { - fn from(value: &Lua) -> Self { - value - .named_registry_value(REGISTRY_KEY) - .expect("Missing require context in lua registry") - } -} - -pub struct NetClientResponse { - ok: bool, - status_code: u16, - status_message: String, - headers: HeaderMap, - body: Vec, - body_decompressed: bool, -} - -impl NetClientResponse { - pub fn into_lua_table(self, lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_value("ok", self.ok)? - .with_value("statusCode", self.status_code)? - .with_value("statusMessage", self.status_message)? - .with_value( - "headers", - header_map_to_table(lua, self.headers, self.body_decompressed)?, - )? - .with_value("body", lua.create_string(&self.body)?)? - .build_readonly() - } -} diff --git a/crates/lune/src/lune/builtins/net/config.rs b/crates/lune/src/lune/builtins/net/config.rs deleted file mode 100644 index 1abd121..0000000 --- a/crates/lune/src/lune/builtins/net/config.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::{ - collections::HashMap, - net::{IpAddr, Ipv4Addr}, -}; - -use bstr::{BString, ByteSlice}; -use mlua::prelude::*; - -use reqwest::Method; - -use super::util::table_to_hash_map; - -const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - -const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#" -return { - status = 426, - body = "Upgrade Required", - headers = { - Upgrade = "websocket", - }, -} -"#; - -// Net request config - -#[derive(Debug, Clone)] -pub struct RequestConfigOptions { - pub decompress: bool, -} - -impl Default for RequestConfigOptions { - fn default() -> Self { - Self { decompress: true } - } -} - -impl<'lua> FromLua<'lua> for RequestConfigOptions { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - if let LuaValue::Nil = value { - // Nil means default options - Ok(Self::default()) - } else if let LuaValue::Table(tab) = value { - // Table means custom options - let decompress = match tab.get::<_, Option>("decompress") { - Ok(decomp) => Ok(decomp.unwrap_or(true)), - Err(_) => Err(LuaError::RuntimeError( - "Invalid option value for 'decompress' in request config options".to_string(), - )), - }?; - Ok(Self { decompress }) - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "RequestConfigOptions", - message: Some(format!( - "Invalid request config options - expected table or nil, got {}", - value.type_name() - )), - }) - } - } -} - -#[derive(Debug, Clone)] -pub struct RequestConfig { - pub url: String, - pub method: Method, - pub query: HashMap>, - pub headers: HashMap>, - pub body: Option>, - pub options: RequestConfigOptions, -} - -impl FromLua<'_> for RequestConfig { - fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult { - // If we just got a string we assume its a GET request to a given url - if let LuaValue::String(s) = value { - Ok(Self { - url: s.to_string_lossy().to_string(), - method: Method::GET, - query: HashMap::new(), - headers: HashMap::new(), - body: None, - options: Default::default(), - }) - } else if let LuaValue::Table(tab) = value { - // If we got a table we are able to configure the entire request - // Extract url - let url = match tab.get::<_, LuaString>("url") { - Ok(config_url) => Ok(config_url.to_string_lossy().to_string()), - Err(_) => Err(LuaError::runtime("Missing 'url' in request config")), - }?; - // Extract method - let method = match tab.get::<_, LuaString>("method") { - Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(), - Err(_) => "GET".to_string(), - }; - // Extract query - let query = match tab.get::<_, LuaTable>("query") { - Ok(tab) => table_to_hash_map(tab, "query")?, - Err(_) => HashMap::new(), - }; - // Extract headers - let headers = match tab.get::<_, LuaTable>("headers") { - Ok(tab) => table_to_hash_map(tab, "headers")?, - Err(_) => HashMap::new(), - }; - // Extract body - let body = match tab.get::<_, BString>("body") { - Ok(config_body) => Some(config_body.as_bytes().to_owned()), - Err(_) => None, - }; - - // Convert method string into proper enum - let method = method.trim().to_ascii_uppercase(); - let method = match method.as_ref() { - "GET" => Ok(Method::GET), - "POST" => Ok(Method::POST), - "PUT" => Ok(Method::PUT), - "DELETE" => Ok(Method::DELETE), - "HEAD" => Ok(Method::HEAD), - "OPTIONS" => Ok(Method::OPTIONS), - "PATCH" => Ok(Method::PATCH), - _ => Err(LuaError::RuntimeError(format!( - "Invalid request config method '{}'", - &method - ))), - }?; - // Parse any extra options given - let options = match tab.get::<_, LuaValue>("options") { - Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?, - Err(_) => RequestConfigOptions::default(), - }; - // All good, validated and we got what we need - Ok(Self { - url, - method, - query, - headers, - body, - options, - }) - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "RequestConfig", - message: Some(format!( - "Invalid request config - expected string or table, got {}", - value.type_name() - )), - }) - } - } -} - -// Net serve config - -#[derive(Debug)] -pub struct ServeConfig<'a> { - pub address: IpAddr, - pub handle_request: LuaFunction<'a>, - pub handle_web_socket: Option>, -} - -impl<'lua> FromLua<'lua> for ServeConfig<'lua> { - fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult { - if let LuaValue::Function(f) = &value { - // Single function = request handler, rest is default - Ok(ServeConfig { - handle_request: f.clone(), - handle_web_socket: None, - address: DEFAULT_IP_ADDRESS, - }) - } else if let LuaValue::Table(t) = &value { - // Table means custom options - let address: Option = t.get("address")?; - let handle_request: Option = t.get("handleRequest")?; - let handle_web_socket: Option = t.get("handleWebSocket")?; - if handle_request.is_some() || handle_web_socket.is_some() { - let address: IpAddr = match &address { - Some(addr) => { - let addr_str = addr.to_str()?; - - addr_str - .trim_start_matches("http://") - .trim_start_matches("https://") - .parse() - .map_err(|_e| LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ServeConfig", - message: Some(format!( - "IP address format is incorrect - \ - expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \ - got '{addr_str}'" - )), - })? - } - None => DEFAULT_IP_ADDRESS, - }; - - Ok(Self { - address, - handle_request: handle_request.unwrap_or_else(|| { - lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER) - .into_function() - .expect("Failed to create default http responder function") - }), - handle_web_socket, - }) - } else { - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ServeConfig", - message: Some(String::from( - "Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function", - )), - }) - } - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ServeConfig", - message: None, - }) - } - } -} diff --git a/crates/lune/src/lune/builtins/net/mod.rs b/crates/lune/src/lune/builtins/net/mod.rs deleted file mode 100644 index 9449e6b..0000000 --- a/crates/lune/src/lune/builtins/net/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -#![allow(unused_variables)] - -use bstr::BString; -use mlua::prelude::*; -use mlua_luau_scheduler::LuaSpawnExt; - -mod client; -mod config; -mod server; -mod util; -mod websocket; - -use crate::lune::util::TableBuilder; - -use self::{ - client::{NetClient, NetClientBuilder}, - config::{RequestConfig, ServeConfig}, - server::serve, - util::create_user_agent_header, - websocket::NetWebSocket, -}; - -use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; - -pub fn create(lua: &Lua) -> LuaResult { - NetClientBuilder::new() - .headers(&[("User-Agent", create_user_agent_header(lua)?)])? - .build()? - .into_registry(lua); - TableBuilder::new(lua)? - .with_function("jsonEncode", net_json_encode)? - .with_function("jsonDecode", net_json_decode)? - .with_async_function("request", net_request)? - .with_async_function("socket", net_socket)? - .with_async_function("serve", net_serve)? - .with_function("urlEncode", net_url_encode)? - .with_function("urlDecode", net_url_decode)? - .build_readonly() -} - -fn net_json_encode<'lua>( - lua: &'lua Lua, - (val, pretty): (LuaValue<'lua>, Option), -) -> LuaResult> { - EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default())) - .serialize_to_string(lua, val) -} - -fn net_json_decode(lua: &Lua, json: BString) -> LuaResult { - EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) -} - -async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult { - let client = NetClient::from_registry(lua); - // NOTE: We spawn the request as a background task to free up resources in lua - let res = lua.spawn(async move { client.request(config).await }); - res.await?.into_lua_table(lua) -} - -async fn net_socket(lua: &Lua, url: String) -> LuaResult { - let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?; - NetWebSocket::new(ws).into_lua_table(lua) -} - -async fn net_serve<'lua>( - lua: &'lua Lua, - (port, config): (u16, ServeConfig<'lua>), -) -> LuaResult> { - serve(lua, port, config).await -} - -fn net_url_encode<'lua>( - lua: &'lua Lua, - (lua_string, as_binary): (LuaString<'lua>, Option), -) -> LuaResult> { - if matches!(as_binary, Some(true)) { - urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua) - } else { - urlencoding::encode(lua_string.to_str()?).into_lua(lua) - } -} - -fn net_url_decode<'lua>( - lua: &'lua Lua, - (lua_string, as_binary): (LuaString<'lua>, Option), -) -> LuaResult> { - if matches!(as_binary, Some(true)) { - urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua) - } else { - urlencoding::decode(lua_string.to_str()?) - .map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))? - .into_lua(lua) - } -} diff --git a/crates/lune/src/lune/builtins/net/server/keys.rs b/crates/lune/src/lune/builtins/net/server/keys.rs deleted file mode 100644 index 9dac06a..0000000 --- a/crates/lune/src/lune/builtins/net/server/keys.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; - -use mlua::prelude::*; - -#[derive(Debug, Clone, Copy)] -pub(super) struct SvcKeys { - key_request: &'static str, - key_websocket: Option<&'static str>, -} - -impl SvcKeys { - pub(super) fn new<'lua>( - lua: &'lua Lua, - handle_request: LuaFunction<'lua>, - handle_websocket: Option>, - ) -> LuaResult { - static SERVE_COUNTER: AtomicUsize = AtomicUsize::new(0); - let count = SERVE_COUNTER.fetch_add(1, Ordering::Relaxed); - - // NOTE: We leak strings here, but this is an acceptable tradeoff since programs - // generally only start one or a couple of servers and they are usually never dropped. - // Leaking here lets us keep this struct Copy and access the request handler callbacks - // very performantly, significantly reducing the per-request overhead of the server. - let key_request: &'static str = - Box::leak(format!("__net_serve_request_{count}").into_boxed_str()); - let key_websocket: Option<&'static str> = if handle_websocket.is_some() { - Some(Box::leak( - format!("__net_serve_websocket_{count}").into_boxed_str(), - )) - } else { - None - }; - - lua.set_named_registry_value(key_request, handle_request)?; - if let Some(key) = key_websocket { - lua.set_named_registry_value(key, handle_websocket.unwrap())?; - } - - Ok(Self { - key_request, - key_websocket, - }) - } - - pub(super) fn has_websocket_handler(&self) -> bool { - self.key_websocket.is_some() - } - - pub(super) fn request_handler<'lua>(&self, lua: &'lua Lua) -> LuaResult> { - lua.named_registry_value(self.key_request) - } - - pub(super) fn websocket_handler<'lua>( - &self, - lua: &'lua Lua, - ) -> LuaResult>> { - self.key_websocket - .map(|key| lua.named_registry_value(key)) - .transpose() - } -} diff --git a/crates/lune/src/lune/builtins/net/server/mod.rs b/crates/lune/src/lune/builtins/net/server/mod.rs deleted file mode 100644 index 5639bcb..0000000 --- a/crates/lune/src/lune/builtins/net/server/mod.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::{ - net::SocketAddr, - rc::{Rc, Weak}, -}; - -use hyper::server::conn::http1; -use hyper_util::rt::TokioIo; -use tokio::{net::TcpListener, pin}; - -use mlua::prelude::*; -use mlua_luau_scheduler::LuaSpawnExt; - -use crate::lune::util::TableBuilder; - -use super::config::ServeConfig; - -mod keys; -mod request; -mod response; -mod service; - -use keys::SvcKeys; -use service::Svc; - -pub async fn serve<'lua>( - lua: &'lua Lua, - port: u16, - config: ServeConfig<'lua>, -) -> LuaResult> { - let addr: SocketAddr = (config.address, port).into(); - let listener = TcpListener::bind(addr).await?; - - let (lua_svc, lua_inner) = { - let rc = lua - .app_data_ref::>() - .expect("Missing weak lua ref") - .upgrade() - .expect("Lua was dropped unexpectedly"); - (Rc::clone(&rc), rc) - }; - - let keys = SvcKeys::new(lua, config.handle_request, config.handle_web_socket)?; - let svc = Svc { - lua: lua_svc, - addr, - keys, - }; - - let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); - lua.spawn_local(async move { - let mut shutdown_rx_outer = shutdown_rx.clone(); - loop { - // Create futures for accepting new connections and shutting down - let fut_shutdown = shutdown_rx_outer.changed(); - let fut_accept = async { - let stream = match listener.accept().await { - Err(_) => return, - Ok((s, _)) => s, - }; - - let io = TokioIo::new(stream); - let svc = svc.clone(); - let mut shutdown_rx_inner = shutdown_rx.clone(); - - lua_inner.spawn_local(async move { - let conn = http1::Builder::new() - .keep_alive(true) // Web sockets need this - .serve_connection(io, svc) - .with_upgrades(); - // NOTE: Because we need to use keep_alive for websockets, we need to - // also manually poll this future and handle the shutdown signal here - pin!(conn); - tokio::select! { - _ = conn.as_mut() => {} - _ = shutdown_rx_inner.changed() => { - conn.as_mut().graceful_shutdown(); - } - } - }); - }; - - // Wait for either a new connection or a shutdown signal - tokio::select! { - _ = fut_accept => {} - res = fut_shutdown => { - // NOTE: We will only get a RecvError here if the serve handle is dropped, - // this means lua has garbage collected it and the user does not want - // to manually stop the server using the serve handle. Run forever. - if res.is_ok() { - break; - } - } - } - } - }); - - TableBuilder::new(lua)? - .with_value("ip", addr.ip().to_string())? - .with_value("port", addr.port())? - .with_function("stop", move |lua, _: ()| match shutdown_tx.send(true) { - Ok(_) => Ok(()), - Err(_) => Err(LuaError::runtime("Server already stopped")), - })? - .build_readonly() -} diff --git a/crates/lune/src/lune/builtins/net/server/request.rs b/crates/lune/src/lune/builtins/net/server/request.rs deleted file mode 100644 index bab7a5d..0000000 --- a/crates/lune/src/lune/builtins/net/server/request.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{collections::HashMap, net::SocketAddr}; - -use http::request::Parts; - -use mlua::prelude::*; - -use crate::lune::util::TableBuilder; - -pub(super) struct LuaRequest { - pub(super) _remote_addr: SocketAddr, - pub(super) head: Parts, - pub(super) body: Vec, -} - -impl LuaRequest { - pub fn into_lua_table(self, lua: &Lua) -> LuaResult { - let method = self.head.method.as_str().to_string(); - let path = self.head.uri.path().to_string(); - let body = lua.create_string(&self.body)?; - - let query: HashMap = self - .head - .uri - .query() - .unwrap_or_default() - .split('&') - .filter_map(|q| q.split_once('=')) - .map(|(k, v)| { - let k = lua.create_string(k)?; - let v = lua.create_string(v)?; - Ok((k, v)) - }) - .collect::>()?; - - let headers: HashMap = self - .head - .headers - .iter() - .map(|(k, v)| { - let k = lua.create_string(k.as_str())?; - let v = lua.create_string(v.as_bytes())?; - Ok((k, v)) - }) - .collect::>()?; - - TableBuilder::new(lua)? - .with_value("method", method)? - .with_value("path", path)? - .with_value("query", query)? - .with_value("headers", headers)? - .with_value("body", body)? - .build() - } -} diff --git a/crates/lune/src/lune/builtins/net/server/response.rs b/crates/lune/src/lune/builtins/net/server/response.rs deleted file mode 100644 index 240a7cd..0000000 --- a/crates/lune/src/lune/builtins/net/server/response.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::str::FromStr; - -use bstr::{BString, ByteSlice}; -use http_body_util::Full; -use hyper::{ - body::Bytes, - header::{HeaderName, HeaderValue}, - HeaderMap, Response, -}; - -use mlua::prelude::*; - -#[derive(Debug, Clone, Copy)] -pub(super) enum LuaResponseKind { - PlainText, - Table, -} - -pub(super) struct LuaResponse { - pub(super) kind: LuaResponseKind, - pub(super) status: u16, - pub(super) headers: HeaderMap, - pub(super) body: Option>, -} - -impl LuaResponse { - pub(super) fn into_response(self) -> LuaResult>> { - Ok(match self.kind { - LuaResponseKind::PlainText => Response::builder() - .status(200) - .header("Content-Type", "text/plain") - .body(Full::new(Bytes::from(self.body.unwrap()))) - .into_lua_err()?, - LuaResponseKind::Table => { - let mut response = Response::builder() - .status(self.status) - .body(Full::new(Bytes::from(self.body.unwrap_or_default()))) - .into_lua_err()?; - response.headers_mut().extend(self.headers); - response - } - }) - } -} - -impl FromLua<'_> for LuaResponse { - fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - match value { - // Plain strings from the handler are plaintext responses - LuaValue::String(s) => Ok(Self { - kind: LuaResponseKind::PlainText, - status: 200, - headers: HeaderMap::new(), - body: Some(s.as_bytes().to_vec()), - }), - // Tables are more detailed responses with potential status, headers, body - LuaValue::Table(t) => { - let status: Option = t.get("status")?; - let headers: Option = t.get("headers")?; - let body: Option = t.get("body")?; - - let mut headers_map = HeaderMap::new(); - if let Some(headers) = headers { - for pair in headers.pairs::() { - let (h, v) = pair?; - let name = HeaderName::from_str(&h).into_lua_err()?; - let value = HeaderValue::from_bytes(v.as_bytes()).into_lua_err()?; - headers_map.insert(name, value); - } - } - - let body_bytes = body.map(|s| s.as_bytes().to_vec()); - - Ok(Self { - kind: LuaResponseKind::Table, - status: status.unwrap_or(200), - headers: headers_map, - body: body_bytes, - }) - } - // Anything else is an error - value => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "NetServeResponse", - message: None, - }), - } - } -} diff --git a/crates/lune/src/lune/builtins/net/server/service.rs b/crates/lune/src/lune/builtins/net/server/service.rs deleted file mode 100644 index 7bc7e53..0000000 --- a/crates/lune/src/lune/builtins/net/server/service.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc}; - -use http_body_util::{BodyExt, Full}; -use hyper::{ - body::{Bytes, Incoming}, - service::Service, - Request, Response, -}; -use hyper_tungstenite::{is_upgrade_request, upgrade}; - -use mlua::prelude::*; -use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt}; - -use super::{ - super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse, -}; - -#[derive(Debug, Clone)] -pub(super) struct Svc { - pub(super) lua: Rc, - pub(super) addr: SocketAddr, - pub(super) keys: SvcKeys, -} - -impl Service> for Svc { - type Response = Response>; - type Error = LuaError; - type Future = Pin>>>; - - fn call(&self, req: Request) -> Self::Future { - let lua = self.lua.clone(); - let addr = self.addr; - let keys = self.keys; - - if keys.has_websocket_handler() && is_upgrade_request(&req) { - Box::pin(async move { - let (res, sock) = upgrade(req, None).into_lua_err()?; - - let lua_inner = lua.clone(); - lua.spawn_local(async move { - let sock = sock.await.unwrap(); - let lua_sock = NetWebSocket::new(sock); - let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap(); - - let handler_websocket: LuaFunction = - keys.websocket_handler(&lua_inner).unwrap().unwrap(); - - lua_inner - .push_thread_back(handler_websocket, lua_tab) - .unwrap(); - }); - - Ok(res) - }) - } else { - let (head, body) = req.into_parts(); - - Box::pin(async move { - let handler_request: LuaFunction = keys.request_handler(&lua).unwrap(); - - let body = body.collect().await.into_lua_err()?; - let body = body.to_bytes().to_vec(); - - let lua_req = LuaRequest { - _remote_addr: addr, - head, - body, - }; - let lua_req_table = lua_req.into_lua_table(&lua)?; - - let thread_id = lua.push_thread_back(handler_request, lua_req_table)?; - lua.track_thread(thread_id); - lua.wait_for_thread(thread_id).await; - let thread_res = lua - .get_thread_result(thread_id) - .expect("Missing handler thread result")?; - - LuaResponse::from_lua_multi(thread_res, &lua)?.into_response() - }) - } - } -} diff --git a/crates/lune/src/lune/builtins/net/util.rs b/crates/lune/src/lune/builtins/net/util.rs deleted file mode 100644 index e18235e..0000000 --- a/crates/lune/src/lune/builtins/net/util.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::collections::HashMap; - -use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH}; -use reqwest::header::HeaderMap; - -use mlua::prelude::*; - -use crate::lune::util::TableBuilder; - -pub fn create_user_agent_header(lua: &Lua) -> LuaResult { - let version_global = lua - .globals() - .get::<_, LuaString>("_VERSION") - .expect("Missing _VERSION global"); - - let version_global_str = version_global - .to_str() - .context("Invalid utf8 found in _VERSION global")?; - - let (package_name, full_version) = version_global_str.split_once(' ').unwrap(); - - Ok(format!("{}/{}", package_name.to_lowercase(), full_version)) -} - -pub fn header_map_to_table( - lua: &Lua, - headers: HeaderMap, - remove_content_headers: bool, -) -> LuaResult { - let mut res_headers: HashMap> = HashMap::new(); - for (name, value) in headers.iter() { - let name = name.as_str(); - let value = value.to_str().unwrap().to_owned(); - if let Some(existing) = res_headers.get_mut(name) { - existing.push(value); - } else { - res_headers.insert(name.to_owned(), vec![value]); - } - } - - if remove_content_headers { - let content_encoding_header_str = CONTENT_ENCODING.as_str(); - let content_length_header_str = CONTENT_LENGTH.as_str(); - res_headers.retain(|name, _| { - name != content_encoding_header_str && name != content_length_header_str - }); - } - - let mut builder = TableBuilder::new(lua)?; - for (name, mut values) in res_headers { - if values.len() == 1 { - let value = values.pop().unwrap().into_lua(lua)?; - builder = builder.with_value(name, value)?; - } else { - let values = TableBuilder::new(lua)? - .with_sequential_values(values)? - .build_readonly()? - .into_lua(lua)?; - builder = builder.with_value(name, values)?; - } - } - - builder.build_readonly() -} - -pub fn table_to_hash_map( - tab: LuaTable, - tab_origin_key: &'static str, -) -> LuaResult>> { - let mut map = HashMap::new(); - - for pair in tab.pairs::() { - let (key, value) = pair?; - match value { - LuaValue::String(s) => { - map.insert(key, vec![s.to_str()?.to_owned()]); - } - LuaValue::Table(t) => { - let mut values = Vec::new(); - for value in t.sequence_values::() { - values.push(value?.to_str()?.to_owned()); - } - map.insert(key, values); - } - _ => { - return Err(LuaError::runtime(format!( - "Value for '{tab_origin_key}' must be a string or array of strings", - ))) - } - } - } - - Ok(map) -} diff --git a/crates/lune/src/lune/builtins/net/websocket.rs b/crates/lune/src/lune/builtins/net/websocket.rs deleted file mode 100644 index 5dba4ec..0000000 --- a/crates/lune/src/lune/builtins/net/websocket.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, AtomicU16, Ordering}, - Arc, -}; - -use bstr::{BString, ByteSlice}; -use mlua::prelude::*; - -use futures_util::{ - stream::{SplitSink, SplitStream}, - SinkExt, StreamExt, -}; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - sync::Mutex as AsyncMutex, -}; - -use hyper_tungstenite::{ - tungstenite::{ - protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame}, - Message as WsMessage, - }, - WebSocketStream, -}; - -use crate::lune::util::TableBuilder; - -// Wrapper implementation for compatibility and changing colon syntax to dot syntax -const WEB_SOCKET_IMPL_LUA: &str = r#" -return freeze(setmetatable({ - close = function(...) - return websocket:close(...) - end, - send = function(...) - return websocket:send(...) - end, - next = function(...) - return websocket:next(...) - end, -}, { - __index = function(self, key) - if key == "closeCode" then - return websocket.closeCode - end - end, -})) -"#; - -#[derive(Debug)] -pub struct NetWebSocket { - close_code_exists: Arc, - close_code_value: Arc, - read_stream: Arc>>>, - write_stream: Arc, WsMessage>>>, -} - -impl Clone for NetWebSocket { - fn clone(&self) -> Self { - Self { - close_code_exists: Arc::clone(&self.close_code_exists), - close_code_value: Arc::clone(&self.close_code_value), - read_stream: Arc::clone(&self.read_stream), - write_stream: Arc::clone(&self.write_stream), - } - } -} - -impl NetWebSocket -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - pub fn new(value: WebSocketStream) -> Self { - let (write, read) = value.split(); - - Self { - close_code_exists: Arc::new(AtomicBool::new(false)), - close_code_value: Arc::new(AtomicU16::new(0)), - read_stream: Arc::new(AsyncMutex::new(read)), - write_stream: Arc::new(AsyncMutex::new(write)), - } - } - - fn get_close_code(&self) -> Option { - if self.close_code_exists.load(Ordering::Relaxed) { - Some(self.close_code_value.load(Ordering::Relaxed)) - } else { - None - } - } - - fn set_close_code(&self, code: u16) { - self.close_code_exists.store(true, Ordering::Relaxed); - self.close_code_value.store(code, Ordering::Relaxed); - } - - pub async fn send(&self, msg: WsMessage) -> LuaResult<()> { - let mut ws = self.write_stream.lock().await; - ws.send(msg).await.into_lua_err() - } - - pub async fn next(&self) -> LuaResult> { - let mut ws = self.read_stream.lock().await; - ws.next().await.transpose().into_lua_err() - } - - pub async fn close(&self, code: Option) -> LuaResult<()> { - if self.close_code_exists.load(Ordering::Relaxed) { - return Err(LuaError::runtime("Socket has already been closed")); - } - - self.send(WsMessage::Close(Some(WsCloseFrame { - code: match code { - Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code), - Some(code) => { - return Err(LuaError::runtime(format!( - "Close code must be between 1000 and 4999, got {code}" - ))) - } - None => WsCloseCode::Normal, - }, - reason: "".into(), - }))) - .await?; - - let mut ws = self.write_stream.lock().await; - ws.close().await.into_lua_err() - } - - pub fn into_lua_table(self, lua: &Lua) -> LuaResult { - let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?; - let table_freeze = lua - .globals() - .get::<_, LuaTable>("table")? - .get::<_, LuaFunction>("freeze")?; - - let env = TableBuilder::new(lua)? - .with_value("websocket", self.clone())? - .with_value("setmetatable", setmetatable)? - .with_value("freeze", table_freeze)? - .build_readonly()?; - - lua.load(WEB_SOCKET_IMPL_LUA) - .set_name("websocket") - .set_environment(env) - .eval() - } -} - -impl LuaUserData for NetWebSocket -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code())); - } - - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_async_method("close", |lua, this, code: Option| async move { - this.close(code).await - }); - - methods.add_async_method( - "send", - |_, this, (string, as_binary): (BString, Option)| async move { - this.send(if as_binary.unwrap_or_default() { - WsMessage::Binary(string.as_bytes().to_vec()) - } else { - let s = string.to_str().into_lua_err()?; - WsMessage::Text(s.to_string()) - }) - .await - }, - ); - - methods.add_async_method("next", |lua, this, _: ()| async move { - let msg = this.next().await?; - - if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() { - this.set_close_code(frame.code.into()); - } - - Ok(match msg { - Some(WsMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?), - Some(WsMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?), - Some(WsMessage::Close(_)) | None => LuaValue::Nil, - // Ignore ping/pong/frame messages, they are handled by tungstenite - msg => unreachable!("Unhandled message: {:?}", msg), - }) - }); - } -} diff --git a/crates/lune/src/lune/builtins/process/mod.rs b/crates/lune/src/lune/builtins/process/mod.rs deleted file mode 100644 index b64e9aa..0000000 --- a/crates/lune/src/lune/builtins/process/mod.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - env::{self, consts}, - path, - process::Stdio, -}; - -use mlua::prelude::*; -use mlua_luau_scheduler::{Functions, LuaSpawnExt}; -use os_str_bytes::RawOsString; -use tokio::io::AsyncWriteExt; - -use crate::lune::util::{paths::CWD, TableBuilder}; - -mod tee_writer; - -mod options; -use options::ProcessSpawnOptions; - -mod wait_for_child; -use wait_for_child::{wait_for_child, WaitForChildResult}; - -pub fn create(lua: &Lua) -> LuaResult { - let cwd_str = { - let cwd_str = CWD.to_string_lossy().to_string(); - if !cwd_str.ends_with(path::MAIN_SEPARATOR) { - format!("{cwd_str}{}", path::MAIN_SEPARATOR) - } else { - cwd_str - } - }; - // Create constants for OS & processor architecture - let os = lua.create_string(&consts::OS.to_lowercase())?; - let arch = lua.create_string(&consts::ARCH.to_lowercase())?; - // Create 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)? - .with_sequential_values(args_vec)? - .build_readonly()?; - // Create proxied table for env that gets & sets real env vars - let env_tab = TableBuilder::new(lua)? - .with_metatable( - TableBuilder::new(lua)? - .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)?; - let process_exit = fns.exit; - // Create the full process table - TableBuilder::new(lua)? - .with_value("os", os)? - .with_value("arch", arch)? - .with_value("args", args_tab)? - .with_value("cwd", cwd_str)? - .with_value("env", env_tab)? - .with_value("exit", process_exit)? - .with_async_function("spawn", process_spawn)? - .build_readonly() -} - -fn process_env_get<'lua>( - lua: &'lua Lua, - (_, key): (LuaValue<'lua>, 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>( - _: &'lua Lua, - (_, key, value): (LuaValue<'lua>, 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 { - match value { - Some(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(()) - } - } - None => { - env::remove_var(&key); - Ok(()) - } - } - } -} - -fn process_env_iter<'lua>( - lua: &'lua Lua, - (_, _): (LuaValue<'lua>, ()), -) -> 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_spawn( - lua: &Lua, - (program, args, options): (String, Option>, ProcessSpawnOptions), -) -> LuaResult { - let res = lua - .spawn(spawn_command(program, args, options)) - .await - .expect("Failed to receive result of spawned process"); - - /* - NOTE: If an exit code was not given by the child process, - we default to 1 if it yielded any error output, otherwise 0 - - An exit code may be missing if the process was terminated by - some external signal, which is the only time we use this default - */ - let code = res.status.code().unwrap_or(match res.stderr.is_empty() { - true => 0, - false => 1, - }); - - // Construct and return a readonly lua table with results - TableBuilder::new(lua)? - .with_value("ok", code == 0)? - .with_value("code", code)? - .with_value("stdout", lua.create_string(&res.stdout)?)? - .with_value("stderr", lua.create_string(&res.stderr)?)? - .build_readonly() -} - -async fn spawn_command( - program: String, - args: Option>, - mut options: ProcessSpawnOptions, -) -> LuaResult { - let stdout = options.stdio.stdout; - let stderr = options.stdio.stderr; - let stdin = options.stdio.stdin.take(); - - let mut child = options - .into_command(program, args) - .stdin(match stdin.is_some() { - true => Stdio::piped(), - false => Stdio::null(), - }) - .stdout(stdout.as_stdio()) - .stderr(stderr.as_stdio()) - .spawn()?; - - if let Some(stdin) = stdin { - let mut child_stdin = child.stdin.take().unwrap(); - child_stdin.write_all(&stdin).await.into_lua_err()?; - } - - wait_for_child(child, stdout, stderr).await -} diff --git a/crates/lune/src/lune/builtins/process/options/kind.rs b/crates/lune/src/lune/builtins/process/options/kind.rs deleted file mode 100644 index 3e0f39c..0000000 --- a/crates/lune/src/lune/builtins/process/options/kind.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{fmt, process::Stdio, str::FromStr}; - -use itertools::Itertools; -use mlua::prelude::*; - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum ProcessSpawnOptionsStdioKind { - // TODO: We need better more obvious names - // for these, but that is a breaking change - #[default] - Default, - Forward, - Inherit, - None, -} - -impl ProcessSpawnOptionsStdioKind { - pub fn all() -> &'static [Self] { - &[Self::Default, Self::Forward, Self::Inherit, Self::None] - } - - pub fn as_stdio(self) -> Stdio { - match self { - Self::None => Stdio::null(), - Self::Forward => Stdio::inherit(), - _ => Stdio::piped(), - } - } -} - -impl fmt::Display for ProcessSpawnOptionsStdioKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match *self { - Self::Default => "default", - Self::Forward => "forward", - Self::Inherit => "inherit", - Self::None => "none", - }; - f.write_str(s) - } -} - -impl FromStr for ProcessSpawnOptionsStdioKind { - type Err = LuaError; - fn from_str(s: &str) -> Result { - Ok(match s.trim().to_ascii_lowercase().as_str() { - "default" => Self::Default, - "forward" => Self::Forward, - "inherit" => Self::Inherit, - "none" => Self::None, - _ => { - return Err(LuaError::RuntimeError(format!( - "Invalid spawn options stdio kind - got '{}', expected one of {}", - s, - ProcessSpawnOptionsStdioKind::all() - .iter() - .map(|k| format!("'{k}'")) - .join(", ") - ))) - } - }) - } -} - -impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdioKind { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - match value { - LuaValue::Nil => Ok(Self::default()), - LuaValue::String(s) => s.to_str()?.parse(), - _ => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ProcessSpawnOptionsStdioKind", - message: Some(format!( - "Invalid spawn options stdio kind - expected string, got {}", - value.type_name() - )), - }), - } - } -} diff --git a/crates/lune/src/lune/builtins/process/options/mod.rs b/crates/lune/src/lune/builtins/process/options/mod.rs deleted file mode 100644 index d37f844..0000000 --- a/crates/lune/src/lune/builtins/process/options/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::{ - collections::HashMap, - env::{self}, - path::PathBuf, -}; - -use directories::UserDirs; -use mlua::prelude::*; -use tokio::process::Command; - -mod kind; -mod stdio; - -pub(super) use kind::*; -pub(super) use stdio::*; - -#[derive(Debug, Clone, Default)] -pub(super) struct ProcessSpawnOptions { - pub cwd: Option, - pub envs: HashMap, - pub shell: Option, - pub stdio: ProcessSpawnOptionsStdio, -} - -impl<'lua> FromLua<'lua> for ProcessSpawnOptions { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - let mut this = Self::default(); - let value = match value { - LuaValue::Nil => return Ok(this), - LuaValue::Table(t) => t, - _ => { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ProcessSpawnOptions", - message: Some(format!( - "Invalid spawn options - expected table, got {}", - value.type_name() - )), - }) - } - }; - - /* - If we got a working directory to use: - - 1. Substitute leading tilde (~) for the users home dir - 2. Make sure it exists - */ - match value.get("cwd")? { - LuaValue::Nil => {} - LuaValue::String(s) => { - let mut cwd = PathBuf::from(s.to_str()?); - if let Ok(stripped) = cwd.strip_prefix("~") { - let user_dirs = UserDirs::new().ok_or_else(|| { - LuaError::runtime( - "Invalid value for option 'cwd' - failed to get home directory", - ) - })?; - cwd = user_dirs.home_dir().join(stripped) - } - if !cwd.exists() { - return Err(LuaError::runtime( - "Invalid value for option 'cwd' - path does not exist", - )); - }; - this.cwd = Some(cwd); - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'cwd' - expected string, got '{}'", - value.type_name() - ))) - } - } - - /* - If we got environment variables, make sure they are strings - */ - match value.get("env")? { - LuaValue::Nil => {} - LuaValue::Table(e) => { - for pair in e.pairs::() { - let (k, v) = pair.context("Environment variables must be strings")?; - this.envs.insert(k, v); - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'env' - expected table, got '{}'", - value.type_name() - ))) - } - } - - /* - If we got a shell to use: - - 1. When given as a string, use that literally - 2. When set to true, use a default shell for the platform - */ - match value.get("shell")? { - LuaValue::Nil => {} - LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()), - LuaValue::Boolean(true) => { - this.shell = match env::consts::FAMILY { - "unix" => Some("/bin/sh".to_string()), - "windows" => Some("powershell".to_string()), - _ => None, - }; - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", - value.type_name() - ))) - } - } - - /* - If we got options for stdio handling, parse those as well - note that - we accept a separate "stdin" value here for compatibility with older - scripts, but the user should preferrably pass it in the stdio table - */ - this.stdio = value.get("stdio")?; - match value.get("stdin")? { - LuaValue::Nil => {} - LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()), - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'stdin' - expected 'string', got '{}'", - value.type_name() - ))) - } - } - - Ok(this) - } -} - -impl ProcessSpawnOptions { - pub fn into_command(self, program: impl Into, args: Option>) -> Command { - let mut program = program.into(); - - // 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) - } - }; - - // 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 - } - }; - - // Set dir to run in and env variables - if let Some(cwd) = self.cwd { - cmd.current_dir(cwd); - } - if !self.envs.is_empty() { - cmd.envs(self.envs); - } - - cmd - } -} diff --git a/crates/lune/src/lune/builtins/process/options/stdio.rs b/crates/lune/src/lune/builtins/process/options/stdio.rs deleted file mode 100644 index 4c12ff4..0000000 --- a/crates/lune/src/lune/builtins/process/options/stdio.rs +++ /dev/null @@ -1,56 +0,0 @@ -use mlua::prelude::*; - -use super::kind::ProcessSpawnOptionsStdioKind; - -#[derive(Debug, Clone, Default)] -pub struct ProcessSpawnOptionsStdio { - pub stdout: ProcessSpawnOptionsStdioKind, - pub stderr: ProcessSpawnOptionsStdioKind, - pub stdin: Option>, -} - -impl From for ProcessSpawnOptionsStdio { - fn from(value: ProcessSpawnOptionsStdioKind) -> Self { - Self { - stdout: value, - stderr: value, - ..Default::default() - } - } -} - -impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdio { - fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult { - match value { - LuaValue::Nil => Ok(Self::default()), - LuaValue::String(s) => { - Ok(ProcessSpawnOptionsStdioKind::from_lua(LuaValue::String(s), lua)?.into()) - } - LuaValue::Table(t) => { - let mut this = Self::default(); - - if let Some(stdin) = t.get("stdin")? { - this.stdin = stdin; - } - - if let Some(stdout) = t.get("stdout")? { - this.stdout = stdout; - } - - if let Some(stderr) = t.get("stderr")? { - this.stderr = stderr; - } - - Ok(this) - } - _ => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ProcessSpawnOptionsStdio", - message: Some(format!( - "Invalid spawn options stdio - expected string or table, got {}", - value.type_name() - )), - }), - } - } -} diff --git a/crates/lune/src/lune/builtins/process/tee_writer.rs b/crates/lune/src/lune/builtins/process/tee_writer.rs deleted file mode 100644 index fee7776..0000000 --- a/crates/lune/src/lune/builtins/process/tee_writer.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{ - io::Write, - pin::Pin, - task::{Context, Poll}, -}; - -use pin_project::pin_project; -use tokio::io::{self, AsyncWrite}; - -#[pin_project] -pub struct AsyncTeeWriter<'a, W> -where - W: AsyncWrite + Unpin, -{ - #[pin] - writer: &'a mut W, - buffer: Vec, -} - -impl<'a, W> AsyncTeeWriter<'a, W> -where - W: AsyncWrite + Unpin, -{ - pub fn new(writer: &'a mut W) -> Self { - Self { - writer, - buffer: Vec::new(), - } - } - - pub fn into_vec(self) -> Vec { - self.buffer - } -} - -impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W> -where - W: AsyncWrite + Unpin, -{ - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let mut this = self.project(); - match this.writer.as_mut().poll_write(cx, buf) { - Poll::Ready(res) => { - this.buffer - .write_all(buf) - .expect("Failed to write to internal tee buffer"); - Poll::Ready(res) - } - Poll::Pending => Poll::Pending, - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.as_mut().poll_flush(cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.as_mut().poll_shutdown(cx) - } -} diff --git a/crates/lune/src/lune/builtins/process/wait_for_child.rs b/crates/lune/src/lune/builtins/process/wait_for_child.rs deleted file mode 100644 index f126efe..0000000 --- a/crates/lune/src/lune/builtins/process/wait_for_child.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::process::ExitStatus; - -use mlua::prelude::*; -use tokio::{ - io::{self, AsyncRead, AsyncReadExt}, - process::Child, - task, -}; - -use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter}; - -#[derive(Debug, Clone)] -pub(super) struct WaitForChildResult { - pub status: ExitStatus, - pub stdout: Vec, - pub stderr: Vec, -} - -async fn read_with_stdio_kind( - read_from: Option, - kind: ProcessSpawnOptionsStdioKind, -) -> LuaResult> -where - R: AsyncRead + Unpin, -{ - Ok(match kind { - ProcessSpawnOptionsStdioKind::None => Vec::new(), - ProcessSpawnOptionsStdioKind::Forward => Vec::new(), - ProcessSpawnOptionsStdioKind::Default => { - let mut read_from = - read_from.expect("read_from must be Some when stdio kind is Default"); - - let mut buffer = Vec::new(); - - read_from.read_to_end(&mut buffer).await.into_lua_err()?; - - buffer - } - ProcessSpawnOptionsStdioKind::Inherit => { - let mut read_from = - read_from.expect("read_from must be Some when stdio kind is Inherit"); - - let mut stdout = io::stdout(); - let mut tee = AsyncTeeWriter::new(&mut stdout); - - io::copy(&mut read_from, &mut tee).await.into_lua_err()?; - - tee.into_vec() - } - }) -} - -pub(super) async fn wait_for_child( - mut child: Child, - stdout_kind: ProcessSpawnOptionsStdioKind, - stderr_kind: ProcessSpawnOptionsStdioKind, -) -> LuaResult { - let stdout_opt = child.stdout.take(); - let stderr_opt = child.stderr.take(); - - let stdout_task = task::spawn(read_with_stdio_kind(stdout_opt, stdout_kind)); - let stderr_task = task::spawn(read_with_stdio_kind(stderr_opt, stderr_kind)); - - let status = child.wait().await.expect("Child process failed to start"); - - let stdout_buffer = stdout_task.await.into_lua_err()??; - let stderr_buffer = stderr_task.await.into_lua_err()??; - - Ok(WaitForChildResult { - status, - stdout: stdout_buffer, - stderr: stderr_buffer, - }) -} diff --git a/crates/lune/src/lune/builtins/regex/captures.rs b/crates/lune/src/lune/builtins/regex/captures.rs deleted file mode 100644 index 5dbea74..0000000 --- a/crates/lune/src/lune/builtins/regex/captures.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::sync::Arc; - -use mlua::prelude::*; -use regex::{Captures, Regex}; -use self_cell::self_cell; - -use super::matches::LuaMatch; - -type OptionalCaptures<'a> = Option>; - -self_cell! { - struct LuaCapturesInner { - owner: Arc, - #[covariant] - dependent: OptionalCaptures, - } -} - -/** - A wrapper over the `regex::Captures` struct that can be used from Lua. -*/ -pub struct LuaCaptures { - inner: LuaCapturesInner, -} - -impl LuaCaptures { - /** - Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text. - - Returns `Some(_)` if captures were found, `None` if no captures were found. - */ - pub fn new(pattern: &Regex, text: String) -> Option { - let inner = - LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())); - if inner.borrow_dependent().is_some() { - Some(Self { inner }) - } else { - None - } - } - - fn captures(&self) -> &Captures { - self.inner - .borrow_dependent() - .as_ref() - .expect("None captures should not be used") - } - - fn num_captures(&self) -> usize { - // NOTE: Here we exclude the match for the entire regex - // pattern, only counting the named and numbered captures - self.captures().len() - 1 - } - - fn text(&self) -> Arc { - Arc::clone(self.inner.borrow_owner()) - } -} - -impl LuaUserData for LuaCaptures { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("get", |_, this, index: usize| { - Ok(this - .captures() - .get(index) - .map(|m| LuaMatch::new(this.text(), m))) - }); - - methods.add_method("group", |_, this, group: String| { - Ok(this - .captures() - .name(&group) - .map(|m| LuaMatch::new(this.text(), m))) - }); - - methods.add_method("format", |_, this, format: String| { - let mut new = String::new(); - this.captures().expand(&format, &mut new); - Ok(new) - }); - - methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures())); - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { - Ok(format!("RegexCaptures({})", this.num_captures())) - }); - } - - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures"); - } -} diff --git a/crates/lune/src/lune/builtins/regex/matches.rs b/crates/lune/src/lune/builtins/regex/matches.rs deleted file mode 100644 index bc109f8..0000000 --- a/crates/lune/src/lune/builtins/regex/matches.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{ops::Range, sync::Arc}; - -use mlua::prelude::*; -use regex::Match; - -/** - A wrapper over the `regex::Match` struct that can be used from Lua. -*/ -pub struct LuaMatch { - text: Arc, - start: usize, - end: usize, -} - -impl LuaMatch { - /** - Create a new `LuaMatch` instance from a `String` text and a `regex::Match`. - */ - pub fn new(text: Arc, matched: Match) -> Self { - Self { - text, - start: matched.start(), - end: matched.end(), - } - } - - fn range(&self) -> Range { - self.start..self.end - } - - fn slice(&self) -> &str { - &self.text[self.range()] - } -} - -impl LuaUserData for LuaMatch { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - // NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive - fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1))); - fields.add_field_method_get("finish", |_, this| Ok(this.end)); - fields.add_field_method_get("len", |_, this| Ok(this.range().len())); - fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string())); - - fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch"); - } - - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { - Ok(format!("RegexMatch({})", this.slice())) - }); - } -} diff --git a/crates/lune/src/lune/builtins/regex/mod.rs b/crates/lune/src/lune/builtins/regex/mod.rs deleted file mode 100644 index bb674c2..0000000 --- a/crates/lune/src/lune/builtins/regex/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![allow(clippy::module_inception)] - -use mlua::prelude::*; - -use crate::lune::util::TableBuilder; - -mod captures; -mod matches; -mod regex; - -use self::regex::LuaRegex; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_function("new", new_regex)? - .build_readonly() -} - -fn new_regex(_: &Lua, pattern: String) -> LuaResult { - LuaRegex::new(pattern) -} diff --git a/crates/lune/src/lune/builtins/regex/regex.rs b/crates/lune/src/lune/builtins/regex/regex.rs deleted file mode 100644 index 3325e5d..0000000 --- a/crates/lune/src/lune/builtins/regex/regex.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::sync::Arc; - -use mlua::prelude::*; -use regex::Regex; - -use super::{captures::LuaCaptures, matches::LuaMatch}; - -/** - A wrapper over the `regex::Regex` struct that can be used from Lua. -*/ -#[derive(Debug, Clone)] -pub struct LuaRegex { - inner: Regex, -} - -impl LuaRegex { - /** - Create a new `LuaRegex` instance from a `String` pattern. - */ - pub fn new(pattern: String) -> LuaResult { - Regex::new(&pattern) - .map(|inner| Self { inner }) - .map_err(LuaError::external) - } -} - -impl LuaUserData for LuaRegex { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("isMatch", |_, this, text: String| { - Ok(this.inner.is_match(&text)) - }); - - methods.add_method("find", |_, this, text: String| { - let arc = Arc::new(text); - Ok(this - .inner - .find(&arc) - .map(|m| LuaMatch::new(Arc::clone(&arc), m))) - }); - - methods.add_method("captures", |_, this, text: String| { - Ok(LuaCaptures::new(&this.inner, text)) - }); - - methods.add_method("split", |_, this, text: String| { - Ok(this - .inner - .split(&text) - .map(|s| s.to_string()) - .collect::>()) - }); - - // TODO: Determine whether it's desirable and / or feasible to support - // using a function or table for `replace` like in the lua string library - methods.add_method( - "replace", - |_, this, (haystack, replacer): (String, String)| { - Ok(this.inner.replace(&haystack, replacer).to_string()) - }, - ); - methods.add_method( - "replaceAll", - |_, this, (haystack, replacer): (String, String)| { - Ok(this.inner.replace_all(&haystack, replacer).to_string()) - }, - ); - - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { - Ok(format!("Regex({})", this.inner.as_str())) - }); - } - - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_meta_field(LuaMetaMethod::Type, "Regex"); - } -} diff --git a/crates/lune/src/lune/builtins/roblox/mod.rs b/crates/lune/src/lune/builtins/roblox/mod.rs deleted file mode 100644 index 308c148..0000000 --- a/crates/lune/src/lune/builtins/roblox/mod.rs +++ /dev/null @@ -1,143 +0,0 @@ -use mlua::prelude::*; -use mlua_luau_scheduler::LuaSpawnExt; -use once_cell::sync::OnceCell; - -use crate::{ - lune::util::TableBuilder, - roblox::{ - self, - document::{Document, DocumentError, DocumentFormat, DocumentKind}, - instance::{registry::InstanceRegistry, Instance}, - reflection::Database as ReflectionDatabase, - }, -}; - -static REFLECTION_DATABASE: OnceCell = OnceCell::new(); - -pub fn create(lua: &Lua) -> LuaResult { - let mut roblox_constants = Vec::new(); - - let roblox_module = roblox::module(lua)?; - for pair in roblox_module.pairs::() { - roblox_constants.push(pair?); - } - - TableBuilder::new(lua)? - .with_values(roblox_constants)? - .with_async_function("deserializePlace", deserialize_place)? - .with_async_function("deserializeModel", deserialize_model)? - .with_async_function("serializePlace", serialize_place)? - .with_async_function("serializeModel", serialize_model)? - .with_function("getAuthCookie", get_auth_cookie)? - .with_function("getReflectionDatabase", get_reflection_database)? - .with_function("implementProperty", implement_property)? - .with_function("implementMethod", implement_method)? - .build_readonly() -} - -async fn deserialize_place<'lua>( - lua: &'lua Lua, - contents: LuaString<'lua>, -) -> LuaResult> { - let bytes = contents.as_bytes().to_vec(); - let fut = lua.spawn_blocking(move || { - let doc = Document::from_bytes(bytes, DocumentKind::Place)?; - let data_model = doc.into_data_model_instance()?; - Ok::<_, DocumentError>(data_model) - }); - fut.await.into_lua_err()?.into_lua(lua) -} - -async fn deserialize_model<'lua>( - lua: &'lua Lua, - contents: LuaString<'lua>, -) -> LuaResult> { - let bytes = contents.as_bytes().to_vec(); - let fut = lua.spawn_blocking(move || { - let doc = Document::from_bytes(bytes, DocumentKind::Model)?; - let instance_array = doc.into_instance_array()?; - Ok::<_, DocumentError>(instance_array) - }); - fut.await.into_lua_err()?.into_lua(lua) -} - -async fn serialize_place<'lua>( - lua: &'lua Lua, - (data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option), -) -> LuaResult> { - let data_model = (*data_model).clone(); - let fut = lua.spawn_blocking(move || { - let doc = Document::from_data_model_instance(data_model)?; - let bytes = doc.to_bytes_with_format(match as_xml { - Some(true) => DocumentFormat::Xml, - _ => DocumentFormat::Binary, - })?; - Ok::<_, DocumentError>(bytes) - }); - let bytes = fut.await.into_lua_err()?; - lua.create_string(bytes) -} - -async fn serialize_model<'lua>( - lua: &'lua Lua, - (instances, as_xml): (Vec>, Option), -) -> LuaResult> { - let instances = instances.iter().map(|i| (*i).clone()).collect(); - let fut = lua.spawn_blocking(move || { - let doc = Document::from_instance_array(instances)?; - let bytes = doc.to_bytes_with_format(match as_xml { - Some(true) => DocumentFormat::Xml, - _ => DocumentFormat::Binary, - })?; - Ok::<_, DocumentError>(bytes) - }); - let bytes = fut.await.into_lua_err()?; - lua.create_string(bytes) -} - -fn get_auth_cookie(_: &Lua, raw: Option) -> LuaResult> { - if matches!(raw, Some(true)) { - Ok(rbx_cookie::get_value()) - } else { - Ok(rbx_cookie::get()) - } -} - -fn get_reflection_database(_: &Lua, _: ()) -> LuaResult { - Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new)) -} - -fn implement_property( - lua: &Lua, - (class_name, property_name, property_getter, property_setter): ( - String, - String, - LuaFunction, - Option, - ), -) -> LuaResult<()> { - let property_setter = match property_setter { - Some(setter) => setter, - None => { - let property_name = property_name.clone(); - lua.create_function(move |_, _: LuaMultiValue| { - Err::<(), _>(LuaError::runtime(format!( - "Property '{property_name}' is read-only" - ))) - })? - } - }; - InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter) - .into_lua_err()?; - InstanceRegistry::insert_property_setter(lua, &class_name, &property_name, property_setter) - .into_lua_err()?; - Ok(()) -} - -fn implement_method( - lua: &Lua, - (class_name, method_name, method): (String, String, LuaFunction), -) -> LuaResult<()> { - InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?; - Ok(()) -} diff --git a/crates/lune/src/lune/builtins/serde/compress_decompress.rs b/crates/lune/src/lune/builtins/serde/compress_decompress.rs deleted file mode 100644 index dac6ceb..0000000 --- a/crates/lune/src/lune/builtins/serde/compress_decompress.rs +++ /dev/null @@ -1,157 +0,0 @@ -use mlua::prelude::*; - -use lz4_flex::{compress_prepend_size, decompress_size_prepended}; -use tokio::io::{copy, BufReader}; - -use async_compression::{ - tokio::bufread::{ - BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder, - }, - Level::Best as CompressionQuality, -}; - -#[derive(Debug, Clone, Copy)] -pub enum CompressDecompressFormat { - Brotli, - GZip, - LZ4, - ZLib, -} - -#[allow(dead_code)] -impl CompressDecompressFormat { - pub fn detect_from_bytes(bytes: impl AsRef<[u8]>) -> Option { - match bytes.as_ref() { - // https://github.com/PSeitz/lz4_flex/blob/main/src/frame/header.rs#L28 - b if b.len() >= 4 - && matches!( - u32::from_le_bytes(b[0..4].try_into().unwrap()), - 0x184D2204 | 0x184C2102 - ) => - { - Some(Self::LZ4) - } - // https://github.com/dropbox/rust-brotli/blob/master/src/enc/brotli_bit_stream.rs#L2805 - b if b.len() >= 4 - && matches!( - b[0..3], - [0xE1, 0x97, 0x81] | [0xE1, 0x97, 0x82] | [0xE1, 0x97, 0x80] - ) => - { - Some(Self::Brotli) - } - // https://github.com/rust-lang/flate2-rs/blob/main/src/gz/mod.rs#L135 - b if b.len() >= 3 && matches!(b[0..3], [0x1F, 0x8B, 0x08]) => Some(Self::GZip), - // https://stackoverflow.com/a/43170354 - b if b.len() >= 2 - && matches!( - b[0..2], - [0x78, 0x01] | [0x78, 0x5E] | [0x78, 0x9C] | [0x78, 0xDA] - ) => - { - Some(Self::ZLib) - } - _ => None, - } - } - - pub fn detect_from_header_str(header: impl AsRef) -> Option { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives - match header.as_ref().to_ascii_lowercase().trim() { - "br" | "brotli" => Some(Self::Brotli), - "deflate" => Some(Self::ZLib), - "gz" | "gzip" => Some(Self::GZip), - _ => None, - } - } -} - -impl<'lua> FromLua<'lua> for CompressDecompressFormat { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - if let LuaValue::String(s) = &value { - match s.to_string_lossy().to_ascii_lowercase().trim() { - "brotli" => Ok(Self::Brotli), - "gzip" => Ok(Self::GZip), - "lz4" => Ok(Self::LZ4), - "zlib" => Ok(Self::ZLib), - kind => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "CompressDecompressFormat", - message: Some(format!( - "Invalid format '{kind}', valid formats are: brotli, gzip, lz4, zlib" - )), - }), - } - } else { - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "CompressDecompressFormat", - message: None, - }) - } - } -} - -pub async fn compress<'lua>( - format: CompressDecompressFormat, - source: impl AsRef<[u8]>, -) -> LuaResult> { - if let CompressDecompressFormat::LZ4 = format { - let source = source.as_ref().to_vec(); - return Ok(blocking::unblock(move || compress_prepend_size(&source)).await); - } - - let mut bytes = Vec::new(); - let reader = BufReader::new(source.as_ref()); - - match format { - CompressDecompressFormat::Brotli => { - let mut encoder = BrotliEncoder::with_quality(reader, CompressionQuality); - copy(&mut encoder, &mut bytes).await?; - } - CompressDecompressFormat::GZip => { - let mut encoder = GzipEncoder::with_quality(reader, CompressionQuality); - copy(&mut encoder, &mut bytes).await?; - } - CompressDecompressFormat::ZLib => { - let mut encoder = ZlibEncoder::with_quality(reader, CompressionQuality); - copy(&mut encoder, &mut bytes).await?; - } - CompressDecompressFormat::LZ4 => unreachable!(), - } - - Ok(bytes) -} - -pub async fn decompress<'lua>( - format: CompressDecompressFormat, - source: impl AsRef<[u8]>, -) -> LuaResult> { - if let CompressDecompressFormat::LZ4 = format { - let source = source.as_ref().to_vec(); - return blocking::unblock(move || decompress_size_prepended(&source)) - .await - .into_lua_err(); - } - - let mut bytes = Vec::new(); - let reader = BufReader::new(source.as_ref()); - - match format { - CompressDecompressFormat::Brotli => { - let mut decoder = BrotliDecoder::new(reader); - copy(&mut decoder, &mut bytes).await?; - } - CompressDecompressFormat::GZip => { - let mut decoder = GzipDecoder::new(reader); - copy(&mut decoder, &mut bytes).await?; - } - CompressDecompressFormat::ZLib => { - let mut decoder = ZlibDecoder::new(reader); - copy(&mut decoder, &mut bytes).await?; - } - CompressDecompressFormat::LZ4 => unreachable!(), - } - - Ok(bytes) -} diff --git a/crates/lune/src/lune/builtins/serde/encode_decode.rs b/crates/lune/src/lune/builtins/serde/encode_decode.rs deleted file mode 100644 index 8457a25..0000000 --- a/crates/lune/src/lune/builtins/serde/encode_decode.rs +++ /dev/null @@ -1,131 +0,0 @@ -use bstr::{BString, ByteSlice}; -use mlua::prelude::*; - -use serde_json::Value as JsonValue; -use serde_yaml::Value as YamlValue; -use toml::Value as TomlValue; - -const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new() - .set_array_metatable(false) - .serialize_none_to_null(false) - .serialize_unit_to_null(false); - -const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new() - .sort_keys(true) - .deny_recursive_tables(false) - .deny_unsupported_types(true); - -#[derive(Debug, Clone, Copy)] -pub enum EncodeDecodeFormat { - Json, - Yaml, - Toml, -} - -impl<'lua> FromLua<'lua> for EncodeDecodeFormat { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - if let LuaValue::String(s) = &value { - match s.to_string_lossy().to_ascii_lowercase().trim() { - "json" => Ok(Self::Json), - "yaml" => Ok(Self::Yaml), - "toml" => Ok(Self::Toml), - kind => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "EncodeDecodeFormat", - message: Some(format!( - "Invalid format '{kind}', valid formats are: json, yaml, toml" - )), - }), - } - } else { - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "EncodeDecodeFormat", - message: None, - }) - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct EncodeDecodeConfig { - pub format: EncodeDecodeFormat, - pub pretty: bool, -} - -impl EncodeDecodeConfig { - pub fn serialize_to_string<'lua>( - self, - lua: &'lua Lua, - value: LuaValue<'lua>, - ) -> LuaResult> { - let bytes = match self.format { - EncodeDecodeFormat::Json => { - let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; - if self.pretty { - serde_json::to_vec_pretty(&serialized).into_lua_err()? - } else { - serde_json::to_vec(&serialized).into_lua_err()? - } - } - EncodeDecodeFormat::Yaml => { - let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; - let mut writer = Vec::with_capacity(128); - serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?; - writer - } - EncodeDecodeFormat::Toml => { - let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?; - let s = if self.pretty { - toml::to_string_pretty(&serialized).into_lua_err()? - } else { - toml::to_string(&serialized).into_lua_err()? - }; - s.as_bytes().to_vec() - } - }; - lua.create_string(bytes) - } - - pub fn deserialize_from_string(self, lua: &Lua, string: BString) -> LuaResult { - let bytes = string.as_bytes(); - match self.format { - EncodeDecodeFormat::Json => { - let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?; - lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) - } - EncodeDecodeFormat::Yaml => { - let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?; - lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) - } - EncodeDecodeFormat::Toml => { - if let Ok(s) = string.to_str() { - let value: TomlValue = toml::from_str(s).into_lua_err()?; - lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS) - } else { - Err(LuaError::RuntimeError( - "TOML must be valid utf-8".to_string(), - )) - } - } - } - } -} - -impl From for EncodeDecodeConfig { - fn from(format: EncodeDecodeFormat) -> Self { - Self { - format, - pretty: false, - } - } -} - -impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig { - fn from(value: (EncodeDecodeFormat, bool)) -> Self { - Self { - format: value.0, - pretty: value.1, - } - } -} diff --git a/crates/lune/src/lune/builtins/serde/mod.rs b/crates/lune/src/lune/builtins/serde/mod.rs deleted file mode 100644 index de351a3..0000000 --- a/crates/lune/src/lune/builtins/serde/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -use bstr::BString; -use mlua::prelude::*; - -pub(super) mod compress_decompress; -pub(super) mod encode_decode; - -use compress_decompress::{compress, decompress, CompressDecompressFormat}; -use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; - -use crate::lune::util::TableBuilder; - -pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)? - .with_function("encode", serde_encode)? - .with_function("decode", serde_decode)? - .with_async_function("compress", serde_compress)? - .with_async_function("decompress", serde_decompress)? - .build_readonly() -} - -fn serde_encode<'lua>( - lua: &'lua Lua, - (format, val, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option), -) -> LuaResult> { - let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default())); - config.serialize_to_string(lua, val) -} - -fn serde_decode(lua: &Lua, (format, str): (EncodeDecodeFormat, BString)) -> LuaResult { - let config = EncodeDecodeConfig::from(format); - config.deserialize_from_string(lua, str) -} - -async fn serde_compress( - lua: &Lua, - (format, str): (CompressDecompressFormat, BString), -) -> LuaResult { - let bytes = compress(format, str).await?; - lua.create_string(bytes) -} - -async fn serde_decompress( - lua: &Lua, - (format, str): (CompressDecompressFormat, BString), -) -> LuaResult { - let bytes = decompress(format, str).await?; - lua.create_string(bytes) -} diff --git a/crates/lune/src/lune/builtins/stdio/mod.rs b/crates/lune/src/lune/builtins/stdio/mod.rs deleted file mode 100644 index f851bc7..0000000 --- a/crates/lune/src/lune/builtins/stdio/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -use mlua::prelude::*; - -use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; -use mlua_luau_scheduler::LuaSpawnExt; -use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; - -use crate::lune::util::{ - formatting::{ - format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str, - }, - TableBuilder, -}; - -mod prompt; -use prompt::{PromptKind, PromptOptions, PromptResult}; - -pub fn create(lua: &Lua) -> LuaResult> { - TableBuilder::new(lua)? - .with_function("color", stdio_color)? - .with_function("style", stdio_style)? - .with_function("format", stdio_format)? - .with_async_function("write", stdio_write)? - .with_async_function("ewrite", stdio_ewrite)? - .with_async_function("readToEnd", stdio_read_to_end)? - .with_async_function("prompt", stdio_prompt)? - .build_readonly() -} - -fn stdio_color(_: &Lua, color: String) -> LuaResult { - let ansi_string = format_style(style_from_color_str(&color)?); - Ok(ansi_string) -} - -fn stdio_style(_: &Lua, color: String) -> LuaResult { - let ansi_string = format_style(style_from_style_str(&color)?); - Ok(ansi_string) -} - -fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult { - pretty_format_multi_value(&args) -} - -async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { - let mut stdout = io::stdout(); - stdout.write_all(s.as_bytes()).await?; - stdout.flush().await?; - Ok(()) -} - -async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { - let mut stderr = io::stderr(); - stderr.write_all(s.as_bytes()).await?; - stderr.flush().await?; - Ok(()) -} - -/* - FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader. - - This is a bit tricky since we would want to be able to use **both** readLine and readToEnd - in the same script, doing something like readLine, readLine, readToEnd from lua, and - having that capture the first two lines and then read the rest of the input. -*/ - -async fn stdio_read_to_end(lua: &Lua, _: ()) -> LuaResult { - let mut input = Vec::new(); - let mut stdin = io::stdin(); - stdin.read_to_end(&mut input).await?; - lua.create_string(&input) -} - -async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult { - lua.spawn_blocking(move || prompt(options)) - .await - .into_lua_err() -} - -fn prompt(options: PromptOptions) -> LuaResult { - let theme = ColorfulTheme::default(); - match options.kind { - PromptKind::Text => { - let input: String = Input::with_theme(&theme) - .allow_empty(true) - .with_prompt(options.text.unwrap_or_default()) - .with_initial_text(options.default_string.unwrap_or_default()) - .interact_text() - .into_lua_err()?; - Ok(PromptResult::String(input)) - } - PromptKind::Confirm => { - let mut prompt = Confirm::with_theme(&theme); - if let Some(b) = options.default_bool { - prompt = prompt.default(b); - }; - let result = prompt - .with_prompt(&options.text.expect("Missing text in prompt options")) - .interact() - .into_lua_err()?; - Ok(PromptResult::Boolean(result)) - } - PromptKind::Select => { - let chosen = Select::with_theme(&theme) - .with_prompt(&options.text.unwrap_or_default()) - .items(&options.options.expect("Missing options in prompt options")) - .interact_opt() - .into_lua_err()?; - Ok(match chosen { - Some(idx) => PromptResult::Index(idx + 1), - None => PromptResult::None, - }) - } - PromptKind::MultiSelect => { - let chosen = MultiSelect::with_theme(&theme) - .with_prompt(&options.text.unwrap_or_default()) - .items(&options.options.expect("Missing options in prompt options")) - .interact_opt() - .into_lua_err()?; - Ok(match chosen { - None => PromptResult::None, - Some(indices) => { - PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect()) - } - }) - } - } -} diff --git a/crates/lune/src/lune/builtins/stdio/prompt.rs b/crates/lune/src/lune/builtins/stdio/prompt.rs deleted file mode 100644 index 9cdd899..0000000 --- a/crates/lune/src/lune/builtins/stdio/prompt.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::fmt; - -use mlua::prelude::*; - -#[derive(Debug, Clone, Copy)] -pub enum PromptKind { - Text, - Confirm, - Select, - MultiSelect, -} - -impl PromptKind { - fn get_all() -> Vec { - vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect] - } -} - -impl Default for PromptKind { - fn default() -> Self { - Self::Text - } -} - -impl fmt::Display for PromptKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Text => "Text", - Self::Confirm => "Confirm", - Self::Select => "Select", - Self::MultiSelect => "MultiSelect", - } - ) - } -} - -impl<'lua> FromLua<'lua> for PromptKind { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - if let LuaValue::Nil = value { - Ok(Self::default()) - } else if let LuaValue::String(s) = value { - let s = s.to_str()?; - /* - If the user only typed the prompt kind slightly wrong, meaning - it has some kind of space in it, a weird character, or an uppercase - character, we should try to be permissive as possible and still work - - Not everyone is using an IDE with proper Luau type definitions - installed, and Luau is still a permissive scripting language - even though it has a strict (but optional) type system - */ - let s = s - .chars() - .filter_map(|c| { - if c.is_ascii_alphabetic() { - Some(c.to_ascii_lowercase()) - } else { - None - } - }) - .collect::(); - // If the prompt kind is still invalid we will - // show the user a descriptive error message - match s.as_ref() { - "text" => Ok(Self::Text), - "confirm" => Ok(Self::Confirm), - "select" => Ok(Self::Select), - "multiselect" => Ok(Self::MultiSelect), - s => Err(LuaError::FromLuaConversionError { - from: "string", - to: "PromptKind", - message: Some(format!( - "Invalid prompt kind '{s}', valid kinds are:\n{}", - PromptKind::get_all() - .iter() - .map(ToString::to_string) - .collect::>() - .join(", ") - )), - }), - } - } else { - Err(LuaError::FromLuaConversionError { - from: "nil", - to: "PromptKind", - message: None, - }) - } - } -} - -pub struct PromptOptions { - pub kind: PromptKind, - pub text: Option, - pub default_string: Option, - pub default_bool: Option, - pub options: Option>, -} - -impl<'lua> FromLuaMulti<'lua> for PromptOptions { - fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult { - // Argument #1 - prompt kind (optional) - let kind = values - .pop_front() - .map(|value| PromptKind::from_lua(value, lua)) - .transpose()? - .unwrap_or_default(); - // Argument #2 - prompt text (optional) - let text = values - .pop_front() - .map(|text| String::from_lua(text, lua)) - .transpose()?; - // Argument #3 - default value / options, - // this is different per each prompt kind - let (default_bool, default_string, options) = match values.pop_front() { - None => (None, None, None), - Some(options) => match options { - LuaValue::Nil => (None, None, None), - LuaValue::Boolean(b) => (Some(b), None, None), - LuaValue::String(s) => ( - None, - Some(String::from_lua(LuaValue::String(s), lua)?), - None, - ), - LuaValue::Table(t) => ( - None, - None, - Some(Vec::::from_lua(LuaValue::Table(t), lua)?), - ), - value => { - return Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "PromptOptions", - message: Some("Argument #3 must be a boolean, table, or nil".to_string()), - }) - } - }, - }; - /* - Make sure we got the required values for the specific prompt kind: - - - "Confirm" requires a message to be present so the user knows what they are confirming - - "Select" and "MultiSelect" both require a table of options to choose from - */ - if matches!(kind, PromptKind::Confirm) && text.is_none() { - return Err(LuaError::FromLuaConversionError { - from: "nil", - to: "PromptOptions", - message: Some("Argument #2 missing or nil".to_string()), - }); - } - if matches!(kind, PromptKind::Select | PromptKind::MultiSelect) && options.is_none() { - return Err(LuaError::FromLuaConversionError { - from: "nil", - to: "PromptOptions", - message: Some("Argument #3 missing or nil".to_string()), - }); - } - // All good, return the prompt options - Ok(Self { - kind, - text, - default_bool, - default_string, - options, - }) - } -} - -#[derive(Debug, Clone)] -pub enum PromptResult { - String(String), - Boolean(bool), - Index(usize), - Indices(Vec), - None, -} - -impl<'lua> IntoLua<'lua> for PromptResult { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - Ok(match self { - Self::String(s) => LuaValue::String(lua.create_string(&s)?), - Self::Boolean(b) => LuaValue::Boolean(b), - Self::Index(i) => LuaValue::Number(i as f64), - Self::Indices(v) => v.into_lua(lua)?, - Self::None => LuaValue::Nil, - }) - } -} diff --git a/crates/lune/src/lune/builtins/task/mod.rs b/crates/lune/src/lune/builtins/task/mod.rs deleted file mode 100644 index 94a4da0..0000000 --- a/crates/lune/src/lune/builtins/task/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::time::Duration; - -use mlua::prelude::*; - -use mlua_luau_scheduler::Functions; -use tokio::time::{self, Instant}; - -use crate::lune::util::TableBuilder; - -const DELAY_IMPL_LUA: &str = r#" -return defer(function(...) - wait(select(1, ...)) - spawn(select(2, ...)) -end, ...) -"#; - -pub fn create(lua: &Lua) -> LuaResult> { - let fns = Functions::new(lua)?; - - // Create wait & delay functions - let task_wait = lua.create_async_function(wait)?; - let task_delay_env = TableBuilder::new(lua)? - .with_value("select", lua.globals().get::<_, LuaFunction>("select")?)? - .with_value("spawn", fns.spawn.clone())? - .with_value("defer", fns.defer.clone())? - .with_value("wait", task_wait.clone())? - .build_readonly()?; - let task_delay = lua - .load(DELAY_IMPL_LUA) - .set_name("task.delay") - .set_environment(task_delay_env) - .into_function()?; - - // Overwrite resume & wrap functions on the coroutine global - // with ones that are compatible with our scheduler - let co = lua.globals().get::<_, LuaTable>("coroutine")?; - co.set("resume", fns.resume.clone())?; - co.set("wrap", fns.wrap.clone())?; - - TableBuilder::new(lua)? - .with_value("cancel", fns.cancel)? - .with_value("defer", fns.defer)? - .with_value("delay", task_delay)? - .with_value("spawn", fns.spawn)? - .with_value("wait", task_wait)? - .build_readonly() -} - -async fn wait(_: &Lua, secs: Option) -> LuaResult { - let duration = Duration::from_secs_f64(secs.unwrap_or_default()); - - let before = Instant::now(); - time::sleep(duration).await; - let after = Instant::now(); - - Ok((after - before).as_secs_f64()) -} diff --git a/crates/lune/src/lune/globals/g_table.rs b/crates/lune/src/lune/globals/g_table.rs deleted file mode 100644 index 8c007c8..0000000 --- a/crates/lune/src/lune/globals/g_table.rs +++ /dev/null @@ -1,5 +0,0 @@ -use mlua::prelude::*; - -pub fn create(lua: &Lua) -> LuaResult> { - lua.create_table() -} diff --git a/crates/lune/src/lune/globals/mod.rs b/crates/lune/src/lune/globals/mod.rs deleted file mode 100644 index 1d8700c..0000000 --- a/crates/lune/src/lune/globals/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -use mlua::prelude::*; - -use super::util::TableBuilder; - -mod g_table; -mod print; -mod require; -mod version; -mod warn; - -pub fn inject_all(lua: &Lua) -> LuaResult<()> { - let all = TableBuilder::new(lua)? - .with_value("_G", g_table::create(lua)?)? - .with_value("_VERSION", version::create(lua)?)? - .with_value("print", print::create(lua)?)? - .with_value("require", require::create(lua)?)? - .with_value("warn", warn::create(lua)?)? - .build_readonly()?; - - for res in all.pairs() { - let (key, value): (LuaValue, LuaValue) = res.unwrap(); - lua.globals().set(key, value)?; - } - - Ok(()) -} diff --git a/crates/lune/src/lune/globals/print.rs b/crates/lune/src/lune/globals/print.rs deleted file mode 100644 index 944d420..0000000 --- a/crates/lune/src/lune/globals/print.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::io::Write as _; - -use mlua::prelude::*; - -use crate::lune::util::formatting::pretty_format_multi_value; - -pub fn create(lua: &Lua) -> LuaResult> { - lua.create_function(|_, args: LuaMultiValue| { - let formatted = format!("{}\n", pretty_format_multi_value(&args)?); - let mut stdout = std::io::stdout(); - stdout.write_all(formatted.as_bytes())?; - stdout.flush()?; - Ok(()) - }) -} diff --git a/crates/lune/src/lune/globals/require/alias.rs b/crates/lune/src/lune/globals/require/alias.rs deleted file mode 100644 index 09a36b0..0000000 --- a/crates/lune/src/lune/globals/require/alias.rs +++ /dev/null @@ -1,75 +0,0 @@ -use console::style; -use mlua::prelude::*; - -use crate::lune::util::{ - luaurc::LuauRc, - paths::{make_absolute_and_clean, CWD}, -}; - -use super::context::*; - -pub(super) async fn require<'lua, 'ctx>( - lua: &'lua Lua, - ctx: &'ctx RequireContext, - source: &str, - alias: &str, - path: &str, -) -> LuaResult> -where - 'lua: 'ctx, -{ - let alias = alias.to_ascii_lowercase(); - - let parent = make_absolute_and_clean(source) - .parent() - .expect("how did a root path end up here..") - .to_path_buf(); - - // Try to gather the first luaurc and / or error we - // encounter to display better error messages to users - let mut first_luaurc = None; - let mut first_error = None; - let predicate = |rc: &LuauRc| { - if first_luaurc.is_none() { - first_luaurc.replace(rc.clone()); - } - if let Err(e) = rc.validate() { - if first_error.is_none() { - first_error.replace(e); - } - false - } else { - rc.find_alias(&alias).is_some() - } - }; - - // Try to find a luaurc that contains the alias we're searching for - let luaurc = LuauRc::read_recursive(parent, predicate) - .await - .ok_or_else(|| { - if let Some(error) = first_error { - LuaError::runtime(format!("error while parsing .luaurc file: {error}")) - } else if let Some(luaurc) = first_luaurc { - LuaError::runtime(format!( - "failed to find alias '{alias}' - known aliases:\n{}", - luaurc - .aliases() - .iter() - .map(|(name, path)| format!(" {name} {} {path}", style(">").dim())) - .collect::>() - .join("\n") - )) - } else { - LuaError::runtime(format!("failed to find alias '{alias}' (no .luaurc)")) - } - })?; - - // We now have our aliased path, our path require function just needs it - // in a slightly different format with both absolute + relative to cwd - let abs_path = luaurc.find_alias(&alias).unwrap().join(path); - let rel_path = pathdiff::diff_paths(&abs_path, CWD.as_path()).ok_or_else(|| { - LuaError::runtime(format!("failed to find relative path for alias '{alias}'")) - })?; - - super::path::require_abs_rel(lua, ctx, abs_path, rel_path).await -} diff --git a/crates/lune/src/lune/globals/require/builtin.rs b/crates/lune/src/lune/globals/require/builtin.rs deleted file mode 100644 index 42302cf..0000000 --- a/crates/lune/src/lune/globals/require/builtin.rs +++ /dev/null @@ -1,14 +0,0 @@ -use mlua::prelude::*; - -use super::context::*; - -pub(super) async fn require<'lua, 'ctx>( - lua: &'lua Lua, - ctx: &'ctx RequireContext, - name: &str, -) -> LuaResult> -where - 'lua: 'ctx, -{ - ctx.load_builtin(lua, name) -} diff --git a/crates/lune/src/lune/globals/require/context.rs b/crates/lune/src/lune/globals/require/context.rs deleted file mode 100644 index 7f018c6..0000000 --- a/crates/lune/src/lune/globals/require/context.rs +++ /dev/null @@ -1,291 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::Arc, -}; - -use mlua::prelude::*; -use mlua_luau_scheduler::LuaSchedulerExt; -use tokio::{ - fs, - sync::{ - broadcast::{self, Sender}, - Mutex as AsyncMutex, - }, -}; - -use crate::lune::{builtins::LuneBuiltin, util::paths::CWD}; - -/** - Context containing cached results for all `require` operations. - - The cache uses absolute paths, so any given relative - path will first be transformed into an absolute path. -*/ -#[derive(Debug, Clone)] -pub(super) struct RequireContext { - cache_builtins: Arc>>>, - cache_results: Arc>>>, - cache_pending: Arc>>>, -} - -impl RequireContext { - /** - Creates a new require context for the given [`Lua`] struct. - - Note that this require context is global and only one require - context should be created per [`Lua`] struct, creating more - than one context may lead to undefined require-behavior. - */ - pub fn new() -> Self { - Self { - cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())), - cache_results: Arc::new(AsyncMutex::new(HashMap::new())), - cache_pending: Arc::new(AsyncMutex::new(HashMap::new())), - } - } - - /** - Resolves the given `source` and `path` into require paths - to use, based on the current require context settings. - - This will resolve path segments such as `./`, `../`, ..., and - if the resolved path is not an absolute path, will create an - absolute path by prepending the current working directory. - */ - pub fn resolve_paths( - &self, - source: impl AsRef, - path: impl AsRef, - ) -> LuaResult<(PathBuf, PathBuf)> { - let path = PathBuf::from(source.as_ref()) - .parent() - .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))? - .join(path.as_ref()); - - let rel_path = path_clean::clean(path); - let abs_path = if rel_path.is_absolute() { - rel_path.to_path_buf() - } else { - CWD.join(&rel_path) - }; - - Ok((abs_path, rel_path)) - } - - /** - Checks if the given path has a cached require result. - */ - pub fn is_cached(&self, abs_path: impl AsRef) -> LuaResult { - let is_cached = self - .cache_results - .try_lock() - .expect("RequireContext may not be used from multiple threads") - .contains_key(abs_path.as_ref()); - Ok(is_cached) - } - - /** - Checks if the given path is currently being used in `require`. - */ - pub fn is_pending(&self, abs_path: impl AsRef) -> LuaResult { - let is_pending = self - .cache_pending - .try_lock() - .expect("RequireContext may not be used from multiple threads") - .contains_key(abs_path.as_ref()); - Ok(is_pending) - } - - /** - Gets the resulting value from the require cache. - - Will panic if the path has not been cached, use [`is_cached`] first. - */ - pub fn get_from_cache<'lua>( - &self, - lua: &'lua Lua, - abs_path: impl AsRef, - ) -> LuaResult> { - let results = self - .cache_results - .try_lock() - .expect("RequireContext may not be used from multiple threads"); - - let cached = results - .get(abs_path.as_ref()) - .expect("Path does not exist in results cache"); - match cached { - Err(e) => Err(e.clone()), - Ok(k) => { - let multi_vec = lua - .registry_value::>(k) - .expect("Missing require result in lua registry"); - Ok(LuaMultiValue::from_vec(multi_vec)) - } - } - } - - /** - Waits for the resulting value from the require cache. - - Will panic if the path has not been cached, use [`is_cached`] first. - */ - pub async fn wait_for_cache<'lua>( - &self, - lua: &'lua Lua, - abs_path: impl AsRef, - ) -> LuaResult> { - let mut thread_recv = { - let pending = self - .cache_pending - .try_lock() - .expect("RequireContext may not be used from multiple threads"); - let thread_id = pending - .get(abs_path.as_ref()) - .expect("Path is not currently pending require"); - thread_id.subscribe() - }; - - thread_recv.recv().await.into_lua_err()?; - - self.get_from_cache(lua, abs_path.as_ref()) - } - - async fn load<'lua>( - &self, - lua: &'lua Lua, - abs_path: impl AsRef, - rel_path: impl AsRef, - ) -> LuaResult { - let abs_path = abs_path.as_ref(); - let rel_path = rel_path.as_ref(); - - // Read the file at the given path, try to parse and - // load it into a new lua thread that we can schedule - let file_contents = fs::read(&abs_path).await?; - let file_thread = lua - .load(file_contents) - .set_name(rel_path.to_string_lossy().to_string()); - - // Schedule the thread to run, wait for it to finish running - let thread_id = lua.push_thread_back(file_thread, ())?; - lua.track_thread(thread_id); - lua.wait_for_thread(thread_id).await; - let thread_res = lua.get_thread_result(thread_id).unwrap(); - - // Return the result of the thread, storing any lua value(s) in the registry - match thread_res { - Err(e) => Err(e), - Ok(v) => { - let multi_vec = v.into_vec(); - let multi_key = lua - .create_registry_value(multi_vec) - .expect("Failed to store require result in registry - out of memory"); - Ok(multi_key) - } - } - } - - /** - Loads (requires) the file at the given path. - */ - pub async fn load_with_caching<'lua>( - &self, - lua: &'lua Lua, - abs_path: impl AsRef, - rel_path: impl AsRef, - ) -> LuaResult> { - let abs_path = abs_path.as_ref(); - let rel_path = rel_path.as_ref(); - - // Set this abs path as currently pending - let (broadcast_tx, _) = broadcast::channel(1); - self.cache_pending - .try_lock() - .expect("RequireContext may not be used from multiple threads") - .insert(abs_path.to_path_buf(), broadcast_tx); - - // Try to load at this abs path - let load_res = self.load(lua, abs_path, rel_path).await; - let load_val = match &load_res { - Err(e) => Err(e.clone()), - Ok(k) => { - let multi_vec = lua - .registry_value::>(k) - .expect("Failed to fetch require result from registry"); - Ok(LuaMultiValue::from_vec(multi_vec)) - } - }; - - // NOTE: We use the async lock and not try_lock here because - // some other thread may be wanting to insert into the require - // cache at the same time, and that's not an actual error case - self.cache_results - .lock() - .await - .insert(abs_path.to_path_buf(), load_res); - - // Remove the pending thread id from the require context, - // broadcast a message to let any listeners know that this - // path has now finished the require process and is cached - let broadcast_tx = self - .cache_pending - .try_lock() - .expect("RequireContext may not be used from multiple threads") - .remove(abs_path) - .expect("Pending require broadcaster was unexpectedly removed"); - broadcast_tx.send(()).ok(); - - load_val - } - - /** - Loads (requires) the builtin with the given name. - */ - pub fn load_builtin<'lua>( - &self, - lua: &'lua Lua, - name: impl AsRef, - ) -> LuaResult> { - let builtin: LuneBuiltin = match name.as_ref().parse() { - Err(e) => return Err(LuaError::runtime(e)), - Ok(b) => b, - }; - - let mut cache = self - .cache_builtins - .try_lock() - .expect("RequireContext may not be used from multiple threads"); - - if let Some(res) = cache.get(&builtin) { - return match res { - Err(e) => return Err(e.clone()), - Ok(key) => { - let multi_vec = lua - .registry_value::>(key) - .expect("Missing builtin result in lua registry"); - Ok(LuaMultiValue::from_vec(multi_vec)) - } - }; - }; - - let result = builtin.create(lua); - - cache.insert( - builtin, - match result.clone() { - Err(e) => Err(e), - Ok(multi) => { - let multi_vec = multi.into_vec(); - let multi_key = lua - .create_registry_value(multi_vec) - .expect("Failed to store require result in registry - out of memory"); - Ok(multi_key) - } - }, - ); - - result - } -} diff --git a/crates/lune/src/lune/globals/require/mod.rs b/crates/lune/src/lune/globals/require/mod.rs deleted file mode 100644 index 1a83e58..0000000 --- a/crates/lune/src/lune/globals/require/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -use mlua::prelude::*; - -use crate::lune::util::TableBuilder; - -mod context; -use context::RequireContext; - -mod alias; -mod builtin; -mod path; - -const REQUIRE_IMPL: &str = r#" -return require(source(), ...) -"#; - -pub fn create(lua: &Lua) -> LuaResult> { - lua.set_app_data(RequireContext::new()); - - /* - Require implementation needs a few workarounds: - - - Async functions run outside of the lua resumption cycle, - so the current lua thread, as well as its stack/debug info - is not available, meaning we have to use a normal function - - - Using the async require function directly in another lua function - would mean yielding across the metamethod/c-call boundary, meaning - we have to first load our two functions into a normal lua chunk - and then load that new chunk into our final require function - - Also note that we inspect the stack at level 2: - - 1. The current c / rust function - 2. The wrapper lua chunk defined above - 3. The lua chunk we are require-ing from - */ - - let require_fn = lua.create_async_function(require)?; - 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", - )), - Some(info) => match info.source().source { - None => Err(LuaError::runtime( - "Stack info is missing source for require", - )), - Some(source) => lua.create_string(source.as_bytes()), - }, - })?; - - let require_env = TableBuilder::new(lua)? - .with_value("source", get_source_fn)? - .with_value("require", require_fn)? - .build_readonly()?; - - lua.load(REQUIRE_IMPL) - .set_name("require") - .set_environment(require_env) - .into_function() -} - -async fn require<'lua>( - lua: &'lua Lua, - (source, path): (LuaString<'lua>, LuaString<'lua>), -) -> LuaResult> { - let source = source - .to_str() - .into_lua_err() - .context("Failed to parse require source as string")? - .to_string(); - - let path = path - .to_str() - .into_lua_err() - .context("Failed to parse require path as string")? - .to_string(); - - let context = lua - .app_data_ref() - .expect("Failed to get RequireContext from app data"); - - if let Some(builtin_name) = path - .strip_prefix("@lune/") - .map(|name| name.to_ascii_lowercase()) - { - builtin::require(lua, &context, &builtin_name).await - } else if let Some(aliased_path) = path.strip_prefix('@') { - let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime( - "Require with custom alias must contain '/' delimiter", - ))?; - alias::require(lua, &context, &source, alias, path).await - } else { - path::require(lua, &context, &source, &path).await - } -} diff --git a/crates/lune/src/lune/globals/require/path.rs b/crates/lune/src/lune/globals/require/path.rs deleted file mode 100644 index 7e8084f..0000000 --- a/crates/lune/src/lune/globals/require/path.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::{Path, PathBuf}; - -use mlua::prelude::*; -use mlua::Error::ExternalError; - -use super::context::*; - -pub(super) async fn require<'lua, 'ctx>( - lua: &'lua Lua, - ctx: &'ctx RequireContext, - source: &str, - path: &str, -) -> LuaResult> -where - 'lua: 'ctx, -{ - let (abs_path, rel_path) = ctx.resolve_paths(source, path)?; - require_abs_rel(lua, ctx, abs_path, rel_path).await -} - -pub(super) async fn require_abs_rel<'lua, 'ctx>( - lua: &'lua Lua, - ctx: &'ctx RequireContext, - abs_path: PathBuf, // Absolute to filesystem - rel_path: PathBuf, // Relative to CWD (for displaying) -) -> LuaResult> -where - 'lua: 'ctx, -{ - // 1. Try to require the exact path - match require_inner(lua, ctx, &abs_path, &rel_path).await { - Ok(res) => return Ok(res), - Err(err) => { - if !is_file_not_found_error(&err) { - return Err(err); - } - } - } - - // 2. Try to require the path with an added "luau" extension - // 3. Try to require the path with an added "lua" extension - for extension in ["luau", "lua"] { - match require_inner( - lua, - ctx, - &append_extension(&abs_path, extension), - &append_extension(&rel_path, extension), - ) - .await - { - Ok(res) => return Ok(res), - Err(err) => { - if !is_file_not_found_error(&err) { - return Err(err); - } - } - } - } - - // We didn't find any direct file paths, look - // for directories with "init" files in them... - let abs_init = abs_path.join("init"); - let rel_init = rel_path.join("init"); - - // 4. Try to require the init path with an added "luau" extension - // 5. Try to require the init path with an added "lua" extension - for extension in ["luau", "lua"] { - match require_inner( - lua, - ctx, - &append_extension(&abs_init, extension), - &append_extension(&rel_init, extension), - ) - .await - { - Ok(res) => return Ok(res), - Err(err) => { - if !is_file_not_found_error(&err) { - return Err(err); - } - } - } - } - - // Nothing left to try, throw an error - Err(LuaError::runtime(format!( - "No file exists at the path '{}'", - rel_path.display() - ))) -} - -async fn require_inner<'lua, 'ctx>( - lua: &'lua Lua, - ctx: &'ctx RequireContext, - abs_path: impl AsRef, - rel_path: impl AsRef, -) -> LuaResult> -where - 'lua: 'ctx, -{ - let abs_path = abs_path.as_ref(); - let rel_path = rel_path.as_ref(); - - if ctx.is_cached(abs_path)? { - ctx.get_from_cache(lua, abs_path) - } else if ctx.is_pending(abs_path)? { - ctx.wait_for_cache(lua, &abs_path).await - } else { - ctx.load_with_caching(lua, &abs_path, &rel_path).await - } -} - -fn append_extension(path: impl Into, ext: &'static str) -> PathBuf { - let mut new = path.into(); - match new.extension() { - // FUTURE: There's probably a better way to do this than converting to a lossy string - Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())), - None => new.set_extension(ext), - }; - new -} - -fn is_file_not_found_error(err: &LuaError) -> bool { - if let ExternalError(err) = err { - err.as_ref().downcast_ref::().is_some() - } else { - false - } -} diff --git a/crates/lune/src/lune/globals/version.rs b/crates/lune/src/lune/globals/version.rs deleted file mode 100644 index 1228fd0..0000000 --- a/crates/lune/src/lune/globals/version.rs +++ /dev/null @@ -1,39 +0,0 @@ -use mlua::prelude::*; - -pub fn create(lua: &Lua) -> LuaResult> { - let lune_version = format!("Lune {}", env!("CARGO_PKG_VERSION")); - - let luau_version_full = lua - .globals() - .get::<_, LuaString>("_VERSION") - .expect("Missing _VERSION global"); - let luau_version_str = luau_version_full - .to_str() - .context("Invalid utf8 found in _VERSION global")?; - - // If this function runs more than once, we - // may get an already formatted lune version. - if luau_version_str.starts_with(lune_version.as_str()) { - return Ok(luau_version_full); - } - - // Luau version is expected to be in the format "Luau 0.x" and sometimes "Luau 0.x.y" - if !luau_version_str.starts_with("Luau 0.") { - panic!("_VERSION global is formatted incorrectly\nGot: '{luau_version_str}'") - } - let luau_version = luau_version_str.strip_prefix("Luau 0.").unwrap().trim(); - - // We make some guarantees about the format of the _VERSION global, - // so make sure that the luau version also follows those rules. - if luau_version.is_empty() { - panic!("_VERSION global is missing version number\nGot: '{luau_version_str}'") - } else if !luau_version.chars().all(is_valid_version_char) { - panic!("_VERSION global contains invalid characters\nGot: '{luau_version_str}'") - } - - lua.create_string(format!("{lune_version}+{luau_version}")) -} - -fn is_valid_version_char(c: char) -> bool { - matches!(c, '0'..='9' | '.') -} diff --git a/crates/lune/src/lune/globals/warn.rs b/crates/lune/src/lune/globals/warn.rs deleted file mode 100644 index dfc409e..0000000 --- a/crates/lune/src/lune/globals/warn.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::io::Write as _; - -use mlua::prelude::*; - -use crate::lune::util::formatting::{format_label, pretty_format_multi_value}; - -pub fn create(lua: &Lua) -> LuaResult> { - lua.create_function(|_, args: LuaMultiValue| { - let formatted = format!( - "{}\n{}\n", - format_label("warn"), - pretty_format_multi_value(&args)? - ); - let mut stderr = std::io::stderr(); - stderr.write_all(formatted.as_bytes())?; - stderr.flush()?; - Ok(()) - }) -} diff --git a/crates/lune/src/lune/util/formatting.rs b/crates/lune/src/lune/util/formatting.rs deleted file mode 100644 index 54376b7..0000000 --- a/crates/lune/src/lune/util/formatting.rs +++ /dev/null @@ -1,477 +0,0 @@ -use std::fmt::Write; - -use console::{colors_enabled, set_colors_enabled, style, Style}; -use mlua::prelude::*; -use once_cell::sync::Lazy; - -const MAX_FORMAT_DEPTH: usize = 4; - -const INDENT: &str = " "; - -pub const STYLE_RESET_STR: &str = "\x1b[0m"; - -// Colors -pub static COLOR_BLACK: Lazy