Compare commits

..

No commits in common. "main" and "v0.8.0" have entirely different histories.
main ... v0.8.0

296 changed files with 5622 additions and 12916 deletions

4
.gitattributes vendored
View file

@ -1,5 +1,9 @@
* text=auto
# Temporarily highlight luau as normal lua files
# until we get native linguist support for Luau
*.luau linguist-language=Lua
# Ensure all lua files use LF
*.lua eol=lf
*.luau eol=lf

View file

@ -23,8 +23,11 @@ jobs:
with:
components: rustfmt
- name: Install Just
uses: extractions/setup-just@v1
- name: Install Tooling
uses: CompeyDev/setup-rokit@v0.1.2
uses: ok-nick/setup-aftman@v0.4.2
- name: Check Formatting
run: just fmt-check
@ -37,8 +40,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v1
- name: Install Tooling
uses: CompeyDev/setup-rokit@v0.1.2
uses: ok-nick/setup-aftman@v0.4.2
- name: Analyze
run: just analyze
@ -58,13 +64,9 @@ jobs:
cargo-target: x86_64-unknown-linux-gnu
- name: macOS x86_64
runner-os: macos-13
runner-os: macos-latest
cargo-target: x86_64-apple-darwin
- name: macOS aarch64
runner-os: macos-14
cargo-target: aarch64-apple-darwin
name: CI - ${{ matrix.name }}
runs-on: ${{ matrix.runner-os }}
steps:
@ -82,20 +84,17 @@ jobs:
- name: Build
run: |
cargo build \
--workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Lint
run: |
cargo clippy \
--workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Test
run: |
cargo test \
--lib --workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}

24
.github/workflows/publish.yaml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Publish
on:
push:
branches:
- "main"
workflow_dispatch:
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io
uses: katyo/publish-crates@v2
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
ignore-unpublished-changes: true

View file

@ -21,32 +21,14 @@ jobs:
uses: actions/checkout@v4
- name: Get version from manifest
uses: SebRollen/toml-action@v1.2.0
uses: SebRollen/toml-action@9062fbef52816d61278d24ce53c8070440e1e8dd
id: get_version
with:
file: crates/lune/Cargo.toml
file: Cargo.toml
field: package.version
# dry-run:
# name: Dry-run
# needs: ["init"]
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Install Rust
# uses: dtolnay/rust-toolchain@stable
# - name: Publish (dry-run)
# uses: katyo/publish-crates@v2
# with:
# dry-run: true
# check-repo: true
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
build:
needs: ["init"] # , "dry-run"]
needs: ["init"]
strategy:
fail-fast: false
matrix:
@ -88,7 +70,7 @@ jobs:
targets: ${{ matrix.cargo-target }}
- name: Install Just
uses: extractions/setup-just@v2
uses: extractions/setup-just@v1
- name: Install build tooling (aarch64-unknown-linux-gnu)
if: matrix.cargo-target == 'aarch64-unknown-linux-gnu'
@ -104,24 +86,24 @@ jobs:
run: just zip-release ${{ matrix.cargo-target }}
- name: Upload release artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact-name }}
path: release.zip
release-github:
name: Release (GitHub)
release:
name: Release
runs-on: ubuntu-latest
needs: ["init", "build"] # , "dry-run", "build"]
needs: ["init", "build"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v2
uses: extractions/setup-just@v1
- name: Download releases
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
path: ./releases
@ -129,7 +111,7 @@ jobs:
run: just unpack-releases "./releases"
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -138,21 +120,3 @@ jobs:
fail_on_unmatched_files: true
files: ./releases/*.zip
draft: true
# release-crates:
# name: Release (crates.io)
# runs-on: ubuntu-latest
# needs: ["init", "dry-run", "build"]
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Install Rust
# uses: dtolnay/rust-toolchain@stable
# - name: Publish crates
# uses: katyo/publish-crates@v2
# with:
# dry-run: false
# check-repo: true
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View file

@ -129,7 +129,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.exec("ping", {
local result = process.spawn("ping", {
"google.com",
"-c 4",
})

View file

@ -28,8 +28,8 @@ end)
for _ = 1, 5 do
local start = os.clock()
socket:send(tostring(1))
local response = socket:next()
socket.send(tostring(1))
local response = socket.next()
local elapsed = os.clock() - start
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
task.wait(1 - elapsed)
@ -38,7 +38,7 @@ end
-- Everything went well, and we are done with the socket, so we can close it
print("Closing web socket...")
socket:close()
socket.close()
task.cancel(forceExit)
print("Done! 🌙")

View file

@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
handleWebSocket = function(socket)
print("Got new web socket connection!")
repeat
local message = socket:next()
local message = socket.next()
if message ~= nil then
socket:send("Echo - " .. message)
socket.send("Echo - " .. message)
end
until message == nil
print("Web socket disconnected.")

View file

@ -8,266 +8,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## `0.9.0`
### Breaking changes
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write stdio of the process.
```lua
local child = process.create("program", {
"cli-argument",
"other-cli-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading from stdout
local data = child.stdout:read()
print(buffer.tostring(data))
```
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
[#178]: https://github.com/lune-org/lune/pull/178
[#211]: https://github.com/lune-org/lune/pull/211
[#248]: https://github.com/lune-org/lune/pull/248
[#250]: https://github.com/lune-org/lune/pull/250
## `0.8.9` - October 7th, 2024
### Changed
- Updated to Luau version `0.640`
## `0.8.8` - August 22nd, 2024
### Fixed
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
[#245]: https://github.com/lune-org/lune/pull/245
## `0.8.7` - August 10th, 2024
### Added
- Added a compression level option to `serde.compress` ([#224])
- Added missing vector methods to the `roblox` library ([#228])
### Changed
- Updated to Luau version `0.635`
- Updated to rbx-dom database version `0.634`
### Fixed
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
[#220]: https://github.com/lune-org/lune/pull/220
[#224]: https://github.com/lune-org/lune/pull/224
[#228]: https://github.com/lune-org/lune/pull/228
## `0.8.6` - June 23rd, 2024
### Added
- Added a builtin API for hashing and calculating HMACs as part of the `serde` library ([#193])
Basic usage:
```lua
local serde = require("@lune/serde")
local hash = serde.hash("sha256", "a message to hash")
local hmac = serde.hmac("sha256", "a message to hash", "a secret string")
print(hash)
print(hmac)
```
The returned hashes are sequences of lowercase hexadecimal digits. The following algorithms are supported:
`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake3`
- Added two new options to `luau.load`:
- `codegenEnabled` - whether or not codegen should be enabled for the loaded chunk.
- `injectGlobals` - whether or not to inject globals into a passed `environment`.
By default, globals are injected and codegen is disabled.
Check the documentation for the `luau` standard library for more information.
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
- Fixed the `_VERSION` global containing an incorrect Lune version string.
### Changed
- Sandboxing and codegen in the Luau VM is now fully enabled, resulting in up to 2x or faster code execution.
This should not result in any behavior differences in Lune, but if it does, please open an issue.
- Improved formatting of custom error objects (such as when `fs.readFile` returns an error) when printed or formatted using `stdio.format`.
### Fixed
- Fixed `__type` and `__tostring` metamethods on userdatas and tables not being respected when printed or formatted using `stdio.format`.
[#193]: https://github.com/lune-org/lune/pull/193
[#196]: https://github.com/lune-org/lune/pull/196
## `0.8.5` - June 1st, 2024
### Changed
- Improved table pretty formatting when using `print`, `warn`, and `stdio.format`:
- Keys are sorted numerically / alphabetically when possible.
- Keys of different types are put in distinct sections for mixed tables.
- Tables that are arrays no longer display their keys.
- Empty tables are no longer spread across lines.
## Fixed
- Fixed formatted values in tables not being separated by newlines.
- Fixed panicking (crashing) when using `process.spawn` with a program that does not exist.
- Fixed `instance:SetAttribute("name", nil)` throwing an error and not removing the attribute.
## `0.8.4` - May 12th, 2024
### Added
- Added a builtin API for regular expressions.
Example basic usage:
```lua
local Regex = require("@lune/regex")
local re = Regex.new("hello")
if re:isMatch("hello, world!") then
print("Matched!")
end
local caps = re:captures("hello, world! hello, again!")
print(#caps) -- 2
print(caps:get(1)) -- "hello"
print(caps:get(2)) -- "hello"
print(caps:get(3)) -- nil
```
Check out the documentation for more details.
- Added support for buffers as arguments in builtin APIs ([#148])
This includes APIs such as `fs.writeFile`, `serde.encode`, and more.
- Added support for cross-compilation of standalone binaries ([#162])
You can now compile standalone binaries for other platforms by passing
an additional `target` argument to the `build` subcommand:
```sh
lune build my-file.luau --output my-bin --target windows-x86_64
```
Currently supported targets are the same as the ones included with each
release of Lune on GitHub. Check releases for a full list of targets.
- Added `stdio.readToEnd()` for reading the entire stdin passed to Lune
### Changed
- Split the repository into modular crates instead of a monolith. ([#188])
If you previously depended on Lune as a crate, nothing about it has changed for version `0.8.4`, but now each individual sub-crate has also been published and is available for use:
- `lune` (old)
- `lune-utils`
- `lune-roblox`
- `lune-std-*` for every builtin library
When depending on the main `lune` crate, each builtin library also has a feature flag that can be toggled in the format `std-*`.
In general, this should mean that it is now much easier to make your own Lune builtin, publish your own flavor of a Lune CLI, or take advantage of all the work that has been done for Lune as a runtime when making your own Rust programs.
- Changed the `User-Agent` header in `net.request` to be more descriptive ([#186])
- Updated to Luau version `0.622`.
### Fixed
- Fixed not being able to decompress `lz4` format in high compression mode
- Fixed stack overflow for tables with circular keys ([#183])
- Fixed `net.serve` no longer accepting ipv6 addresses
- Fixed headers in `net.serve` being raw bytes instead of strings
[#148]: https://github.com/lune-org/lune/pull/148
[#162]: https://github.com/lune-org/lune/pull/162
[#183]: https://github.com/lune-org/lune/pull/183
[#186]: https://github.com/lune-org/lune/pull/186
[#188]: https://github.com/lune-org/lune/pull/188
## `0.8.3` - April 15th, 2024
### Fixed
- Fixed `require` not throwing syntax errors ([#168])
- Fixed `require` caching not working correctly ([#171])
- Fixed case-sensitivity issue in `require` with aliases ([#173])
- Fixed `itertools` dependency being marked optional even though it is mandatory ([#176])
- Fixed test cases for the `net` built-in library on Windows ([#177])
[#168]: https://github.com/lune-org/lune/pull/168
[#171]: https://github.com/lune-org/lune/pull/171
[#173]: https://github.com/lune-org/lune/pull/173
[#176]: https://github.com/lune-org/lune/pull/176
[#177]: https://github.com/lune-org/lune/pull/177
## `0.8.2` - March 12th, 2024
### Fixed
- Fixed REPL panicking after the first evaluation / run.
- Fixed globals reloading on each run in the REPL, causing unnecessary slowdowns.
- Fixed `net.serve` requests no longer being plain tables in Lune `0.8.1`, breaking usage of things such as `table.clone`.
## `0.8.1` - March 11th, 2024
### Added
- Added the ability to specify an address in `net.serve`. ([#142])
### Changed
- Update to Luau version `0.616`.
- Major performance improvements when using a large amount of threads / asynchronous Lune APIs. ([#165])
- Minor performance improvements and less overhead for `net.serve` and `net.socket`. ([#165])
### Fixed
- Fixed `fs.copy` not working with empty dirs. ([#155])
- Fixed stack overflow when printing tables with cyclic references. ([#158])
- Fixed not being able to yield in `net.serve` handlers without blocking other requests. ([#165])
- Fixed various scheduler issues / panics. ([#165])
[#142]: https://github.com/lune-org/lune/pull/142
[#155]: https://github.com/lune-org/lune/pull/155
[#158]: https://github.com/lune-org/lune/pull/158
[#165]: https://github.com/lune-org/lune/pull/165
## `0.8.0` - January 14th, 2024
### Breaking Changes

2238
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,42 @@
[workspace]
resolver = "2"
default-members = ["crates/lune"]
members = [
"crates/lune",
"crates/lune-roblox",
"crates/lune-std",
"crates/lune-std-datetime",
"crates/lune-std-fs",
"crates/lune-std-luau",
"crates/lune-std-net",
"crates/lune-std-process",
"crates/lune-std-regex",
"crates/lune-std-roblox",
"crates/lune-std-serde",
"crates/lune-std-stdio",
"crates/lune-std-task",
"crates/lune-utils",
"crates/mlua-luau-scheduler",
[package]
name = "lune"
version = "0.8.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "A standalone Luau runtime"
readme = "README.md"
keywords = ["cli", "lua", "luau", "runtime"]
categories = ["command-line-interface"]
[[bin]]
name = "lune"
path = "src/main.rs"
[lib]
name = "lune"
path = "src/lib.rs"
[features]
default = ["cli", "roblox"]
cli = [
"dep:anyhow",
"dep:env_logger",
"dep:itertools",
"dep:clap",
"dep:include_dir",
"dep:regex",
"dep:rustyline",
]
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",
]
# Profile for building the release binary, with the following options set:
@ -34,31 +54,86 @@ opt-level = "z"
strip = true
lto = true
# Lints for all crates in the workspace
# All of the dependencies for Lune.
#
# 1. Error on all lints by default, then make cargo + clippy pedantic lints just warn
# 2. Selectively allow some lints that are _too_ pedantic, such as:
# - Casts between number types
# - Module naming conventions
# - Imports and multiple dependency versions
[workspace.lints.clippy]
all = { level = "deny", priority = -3 }
cargo = { level = "warn", priority = -2 }
pedantic = { level = "warn", priority = -1 }
# 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]
console = "0.15"
directories = "5.0"
futures-util = "0.3"
once_cell = "1.17"
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"
cast_lossless = { level = "allow", priority = 1 }
cast_possible_truncation = { level = "allow", priority = 1 }
cast_possible_wrap = { level = "allow", priority = 1 }
cast_precision_loss = { level = "allow", priority = 1 }
cast_sign_loss = { level = "allow", priority = 1 }
### RUNTIME
similar_names = { level = "allow", priority = 1 }
unnecessary_wraps = { level = "allow", priority = 1 }
unnested_or_patterns = { level = "allow", priority = 1 }
unreadable_literal = { level = "allow", priority = 1 }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
mlua = { version = "0.9.1", features = ["luau", "luau-jit", "serialize"] }
tokio = { version = "1.24", features = ["full", "tracing"] }
os_str_bytes = { version = "6.4", features = ["conversions"] }
multiple_crate_versions = { level = "allow", priority = 1 }
module_inception = { level = "allow", priority = 1 }
module_name_repetitions = { level = "allow", priority = 1 }
needless_pass_by_value = { level = "allow", priority = 1 }
wildcard_imports = { level = "allow", priority = 1 }
### 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 = "0.14", features = ["full"] }
hyper-tungstenite = { version = "0.11" }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
### DATETIME
chrono = "0.4"
chrono_lc = "0.1"
### CLI
anyhow = { optional = true, version = "1.0" }
env_logger = { optional = true, version = "0.10" }
itertools = { optional = true, version = "0.12" }
clap = { optional = true, version = "4.1", features = ["derive"] }
include_dir = { optional = true, version = "0.7", features = ["glob"] }
regex = { optional = true, version = "1.7", default-features = false, features = [
"std",
"unicode-perl",
] }
rustyline = { optional = true, version = "13.0" }
### ROBLOX
glam = { optional = true, version = "0.25" }
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" }

View file

@ -33,7 +33,7 @@ Lune provides fully asynchronous APIs wherever possible, and is built in Rust
## Features
- 🌙 Strictly minimal but powerful interface that is easy to read and remember, just like Luau itself
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb zipped) executable
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb) executable
- 📚 World-class documentation, on the web _or_ directly in your editor, no network connection necessary
- 🏡 Familiar runtime environment for Roblox developers, with an included 1-to-1 task scheduler port
- ✏️ Optional built-in library for manipulating Roblox place & model files, and their instances

4
aftman.toml Normal file
View file

@ -0,0 +1,4 @@
[tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.27.0"
selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/StyLua@0.19.1"

View file

@ -1,29 +0,0 @@
[package]
name = "lune-roblox"
version = "0.1.4"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Roblox library for Lune"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
glam = "0.27"
rand = "0.8"
thiserror = "1.0"
once_cell = "1.17"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,120 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::{Content as DomContent, ContentType};
use lune_utils::TableBuilder;
use crate::{exports::LuaExportsTable, instance::Instance};
use super::{super::*, EnumItem};
/**
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
This implements all documented properties, methods & constructors of the Content type as of April 2025.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct Content(ContentType);
impl LuaExportsTable<'_> for Content {
const EXPORT_NAME: &'static str = "Content";
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};
TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}
impl LuaUserData for Content {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}
impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}

View file

@ -1,22 +0,0 @@
[package]
name = "lune-std-datetime"
version = "0.1.3"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - DateTime"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
thiserror = "1.0"
chrono = "0.4.38"
chrono_lc = "0.1.6"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,36 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod date_time;
mod result;
mod values;
pub use self::date_time::DateTime;
/**
Creates the `datetime` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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()
}

View file

@ -1,23 +0,0 @@
[package]
name = "lune-std-fs"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - FS"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
bstr = "1.9"
tokio = { version = "1", default-features = false, features = ["fs"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }

View file

@ -1,18 +0,0 @@
[package]
name = "lune-std-luau"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Luau"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "luau-jit"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,90 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::{jit::JitStatus, TableBuilder};
mod options;
use self::options::{LuauCompileOptions, LuauLoadOptions};
const BYTECODE_ERROR_BYTE: u8 = 0;
/**
Creates the `luau` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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<LuaString<'lua>> {
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<LuaFunction<'lua>> {
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
let env_changed = options.environment.is_some();
if let Some(custom_environment) = options.environment {
let environment = lua.create_table()?;
// Inject all globals into the environment
if options.inject_globals {
for pair in lua.globals().pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
}
if let Some(global_metatable) = lua.globals().get_metatable() {
environment.set_metatable(Some(global_metatable));
}
} else if let Some(custom_metatable) = custom_environment.get_metatable() {
// Since we don't need to set the global metatable,
// we can just set a custom metatable if it exists
environment.set_metatable(Some(custom_metatable));
}
// Inject the custom environment
for pair in custom_environment.pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
}
chunk = chunk.set_environment(environment);
}
// Enable JIT if codegen is enabled and the environment hasn't
// changed, otherwise disable JIT since it'll fall back anyways
lua.enable_jit(options.codegen_enabled && !env_changed);
let function = chunk.into_function()?;
lua.enable_jit(
lua.app_data_ref::<JitStatus>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function)
}

View file

@ -1,39 +0,0 @@
[package]
name = "lune-std-net"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Net"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
bstr = "1.9"
futures-util = "0.3"
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" }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
urlencoding = "2.1"
tokio = { version = "1", default-features = false, features = [
"sync",
"net",
"macros",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-serde = { version = "0.1.2", path = "../lune-std-serde" }

View file

@ -1,163 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
use lune_std_serde::{decompress, CompressDecompressFormat};
use lune_utils::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<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
where
K: AsRef<str>,
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<NetClient> {
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<NetClientResponse> {
// 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::<Vec<_>>(),
);
}
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(res_bytes, format).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<Self> {
if let LuaValue::UserData(ud) = value {
if let Ok(ctx) = ud.borrow::<NetClient>() {
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<u8>,
body_decompressed: bool,
}
impl NetClientResponse {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
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()
}
}

View file

@ -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<Self> {
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<bool>>("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<String, Vec<String>>,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl FromLua<'_> for RequestConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
// 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: RequestConfigOptions::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<LuaFunction<'a>>,
}
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
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<LuaString> = t.get("address")?;
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = 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,
})
}
}
}

View file

@ -1,102 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use bstr::BString;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
mod client;
mod config;
mod server;
mod util;
mod websocket;
use lune_utils::TableBuilder;
use self::{
client::{NetClient, NetClientBuilder},
config::{RequestConfig, ServeConfig},
server::serve,
util::create_user_agent_header,
websocket::NetWebSocket,
};
use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
/**
Creates the `net` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()));
encode(val, lua, config)
}
fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json);
decode(json, lua, config)
}
async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
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<LuaValue> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua(lua)
}
async fn net_serve<'lua>(
lua: &'lua Lua,
(port, config): (u16, ServeConfig<'lua>),
) -> LuaResult<LuaTable<'lua>> {
serve(lua, port, config).await
}
fn net_url_encode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
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<bool>),
) -> LuaResult<LuaValue<'lua>> {
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)
}
}

View file

@ -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<LuaFunction<'lua>>,
) -> LuaResult<Self> {
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<LuaFunction<'lua>> {
lua.named_registry_value(self.key_request)
}
pub(super) fn websocket_handler<'lua>(
&self,
lua: &'lua Lua,
) -> LuaResult<Option<LuaFunction<'lua>>> {
self.key_websocket
.map(|key| lua.named_registry_value(key))
.transpose()
}
}

View file

@ -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 lune_utils::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<LuaTable<'lua>> {
let addr: SocketAddr = (config.address, port).into();
let listener = TcpListener::bind(addr).await?;
let (lua_svc, lua_inner) = {
let rc = lua
.app_data_ref::<Weak<Lua>>()
.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 |_, (): ()| match shutdown_tx.send(true) {
Ok(()) => Ok(()),
Err(_) => Err(LuaError::runtime("Server already stopped")),
})?
.build_readonly()
}

View file

@ -1,56 +0,0 @@
use std::{collections::HashMap, net::SocketAddr};
use http::request::Parts;
use mlua::prelude::*;
use lune_utils::TableBuilder;
pub(super) struct LuaRequest {
pub(super) _remote_addr: SocketAddr,
pub(super) head: Parts,
pub(super) body: Vec<u8>,
}
impl LuaRequest {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
let method = self.head.method.as_str().to_string();
let path = self.head.uri.path().to_string();
let body = lua.create_string(&self.body)?;
#[allow(clippy::mutable_key_type)]
let query: HashMap<LuaString, LuaString> = 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::<LuaResult<_>>()?;
#[allow(clippy::mutable_key_type)]
let headers: HashMap<LuaString, LuaString> = 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::<LuaResult<_>>()?;
TableBuilder::new(lua)?
.with_value("method", method)?
.with_value("path", path)?
.with_value("query", query)?
.with_value("headers", headers)?
.with_value("body", body)?
.build()
}
}

View file

@ -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<Lua>,
pub(super) addr: SocketAddr,
pub(super) keys: SvcKeys,
}
impl Service<Request<Incoming>> for Svc {
type Response = Response<Full<Bytes>>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: Request<Incoming>) -> 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_val = lua_sock.into_lua(&lua_inner).unwrap();
let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner
.push_thread_back(handler_websocket, lua_val)
.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()
})
}
}
}

View file

@ -1,149 +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,
};
#[derive(Debug)]
pub struct NetWebSocket<T> {
close_code_exists: Arc<AtomicBool>,
close_code_value: Arc<AtomicU16>,
read_stream: Arc<AsyncMutex<SplitStream<WebSocketStream<T>>>>,
write_stream: Arc<AsyncMutex<SplitSink<WebSocketStream<T>, WsMessage>>>,
}
impl<T> Clone for NetWebSocket<T> {
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<T> NetWebSocket<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
pub fn new(value: WebSocketStream<T>) -> 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<u16> {
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<Option<WsMessage>> {
let mut ws = self.read_stream.lock().await;
ws.next().await.transpose().into_lua_err()
}
pub async fn close(&self, code: Option<u16>) -> 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()
}
}
impl<T> LuaUserData for NetWebSocket<T>
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", |_, this, code: Option<u16>| async move {
this.close(code).await
});
methods.add_async_method(
"send",
|_, this, (string, as_binary): (BString, Option<bool>)| 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),
})
});
}
}

View file

@ -1,34 +0,0 @@
[package]
name = "lune-std-process"
version = "0.1.3"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Process"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
directories = "5.0"
pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
"process",
"rt",
"sync",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,289 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
cell::RefCell,
env::{
self,
consts::{ARCH, OS},
},
path::MAIN_SEPARATOR,
process::Stdio,
rc::Rc,
sync::Arc,
};
use mlua::prelude::*;
use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString;
use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
mod options;
mod stream;
mod tee_writer;
mod wait_for_child;
use self::options::ProcessSpawnOptions;
use self::wait_for_child::wait_for_child;
use lune_utils::path::get_current_dir;
/**
Creates the `process` standard library module.
# Errors
Errors when out of memory.
*/
#[allow(clippy::missing_panics_doc)]
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
let mut cwd_str = get_current_dir()
.to_str()
.expect("cwd should be valid UTF-8")
.to_string();
if !cwd_str.ends_with(MAIN_SEPARATOR) {
cwd_str.push(MAIN_SEPARATOR);
}
// Create constants for OS & processor architecture
let os = lua.create_string(OS.to_lowercase())?;
let arch = lua.create_string(ARCH.to_lowercase())?;
let endianness = lua.create_string(if cfg!(target_endian = "big") {
"big"
} else {
"little"
})?;
// Create readonly args array
let args_vec = lua
.app_data_ref::<Vec<String>>()
.ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
.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("endianness", endianness)?
.with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly()
}
fn process_env_get<'lua>(
lua: &'lua Lua,
(_, key): (LuaValue<'lua>, String),
) -> LuaResult<LuaValue<'lua>> {
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<String>),
) -> LuaResult<()> {
// Make sure key is valid, otherwise set_var will panic
if key.is_empty() {
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
} else if key.contains('=') {
Err(LuaError::RuntimeError(
"Key must not contain the equals character '='".to_string(),
))
} else if key.contains('\0') {
Err(LuaError::RuntimeError(
"Key must not contain the NUL character".to_string(),
))
} else if let Some(value) = value {
// Make sure value is valid, otherwise set_var will panic
if value.contains('\0') {
Err(LuaError::RuntimeError(
"Value must not contain the NUL character".to_string(),
))
} else {
env::set_var(&key, &value);
Ok(())
}
} else {
env::remove_var(&key);
Ok(())
}
}
fn process_env_iter<'lua>(
lua: &'lua Lua,
(_, ()): (LuaValue<'lua>, ()),
) -> LuaResult<LuaFunction<'lua>> {
let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
lua.create_function_mut(move |lua, (): ()| match vars.next() {
Some((key, value)) => {
let raw_key = RawOsString::new(key);
let raw_value = RawOsString::new(value);
Ok((
LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
))
}
None => Ok((LuaValue::Nil, LuaValue::Nil)),
})
}
async fn process_exec(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
/*
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(i32::from(!res.stderr.is_empty()));
// 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()
}
#[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
program: String,
args: Option<Vec<String>>,
mut options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdin = options.stdio.stdin.take();
let mut child = spawn_command(program, args, options)?;
if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?;
}
Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
}

View file

@ -1,58 +0,0 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -1,21 +0,0 @@
[package]
name = "lune-std-regex"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - RegEx"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
regex = "1.10"
self_cell = "1.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -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<Captures<'a>>;
self_cell! {
struct LuaCapturesInner {
owner: Arc<String>,
#[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<Self> {
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<String> {
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!("{}", this.num_captures()))
});
}
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
}
}

View file

@ -1,28 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod captures;
mod matches;
mod regex;
use self::regex::LuaRegex;
/**
Creates the `regex` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("new", new_regex)?
.build_readonly()
}
fn new_regex(_: &Lua, pattern: String) -> LuaResult<LuaRegex> {
LuaRegex::new(pattern)
}

View file

@ -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<String>,
start: usize,
end: usize,
}
impl LuaMatch {
/**
Create a new `LuaMatch` instance from a `String` text and a `regex::Match`.
*/
pub fn new(text: Arc<String>, matched: Match) -> Self {
Self {
text,
start: matched.start(),
end: matched.end(),
}
}
fn range(&self) -> Range<usize> {
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(this.slice().to_string())
});
}
}

View file

@ -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<Self> {
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(ToString::to_string)
.collect::<Vec<_>>())
});
// 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(this.inner.as_str().to_string())
});
}
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "Regex");
}
}

View file

@ -1,24 +0,0 @@
[package]
name = "lune-std-roblox"
version = "0.1.4"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Roblox"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
once_cell = "1.17"
rbx_cookie = { version = "0.1.4", default-features = false }
roblox_install = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-roblox = { version = "0.1.4", path = "../lune-roblox" }

View file

@ -1,47 +0,0 @@
[package]
name = "lune-std-serde"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Serde"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "serialize"] }
async-compression = { version = "0.4", features = [
"tokio",
"brotli",
"deflate",
"gzip",
"zlib",
] }
bstr = "1.9"
lz4 = "1.26"
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"] }
digest = "0.10.7"
hmac = "0.12.1"
md-5 = "0.10.6"
sha1 = "0.10.6"
sha2 = "0.10.8"
sha3 = "0.10.8"
# This feature MIGHT break due to the unstable nature of the digest crate.
# Check before updating it.
blake3 = { version = "=1.5.0", features = ["traits-preview"] }
tokio = { version = "1", default-features = false, features = [
"rt",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,158 +0,0 @@
use mlua::prelude::*;
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use toml::Value as TomlValue;
// NOTE: These are options for going from other format -> lua ("serializing" lua values)
const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
.set_array_metatable(false)
.serialize_none_to_null(false)
.serialize_unit_to_null(false);
// NOTE: These are options for going from lua -> other format ("deserializing" lua values)
const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
.sort_keys(true)
.deny_recursive_tables(false)
.deny_unsupported_types(true);
/**
An encoding and decoding format supported by Lune.
Encode / decode in this case is synonymous with serialize / deserialize.
*/
#[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<Self> {
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,
})
}
}
}
/**
Configuration for encoding and decoding values.
Encoding / decoding in this case is synonymous with serialize / deserialize.
*/
#[derive(Debug, Clone, Copy)]
pub struct EncodeDecodeConfig {
pub format: EncodeDecodeFormat,
pub pretty: bool,
}
impl From<EncodeDecodeFormat> 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,
}
}
}
/**
Encodes / serializes the given value into a string, using the specified configuration.
# Errors
Errors when the encoding fails.
*/
pub fn encode<'lua>(
value: LuaValue<'lua>,
lua: &'lua Lua,
config: EncodeDecodeConfig,
) -> LuaResult<LuaString<'lua>> {
let bytes = match config.format {
EncodeDecodeFormat::Json => {
let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
if config.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 config.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)
}
/**
Decodes / deserializes the given string into a value, using the specified configuration.
# Errors
Errors when the decoding fails.
*/
pub fn decode(
bytes: impl AsRef<[u8]>,
lua: &Lua,
config: EncodeDecodeConfig,
) -> LuaResult<LuaValue> {
let bytes = bytes.as_ref();
match config.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::from_utf8(bytes.to_vec()) {
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(),
))
}
}
}
}

View file

@ -1,260 +0,0 @@
use std::fmt::Write;
use bstr::BString;
use md5::Md5;
use mlua::prelude::*;
use blake3::Hasher as Blake3;
use sha1::Sha1;
use sha2::{Sha224, Sha256, Sha384, Sha512};
use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512};
pub struct HashOptions {
algorithm: HashAlgorithm,
message: BString,
secret: Option<BString>,
// seed: Option<BString>,
}
#[derive(Debug, Clone, Copy)]
enum HashAlgorithm {
Md5,
Sha1,
// SHA-2 variants
Sha2_224,
Sha2_256,
Sha2_384,
Sha2_512,
// SHA-3 variants
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
// Blake3
Blake3,
}
impl HashAlgorithm {
pub const ALL: [Self; 11] = [
Self::Md5,
Self::Sha1,
Self::Sha2_224,
Self::Sha2_256,
Self::Sha2_384,
Self::Sha2_512,
Self::Sha3_224,
Self::Sha3_256,
Self::Sha3_384,
Self::Sha3_512,
Self::Blake3,
];
pub const fn name(self) -> &'static str {
match self {
Self::Md5 => "md5",
Self::Sha1 => "sha1",
Self::Sha2_224 => "sha224",
Self::Sha2_256 => "sha256",
Self::Sha2_384 => "sha384",
Self::Sha2_512 => "sha512",
Self::Sha3_224 => "sha3-224",
Self::Sha3_256 => "sha3-256",
Self::Sha3_384 => "sha3-384",
Self::Sha3_512 => "sha3-512",
Self::Blake3 => "blake3",
}
}
}
impl HashOptions {
/**
Computes the hash for the `message` using whatever `algorithm` is
contained within this struct and returns it as a string of hex digits.
*/
#[inline]
#[must_use = "hashing a message is useless without using the resulting hash"]
pub fn hash(self) -> String {
use digest::Digest;
let message = self.message;
let bytes = match self.algorithm {
HashAlgorithm::Md5 => Md5::digest(message).to_vec(),
HashAlgorithm::Sha1 => Sha1::digest(message).to_vec(),
HashAlgorithm::Sha2_224 => Sha224::digest(message).to_vec(),
HashAlgorithm::Sha2_256 => Sha256::digest(message).to_vec(),
HashAlgorithm::Sha2_384 => Sha384::digest(message).to_vec(),
HashAlgorithm::Sha2_512 => Sha512::digest(message).to_vec(),
HashAlgorithm::Sha3_224 => Sha3_224::digest(message).to_vec(),
HashAlgorithm::Sha3_256 => Sha3_256::digest(message).to_vec(),
HashAlgorithm::Sha3_384 => Sha3_384::digest(message).to_vec(),
HashAlgorithm::Sha3_512 => Sha3_512::digest(message).to_vec(),
HashAlgorithm::Blake3 => Blake3::digest(message).to_vec(),
};
// We don't want to return raw binary data generally, since that's not
// what most people want a hash for. So we have to make a hex string.
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
/**
Computes the HMAC for the `message` using whatever `algorithm` and
`secret` are contained within this struct. The computed value is
returned as a string of hex digits.
# Errors
If the `secret` is not provided or is otherwise invalid.
*/
#[inline]
pub fn hmac(self) -> LuaResult<String> {
use hmac::{Hmac, Mac, SimpleHmac};
let secret = self
.secret
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #3 missing or nil".to_string()),
})?;
/*
These macros exist to remove what would ultimately be dozens of
repeating lines. Essentially, there's several step to processing
HMacs, which expands into the 3 lines you see below. However,
the Hmac struct is specialized towards eager block-based processes.
In order to support anything else, like blake3, there's a second
type named `SimpleHmac`. This results in duplicate macros like
there are below.
*/
macro_rules! hmac {
($Type:ty) => {{
let mut mac: Hmac<$Type> = Hmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
macro_rules! hmac_no_blocks {
($Type:ty) => {{
let mut mac: SimpleHmac<$Type> =
SimpleHmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
let bytes = match self.algorithm {
HashAlgorithm::Md5 => hmac!(Md5),
HashAlgorithm::Sha1 => hmac!(Sha1),
HashAlgorithm::Sha2_224 => hmac!(Sha224),
HashAlgorithm::Sha2_256 => hmac!(Sha256),
HashAlgorithm::Sha2_384 => hmac!(Sha384),
HashAlgorithm::Sha2_512 => hmac!(Sha512),
HashAlgorithm::Sha3_224 => hmac!(Sha3_224),
HashAlgorithm::Sha3_256 => hmac!(Sha3_256),
HashAlgorithm::Sha3_384 => hmac!(Sha3_384),
HashAlgorithm::Sha3_512 => hmac!(Sha3_512),
HashAlgorithm::Blake3 => hmac_no_blocks!(Blake3),
};
Ok(bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
}))
}
}
impl<'lua> FromLua<'lua> for HashAlgorithm {
fn from_lua(value: LuaValue<'lua>, _lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(str) = value {
/*
Casing tends to vary for algorithms, so rather than force
people to remember it we'll just accept any casing.
*/
let str = str.to_str()?.to_ascii_lowercase();
match str.as_str() {
"md5" => Ok(Self::Md5),
"sha1" => Ok(Self::Sha1),
"sha2-224" | "sha2_224" | "sha224" => Ok(Self::Sha2_224),
"sha2-256" | "sha2_256" | "sha256" => Ok(Self::Sha2_256),
"sha2-384" | "sha2_384" | "sha384" => Ok(Self::Sha2_384),
"sha2-512" | "sha2_512" | "sha512" => Ok(Self::Sha2_512),
"sha3-224" | "sha3_224" => Ok(Self::Sha3_224),
"sha3-256" | "sha3_256" => Ok(Self::Sha3_256),
"sha3-384" | "sha3_384" => Ok(Self::Sha3_384),
"sha3-512" | "sha3_512" => Ok(Self::Sha3_512),
"blake3" => Ok(Self::Blake3),
_ => Err(LuaError::FromLuaConversionError {
from: "string",
to: "HashAlgorithm",
message: Some(format!(
"Invalid hashing algorithm '{str}', valid kinds are:\n{}",
HashAlgorithm::ALL
.into_iter()
.map(HashAlgorithm::name)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "HashAlgorithm",
message: None,
})
}
}
}
impl<'lua> FromLuaMulti<'lua> for HashOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let algorithm = values
.pop_front()
.map(|value| HashAlgorithm::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "HashAlgorithm",
message: Some("Argument #1 missing or nil".to_string()),
})?;
let message = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #2 missing or nil".to_string()),
})?;
let secret = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?;
// let seed = values
// .pop_front()
// .map(|value| BString::from_lua(value, lua))
// .transpose()?;
Ok(HashOptions {
algorithm,
message,
secret,
// seed,
})
}
}

View file

@ -1,69 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use bstr::BString;
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod compress_decompress;
mod encode_decode;
mod hash;
pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
pub use self::hash::HashOptions;
/**
Creates the `serde` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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)?
.with_function("hash", hash_message)?
.with_function("hmac", hmac_message)?
.build_readonly()
}
fn serde_encode<'lua>(
lua: &'lua Lua,
(format, value, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
encode(value, lua, config)
}
fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaResult<LuaValue> {
let config = EncodeDecodeConfig::from(format);
decode(bs, lua, config)
}
async fn serde_compress(
lua: &Lua,
(format, bs, level): (CompressDecompressFormat, BString, Option<i32>),
) -> LuaResult<LuaString> {
let bytes = compress(bs, format, level).await?;
lua.create_string(bytes)
}
async fn serde_decompress(
lua: &Lua,
(format, bs): (CompressDecompressFormat, BString),
) -> LuaResult<LuaString> {
let bytes = decompress(bs, format).await?;
lua.create_string(bytes)
}
fn hash_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hash())
}
fn hmac_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hmac()?)
}

View file

@ -1,25 +0,0 @@
[package]
name = "lune-std-stdio"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Stdio"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
dialoguer = "0.11"
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,85 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use tokio::io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt};
use lune_utils::TableBuilder;
mod prompt;
mod style_and_color;
use self::prompt::{prompt, PromptOptions, PromptResult};
use self::style_and_color::{ColorKind, StyleKind};
const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
.with_max_depth(4)
.with_colors_enabled(false);
/**
Creates the `stdio` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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: &Lua, color: ColorKind) -> LuaResult<LuaValue> {
color.ansi_escape_sequence().into_lua(lua)
}
fn stdio_style(lua: &Lua, style: StyleKind) -> LuaResult<LuaValue> {
style.ansi_escape_sequence().into_lua(lua)
}
fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
Ok(pretty_format_multi_value(&args, &FORMAT_CONFIG))
}
async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stdout = stdout();
stdout.write_all(s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
}
async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stderr = 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<LuaString> {
let mut input = Vec::new();
let mut stdin = stdin();
stdin.read_to_end(&mut input).await?;
lua.create_string(&input)
}
async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
lua.spawn_blocking(move || prompt(options))
.await
.into_lua_err()
}

View file

@ -1,195 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
const ESCAPE_SEQ_RESET: &str = "\x1b[0m";
/**
A color kind supported by the `stdio` standard library.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorKind {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl ColorKind {
pub const ALL: [Self; 9] = [
Self::Reset,
Self::Black,
Self::Red,
Self::Green,
Self::Yellow,
Self::Blue,
Self::Magenta,
Self::Cyan,
Self::White,
];
/**
Returns the human-friendly name of this color kind.
*/
pub fn name(self) -> &'static str {
match self {
Self::Reset => "reset",
Self::Black => "black",
Self::Red => "red",
Self::Green => "green",
Self::Yellow => "yellow",
Self::Blue => "blue",
Self::Magenta => "magenta",
Self::Cyan => "cyan",
Self::White => "white",
}
}
/**
Returns the ANSI escape sequence for the color kind.
*/
pub fn ansi_escape_sequence(self) -> &'static str {
match self {
Self::Reset => ESCAPE_SEQ_RESET,
Self::Black => "\x1b[30m",
Self::Red => "\x1b[31m",
Self::Green => "\x1b[32m",
Self::Yellow => "\x1b[33m",
Self::Blue => "\x1b[34m",
Self::Magenta => "\x1b[35m",
Self::Cyan => "\x1b[36m",
Self::White => "\x1b[37m",
}
}
}
impl FromStr for ColorKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
// NOTE: Previous versions of Lune had this color as "purple" instead
// of "magenta", so we keep this here for backwards compatibility.
"magenta" | "purple" => Self::Magenta,
"cyan" => Self::Cyan,
"white" => Self::White,
_ => return Err(()),
})
}
}
impl FromLua<'_> for ColorKind {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
let s = s.to_str()?;
match s.parse() {
Ok(color) => Ok(color),
Err(()) => Err(LuaError::FromLuaConversionError {
from: "string",
to: "ColorKind",
message: Some(format!(
"Invalid color kind '{s}'\nValid kinds are: {}",
Self::ALL
.iter()
.map(|kind| kind.name())
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ColorKind",
message: None,
})
}
}
}
/**
A style kind supported by the `stdio` standard library.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StyleKind {
Reset,
Bold,
Dim,
}
impl StyleKind {
pub const ALL: [Self; 3] = [Self::Reset, Self::Bold, Self::Dim];
/**
Returns the human-friendly name for this style kind.
*/
pub fn name(self) -> &'static str {
match self {
Self::Reset => "reset",
Self::Bold => "bold",
Self::Dim => "dim",
}
}
/**
Returns the ANSI escape sequence for this style kind.
*/
pub fn ansi_escape_sequence(self) -> &'static str {
match self {
Self::Reset => ESCAPE_SEQ_RESET,
Self::Bold => "\x1b[1m",
Self::Dim => "\x1b[2m",
}
}
}
impl FromStr for StyleKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"reset" => Self::Reset,
"bold" => Self::Bold,
"dim" => Self::Dim,
_ => return Err(()),
})
}
}
impl FromLua<'_> for StyleKind {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
let s = s.to_str()?;
match s.parse() {
Ok(style) => Ok(style),
Err(()) => Err(LuaError::FromLuaConversionError {
from: "string",
to: "StyleKind",
message: Some(format!(
"Invalid style kind '{s}'\nValid kinds are: {}",
Self::ALL
.iter()
.map(|kind| kind.name())
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "StyleKind",
message: None,
})
}
}
}

View file

@ -1,21 +0,0 @@
[package]
name = "lune-std-task"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Task"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = ["time"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,60 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use mlua::prelude::*;
use mlua_luau_scheduler::Functions;
use tokio::time::{sleep, Instant};
use lune_utils::TableBuilder;
/**
Creates the `task` standard library module.
# Errors
Errors when out of memory, or if default Lua globals are missing.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
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()?;
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()
}
const DELAY_IMPL_LUA: &str = r"
return defer(function(...)
wait(select(1, ...))
spawn(select(2, ...))
end, ...)
";
async fn wait(_: &Lua, secs: Option<f64>) -> LuaResult<f64> {
let duration = Duration::from_secs_f64(secs.unwrap_or_default());
let before = Instant::now();
sleep(duration).await;
let after = Instant::now();
Ok((after - before).as_secs_f64())
}

View file

@ -1,59 +0,0 @@
[package]
name = "lune-std"
version = "0.1.5"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[features]
default = [
"datetime",
"fs",
"luau",
"net",
"process",
"regex",
"roblox",
"serde",
"stdio",
"task",
]
datetime = ["dep:lune-std-datetime"]
fs = ["dep:lune-std-fs"]
luau = ["dep:lune-std-luau"]
net = ["dep:lune-std-net"]
process = ["dep:lune-std-process"]
regex = ["dep:lune-std-regex"]
roblox = ["dep:lune-std-roblox"]
serde = ["dep:lune-std-serde"]
stdio = ["dep:lune-std-stdio"]
task = ["dep:lune-std-task"]
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", default-features = false, features = ["fs", "sync"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { optional = true, version = "0.1.3", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.1.2", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.1.2", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }

View file

@ -1,92 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
/**
A standard global provided by Lune.
*/
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LuneStandardGlobal {
GTable,
Print,
Require,
Version,
Warn,
}
impl LuneStandardGlobal {
/**
All available standard globals.
*/
pub const ALL: &'static [Self] = &[
Self::GTable,
Self::Print,
Self::Require,
Self::Version,
Self::Warn,
];
/**
Gets the name of the global, such as `_G` or `require`.
*/
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::GTable => "_G",
Self::Print => "print",
Self::Require => "require",
Self::Version => "_VERSION",
Self::Warn => "warn",
}
}
/**
Creates the Lua value for the global.
# Errors
If the global could not be created.
*/
#[rustfmt::skip]
#[allow(unreachable_patterns)]
pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let res = match self {
Self::GTable => crate::globals::g_table::create(lua),
Self::Print => crate::globals::print::create(lua),
Self::Require => crate::globals::require::create(lua),
Self::Version => crate::globals::version::create(lua),
Self::Warn => crate::globals::warn::create(lua),
};
match res {
Ok(v) => Ok(v),
Err(e) => Err(e.context(format!(
"Failed to create standard global '{}'",
self.name()
))),
}
}
}
impl FromStr for LuneStandardGlobal {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let low = s.trim().to_ascii_lowercase();
Ok(match low.as_str() {
"_g" => Self::GTable,
"print" => Self::Print,
"require" => Self::Require,
"_version" => Self::Version,
"warn" => Self::Warn,
_ => {
return Err(format!(
"Unknown standard global '{low}'\nValid globals are: {}",
Self::ALL
.iter()
.map(Self::name)
.collect::<Vec<_>>()
.join(", ")
))
}
})
}
}

View file

@ -1,5 +0,0 @@
use mlua::prelude::*;
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
lua.create_table()?.into_lua(lua)
}

View file

@ -1,5 +0,0 @@
pub mod g_table;
pub mod print;
pub mod require;
pub mod version;
pub mod warn;

View file

@ -1,19 +0,0 @@
use std::io::Write;
use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
use mlua::prelude::*;
const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
.with_max_depth(4)
.with_colors_enabled(true);
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
let f = lua.create_function(|_, args: LuaMultiValue| {
let formatted = format!("{}\n", pretty_format_multi_value(&args, &FORMAT_CONFIG));
let mut stdout = std::io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
})?;
f.into_lua(lua)
}

View file

@ -1,14 +0,0 @@
use mlua::prelude::*;
use super::context::*;
pub(super) fn require<'lua, 'ctx>(
lua: &'lua Lua,
ctx: &'ctx RequireContext,
name: &str,
) -> LuaResult<LuaMultiValue<'lua>>
where
'lua: 'ctx,
{
ctx.load_library(lua, name)
}

View file

@ -1,35 +0,0 @@
use mlua::prelude::*;
use lune_utils::get_version_string;
struct Version(String);
impl LuaUserData for Version {}
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
let v = match lua.app_data_ref::<Version>() {
Some(v) => v.0.to_string(),
None => env!("CARGO_PKG_VERSION").to_string(),
};
let s = get_version_string(v);
lua.create_string(s)?.into_lua(lua)
}
/**
Overrides the version string to be used by the `_VERSION` global.
The global will be a string in the format `Lune x.y.z+luau`,
where `x.y.z` is the string passed to this function.
The version string passed should be the version of the Lune runtime,
obtained from `env!("CARGO_PKG_VERSION")` or a similar mechanism.
# Panics
Panics if the version string is empty or contains invalid characters.
*/
pub fn set_global_version(lua: &Lua, version: impl Into<String>) {
let v = version.into();
let _ = get_version_string(&v); // Validate version string
lua.set_app_data(Version(v));
}

View file

@ -1,23 +0,0 @@
use std::io::Write;
use lune_utils::fmt::{pretty_format_multi_value, Label, ValueFormatConfig};
use mlua::prelude::*;
const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
.with_max_depth(4)
.with_colors_enabled(true);
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
let f = lua.create_function(|_, args: LuaMultiValue| {
let formatted = format!(
"{}\n{}\n",
Label::Warn,
pretty_format_multi_value(&args, &FORMAT_CONFIG)
);
let mut stdout = std::io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
})?;
f.into_lua(lua)
}

View file

@ -1,29 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
mod global;
mod globals;
mod library;
mod luaurc;
pub use self::global::LuneStandardGlobal;
pub use self::globals::version::set_global_version;
pub use self::library::LuneStandardLibrary;
/**
Injects all standard globals into the given Lua state / VM.
This includes all enabled standard libraries, which can
be used from Lua with `require("@lune/library-name")`.
# Errors
Errors when out of memory, or if *default* Lua globals are missing.
*/
pub fn inject_globals(lua: &Lua) -> LuaResult<()> {
for global in LuneStandardGlobal::ALL {
lua.globals().set(global.name(), global.create(lua)?)?;
}
Ok(())
}

View file

@ -1,127 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
/**
A standard library provided by Lune.
*/
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[rustfmt::skip]
pub enum LuneStandardLibrary {
#[cfg(feature = "datetime")] DateTime,
#[cfg(feature = "fs")] Fs,
#[cfg(feature = "luau")] Luau,
#[cfg(feature = "net")] Net,
#[cfg(feature = "task")] Task,
#[cfg(feature = "process")] Process,
#[cfg(feature = "regex")] Regex,
#[cfg(feature = "serde")] Serde,
#[cfg(feature = "stdio")] Stdio,
#[cfg(feature = "roblox")] Roblox,
}
impl LuneStandardLibrary {
/**
All available standard libraries.
*/
#[rustfmt::skip]
pub const ALL: &'static [Self] = &[
#[cfg(feature = "datetime")] Self::DateTime,
#[cfg(feature = "fs")] Self::Fs,
#[cfg(feature = "luau")] Self::Luau,
#[cfg(feature = "net")] Self::Net,
#[cfg(feature = "task")] Self::Task,
#[cfg(feature = "process")] Self::Process,
#[cfg(feature = "regex")] Self::Regex,
#[cfg(feature = "serde")] Self::Serde,
#[cfg(feature = "stdio")] Self::Stdio,
#[cfg(feature = "roblox")] Self::Roblox,
];
/**
Gets the name of the library, such as `datetime` or `fs`.
*/
#[must_use]
#[rustfmt::skip]
#[allow(unreachable_patterns)]
pub fn name(&self) -> &'static str {
match self {
#[cfg(feature = "datetime")] Self::DateTime => "datetime",
#[cfg(feature = "fs")] Self::Fs => "fs",
#[cfg(feature = "luau")] Self::Luau => "luau",
#[cfg(feature = "net")] Self::Net => "net",
#[cfg(feature = "task")] Self::Task => "task",
#[cfg(feature = "process")] Self::Process => "process",
#[cfg(feature = "regex")] Self::Regex => "regex",
#[cfg(feature = "serde")] Self::Serde => "serde",
#[cfg(feature = "stdio")] Self::Stdio => "stdio",
#[cfg(feature = "roblox")] Self::Roblox => "roblox",
_ => unreachable!("no standard library enabled"),
}
}
/**
Creates the Lua module for the library.
# Errors
If the library could not be created.
*/
#[rustfmt::skip]
#[allow(unreachable_patterns)]
pub fn module<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
let res: LuaResult<LuaTable> = match self {
#[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::module(lua),
#[cfg(feature = "fs")] Self::Fs => lune_std_fs::module(lua),
#[cfg(feature = "luau")] Self::Luau => lune_std_luau::module(lua),
#[cfg(feature = "net")] Self::Net => lune_std_net::module(lua),
#[cfg(feature = "task")] Self::Task => lune_std_task::module(lua),
#[cfg(feature = "process")] Self::Process => lune_std_process::module(lua),
#[cfg(feature = "regex")] Self::Regex => lune_std_regex::module(lua),
#[cfg(feature = "serde")] Self::Serde => lune_std_serde::module(lua),
#[cfg(feature = "stdio")] Self::Stdio => lune_std_stdio::module(lua),
#[cfg(feature = "roblox")] Self::Roblox => lune_std_roblox::module(lua),
_ => unreachable!("no standard library enabled"),
};
match res {
Ok(v) => v.into_lua_multi(lua),
Err(e) => Err(e.context(format!(
"Failed to create standard library '{}'",
self.name()
))),
}
}
}
impl FromStr for LuneStandardLibrary {
type Err = String;
#[rustfmt::skip]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let low = s.trim().to_ascii_lowercase();
Ok(match low.as_str() {
#[cfg(feature = "datetime")] "datetime" => Self::DateTime,
#[cfg(feature = "fs")] "fs" => Self::Fs,
#[cfg(feature = "luau")] "luau" => Self::Luau,
#[cfg(feature = "net")] "net" => Self::Net,
#[cfg(feature = "task")] "task" => Self::Task,
#[cfg(feature = "process")] "process" => Self::Process,
#[cfg(feature = "regex")] "regex" => Self::Regex,
#[cfg(feature = "serde")] "serde" => Self::Serde,
#[cfg(feature = "stdio")] "stdio" => Self::Stdio,
#[cfg(feature = "roblox")] "roblox" => Self::Roblox,
_ => {
return Err(format!(
"Unknown standard library '{low}'\nValid libraries are: {}",
Self::ALL
.iter()
.map(Self::name)
.collect::<Vec<_>>()
.join(", ")
))
}
})
}
}

View file

@ -1,26 +0,0 @@
[package]
name = "lune-utils"
version = "0.1.3"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Utilities library for Lune"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "async"] }
tokio = { version = "1", default-features = false, features = ["fs"] }
console = "0.15"
dunce = "1.0"
once_cell = "1.17"
path-clean = "1.0"
pathdiff = "0.2"
parking_lot = "0.12.3"
semver = "1.0"

View file

@ -1,196 +0,0 @@
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use console::style;
use mlua::prelude::*;
use once_cell::sync::Lazy;
use super::StackTrace;
static STYLED_STACK_BEGIN: Lazy<String> = Lazy::new(|| {
format!(
"{}{}{}",
style("[").dim(),
style("Stack Begin").blue(),
style("]").dim()
)
});
static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
format!(
"{}{}{}",
style("[").dim(),
style("Stack End").blue(),
style("]").dim()
)
});
// NOTE: We indent using 4 spaces instead of tabs since
// these errors are most likely to be displayed in a terminal
// or some kind of live output - and tabs don't work well there
const STACK_TRACE_INDENT: &str = " ";
/**
Error components parsed from a [`LuaError`].
Can be used to display a human-friendly error message
and stack trace, in the following Roblox-inspired format:
```plaintext
Error message
[Stack Begin]
Stack trace line
Stack trace line
Stack trace line
[Stack End]
```
*/
#[derive(Debug, Default, Clone)]
pub struct ErrorComponents {
messages: Vec<String>,
trace: Option<StackTrace>,
}
impl ErrorComponents {
/**
Returns the error messages.
*/
#[must_use]
pub fn messages(&self) -> &[String] {
&self.messages
}
/**
Returns the stack trace, if it exists.
*/
#[must_use]
pub fn trace(&self) -> Option<&StackTrace> {
self.trace.as_ref()
}
/**
Returns `true` if the error has a non-empty stack trace.
Note that a trace may still *exist*, but it may be empty.
*/
#[must_use]
pub fn has_trace(&self) -> bool {
self.trace
.as_ref()
.is_some_and(|trace| !trace.lines().is_empty())
}
}
impl fmt::Display for ErrorComponents {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for message in self.messages() {
writeln!(f, "{message}")?;
}
if self.has_trace() {
let trace = self.trace.as_ref().unwrap();
writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
for line in trace.lines() {
writeln!(f, "{STACK_TRACE_INDENT}{line}")?;
}
writeln!(f, "{}", *STYLED_STACK_END)?;
}
Ok(())
}
}
impl From<LuaError> for ErrorComponents {
fn from(error: LuaError) -> Self {
fn lua_error_message(e: &LuaError) -> String {
if let LuaError::RuntimeError(s) = e {
s.to_string()
} else {
e.to_string()
}
}
fn lua_stack_trace(source: &str) -> Option<StackTrace> {
// FUTURE: Preserve a parsing error here somehow?
// Maybe we can emit parsing errors using tracing?
StackTrace::from_str(source).ok()
}
// Extract any additional "context" messages before the actual error(s)
// The Arc is necessary here because mlua wraps all inner errors in an Arc
let mut error = Arc::new(error);
let mut messages = Vec::new();
while let LuaError::WithContext {
ref context,
ref cause,
} = *error
{
messages.push(context.to_string());
error = cause.clone();
}
// We will then try to extract any stack trace
let mut trace = if let LuaError::CallbackError {
ref traceback,
ref cause,
} = *error
{
messages.push(lua_error_message(cause));
lua_stack_trace(traceback)
} else if let LuaError::RuntimeError(ref s) = *error {
// NOTE: Runtime errors may include tracebacks, but they're
// joined with error messages, so we need to split them out
if let Some(pos) = s.find("stack traceback:") {
let (message, traceback) = s.split_at(pos);
messages.push(message.trim().to_string());
lua_stack_trace(traceback)
} else {
messages.push(s.to_string());
None
}
} else {
messages.push(lua_error_message(&error));
None
};
// Sometimes, we can get duplicate stack trace lines that only
// mention "[C]", without a function name or path, and these can
// be safely ignored / removed if the following line has more info
if let Some(trace) = &mut trace {
let lines = trace.lines_mut();
loop {
let first_is_c_and_empty = lines
.first()
.is_some_and(|line| line.source().is_c() && line.is_empty());
let second_is_c_and_nonempty = lines
.get(1)
.is_some_and(|line| line.source().is_c() && !line.is_empty());
if first_is_c_and_empty && second_is_c_and_nonempty {
lines.remove(0);
} else {
break;
}
}
}
// Finally, we do some light postprocessing to remove duplicate
// information, such as the location prefix in the error message
if let Some(message) = messages.last_mut() {
if let Some(line) = trace
.iter()
.flat_map(StackTrace::lines)
.find(|line| line.source().is_lua())
{
let location_prefix = format!(
"[string \"{}\"]:{}:",
line.path().unwrap(),
line.line_number().unwrap()
);
if message.starts_with(&location_prefix) {
*message = message[location_prefix.len()..].trim().to_string();
}
}
}
ErrorComponents { messages, trace }
}
}

View file

@ -1,8 +0,0 @@
mod components;
mod stack_trace;
#[cfg(test)]
mod tests;
pub use self::components::ErrorComponents;
pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource};

View file

@ -1,210 +0,0 @@
use std::fmt;
use std::str::FromStr;
fn parse_path(s: &str) -> Option<(&str, &str)> {
let path = s.strip_prefix("[string \"")?;
let (path, after) = path.split_once("\"]:")?;
// Remove line number after any found colon, this may
// exist if the source path is from a rust source file
let path = match path.split_once(':') {
Some((before, _)) => before,
None => path,
};
Some((path, after))
}
fn parse_function_name(s: &str) -> Option<&str> {
s.strip_prefix("in function '")
.and_then(|s| s.strip_suffix('\''))
}
fn parse_line_number(s: &str) -> (Option<usize>, &str) {
match s.split_once(':') {
Some((before, after)) => (before.parse::<usize>().ok(), after),
None => (None, s),
}
}
/**
Source of a stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone, Copy)]
pub enum StackTraceSource {
/// Error originated from a C / Rust function.
C,
/// Error originated from a Lua (user) function.
#[default]
Lua,
}
impl StackTraceSource {
/**
Returns `true` if the error originated from a C / Rust function, `false` otherwise.
*/
#[must_use]
pub const fn is_c(self) -> bool {
matches!(self, Self::C)
}
/**
Returns `true` if the error originated from a Lua (user) function, `false` otherwise.
*/
#[must_use]
pub const fn is_lua(self) -> bool {
matches!(self, Self::Lua)
}
}
/**
Stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTraceLine {
source: StackTraceSource,
path: Option<String>,
line_number: Option<usize>,
function_name: Option<String>,
}
impl StackTraceLine {
/**
Returns the source of the stack trace line.
*/
#[must_use]
pub fn source(&self) -> StackTraceSource {
self.source
}
/**
Returns the path, if it exists.
*/
#[must_use]
pub fn path(&self) -> Option<&str> {
self.path.as_deref()
}
/**
Returns the line number, if it exists.
*/
#[must_use]
pub fn line_number(&self) -> Option<usize> {
self.line_number
}
/**
Returns the function name, if it exists.
*/
#[must_use]
pub fn function_name(&self) -> Option<&str> {
self.function_name.as_deref()
}
/**
Returns `true` if the stack trace line contains no "useful" information, `false` otherwise.
Useful information is determined as one of:
- A path
- A line number
- A function name
*/
#[must_use]
pub const fn is_empty(&self) -> bool {
self.path.is_none() && self.line_number.is_none() && self.function_name.is_none()
}
}
impl FromStr for StackTraceLine {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(after) = s.strip_prefix("[C]: ") {
let function_name = parse_function_name(after).map(ToString::to_string);
Ok(Self {
source: StackTraceSource::C,
path: None,
line_number: None,
function_name,
})
} else if let Some((path, after)) = parse_path(s) {
let (line_number, after) = parse_line_number(after);
let function_name = parse_function_name(after).map(ToString::to_string);
Ok(Self {
source: StackTraceSource::Lua,
path: Some(path.to_string()),
line_number,
function_name,
})
} else {
Err(String::from("unknown format"))
}
}
}
impl fmt::Display for StackTraceLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if matches!(self.source, StackTraceSource::C) {
write!(f, "Script '[C]'")?;
} else {
write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?;
if let Some(line_number) = self.line_number {
write!(f, ", Line {line_number}")?;
}
}
if let Some(function_name) = self.function_name.as_deref() {
write!(f, " - function '{function_name}'")?;
}
Ok(())
}
}
/**
Stack trace parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTrace {
lines: Vec<StackTraceLine>,
}
impl StackTrace {
/**
Returns the individual stack trace lines.
*/
#[must_use]
pub fn lines(&self) -> &[StackTraceLine] {
&self.lines
}
/**
Returns the individual stack trace lines, mutably.
*/
#[must_use]
pub fn lines_mut(&mut self) -> &mut Vec<StackTraceLine> {
&mut self.lines
}
}
impl FromStr for StackTrace {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, after) = s
.split_once("stack traceback:")
.ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?;
let lines = after
.trim()
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
None
} else {
Some(line.parse())
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(StackTrace { lines })
}
}

View file

@ -1,150 +0,0 @@
use mlua::prelude::*;
use crate::fmt::ErrorComponents;
fn new_lua_runtime_error() -> LuaResult<()> {
let lua = Lua::new();
lua.globals()
.set(
"f",
LuaFunction::wrap(|_, (): ()| {
Err::<(), _>(LuaError::runtime("oh no, a runtime error"))
}),
)
.unwrap();
lua.load("f()").set_name("chunk_name").eval()
}
fn new_lua_script_error() -> LuaResult<()> {
let lua = Lua::new();
lua.load(
"local function inner()\
\n error(\"oh no, a script error\")\
\nend\
\n\
\nlocal function outer()\
\n inner()\
\nend\
\n\
\nouter()\
",
)
.set_name("chunk_name")
.eval()
}
// Tests for error context stack
mod context {
use super::*;
#[test]
fn preserves_original() {
let lua_error = new_lua_runtime_error()
.context("additional context")
.unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "additional context");
assert_eq!(components.messages()[1], "oh no, a runtime error");
}
#[test]
fn preserves_levels() {
// NOTE: The behavior in mlua is to preserve a single level of context
// and not all levels (context gets replaced on each call to `context`)
let lua_error = new_lua_runtime_error()
.context("level 1")
.context("level 2")
.context("level 3")
.unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(
components.messages(),
&["level 3", "oh no, a runtime error"]
);
}
}
// Tests for error components struct: separated messages + stack trace
mod error_components {
use super::*;
#[test]
fn message() {
let lua_error = new_lua_runtime_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "oh no, a runtime error");
}
#[test]
fn stack_begin_end() {
let lua_error = new_lua_runtime_error().unwrap_err();
let formatted = format!("{}", ErrorComponents::from(lua_error));
assert!(formatted.contains("Stack Begin"));
assert!(formatted.contains("Stack End"));
}
#[test]
fn stack_lines() {
let lua_error = new_lua_runtime_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let mut lines = components.trace().unwrap().lines().iter();
let line_1 = lines.next().unwrap().to_string();
let line_2 = lines.next().unwrap().to_string();
assert!(lines.next().is_none());
assert_eq!(line_1, "Script '[C]' - function 'f'");
assert_eq!(line_2, "Script 'chunk_name', Line 1");
}
}
// Tests for general formatting
mod general {
use super::*;
#[test]
fn message_does_not_contain_location() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let first_message = components.messages().first().unwrap();
let first_lua_stack_line = trace
.lines()
.iter()
.find(|line| line.source().is_lua())
.unwrap();
let location_prefix = format!(
"[string \"{}\"]:{}:",
first_lua_stack_line.path().unwrap(),
first_lua_stack_line.line_number().unwrap()
);
assert!(!first_message.starts_with(&location_prefix));
}
#[test]
fn no_redundant_c_mentions() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let c_stack_lines = trace
.lines()
.iter()
.filter(|line| line.source().is_c())
.collect::<Vec<_>>();
assert_eq!(c_stack_lines.len(), 1); // Just the "error" call
}
}

View file

@ -1,66 +0,0 @@
use std::fmt;
use console::{style, Color};
/**
Label enum used for consistent output formatting throughout Lune.
# Example usage
```rs
use lune_utils::fmt::Label;
println!("{} This is an info message", Label::Info);
// [INFO] This is an info message
println!("{} This is a warning message", Label::Warn);
// [WARN] This is a warning message
println!("{} This is an error message", Label::Error);
// [ERROR] This is an error message
```
*/
#[derive(Debug, Clone, Copy)]
pub enum Label {
Info,
Warn,
Error,
}
impl Label {
/**
Returns the name of the label in all uppercase.
*/
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
/**
Returns the color of the label.
*/
#[must_use]
pub fn color(&self) -> Color {
match self {
Self::Info => Color::Blue,
Self::Warn => Color::Yellow,
Self::Error => Color::Red,
}
}
}
impl fmt::Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}",
style("[").dim(),
style(self.name()).fg(self.color()),
style("]").dim()
)
}
}

View file

@ -1,7 +0,0 @@
mod error;
mod label;
mod value;
pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource};
pub use self::label::Label;
pub use self::value::{pretty_format_multi_value, pretty_format_value, ValueFormatConfig};

View file

@ -1,99 +0,0 @@
use mlua::prelude::*;
use crate::fmt::ErrorComponents;
use super::{
metamethods::{
call_table_tostring_metamethod, call_userdata_tostring_metamethod,
get_table_type_metavalue, get_userdata_type_metavalue,
},
style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW},
};
const STRING_REPLACEMENTS: &[(&str, &str)] =
&[("\"", r#"\""#), ("\t", r"\t"), ("\r", r"\r"), ("\n", r"\n")];
/**
Tries to return the given value as a plain string key.
A plain string key must:
- Start with an alphabetic character.
- Only contain alphanumeric characters and underscores.
*/
pub(crate) fn lua_value_as_plain_string_key(value: &LuaValue) -> Option<String> {
if let LuaValue::String(s) = value {
if let Ok(s) = s.to_str() {
let first_valid = s.chars().next().is_some_and(|c| c.is_ascii_alphabetic());
let all_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
if first_valid && all_valid {
return Some(s.to_string());
}
}
}
None
}
/**
Formats a Lua value into a pretty string.
This does not recursively format tables.
*/
pub(crate) fn format_value_styled(value: &LuaValue, prefer_plain: bool) -> String {
match value {
LuaValue::Nil => COLOR_YELLOW.apply_to("nil").to_string(),
LuaValue::Boolean(true) => COLOR_YELLOW.apply_to("true").to_string(),
LuaValue::Boolean(false) => COLOR_YELLOW.apply_to("false").to_string(),
LuaValue::Number(n) => COLOR_CYAN.apply_to(n).to_string(),
LuaValue::Integer(i) => COLOR_CYAN.apply_to(i).to_string(),
LuaValue::String(s) if prefer_plain => s.to_string_lossy().to_string(),
LuaValue::String(s) => COLOR_GREEN
.apply_to({
let mut s = s.to_string_lossy().to_string();
for (from, to) in STRING_REPLACEMENTS {
s = s.replace(from, to);
}
format!(r#""{s}""#)
})
.to_string(),
LuaValue::Vector(_) => COLOR_MAGENTA.apply_to("<vector>").to_string(),
LuaValue::Thread(_) => COLOR_MAGENTA.apply_to("<thread>").to_string(),
LuaValue::Function(_) => COLOR_MAGENTA.apply_to("<function>").to_string(),
LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(),
LuaValue::UserData(u) => {
let formatted = format_typename_and_tostringed(
"userdata",
get_userdata_type_metavalue(u),
call_userdata_tostring_metamethod(u),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
}
LuaValue::Table(t) => {
let formatted = format_typename_and_tostringed(
"table",
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
}
LuaValue::Error(e) => COLOR_MAGENTA
.apply_to(format!(
"<LuaError(\n{})>",
ErrorComponents::from(e.clone())
))
.to_string(),
}
}
fn format_typename_and_tostringed(
fallback: &'static str,
typename: Option<String>,
tostringed: Option<String>,
) -> String {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => format!("<{typename}({tostringed})>"),
(Some(typename), None) => format!("<{typename}>"),
(None, Some(tostringed)) => format!("<{tostringed}>"),
(None, None) => format!("<{fallback}>"),
}
}

View file

@ -1,48 +0,0 @@
/**
Configuration for formatting values.
*/
#[derive(Debug, Clone, Copy)]
pub struct ValueFormatConfig {
pub(super) max_depth: usize,
pub(super) colors_enabled: bool,
}
impl ValueFormatConfig {
/**
Creates a new config with default values.
*/
#[must_use]
pub const fn new() -> Self {
Self {
max_depth: 3,
colors_enabled: false,
}
}
/**
Sets the maximum depth to which tables will be formatted.
*/
#[must_use]
pub const fn with_max_depth(self, max_depth: usize) -> Self {
Self { max_depth, ..self }
}
/**
Sets whether colors should be enabled.
Colors are disabled by default.
*/
#[must_use]
pub const fn with_colors_enabled(self, colors_enabled: bool) -> Self {
Self {
colors_enabled,
..self
}
}
}
impl Default for ValueFormatConfig {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,37 +0,0 @@
use mlua::prelude::*;
pub fn get_table_type_metavalue<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let s = tab
.get_metatable()?
.get::<_, LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn get_userdata_type_metavalue<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let s = tab
.get_metatable()
.ok()?
.get::<LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
tab.get_metatable()?
.get::<_, LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
}
pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
tab.get_metatable()
.ok()?
.get::<LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
}

View file

@ -1,63 +0,0 @@
use std::{collections::HashSet, sync::Arc};
use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use parking_lot::ReentrantMutex;
mod basic;
mod config;
mod metamethods;
mod recursive;
mod style;
use self::recursive::format_value_recursive;
pub use self::config::ValueFormatConfig;
// NOTE: Since the setting for colors being enabled is global,
// and these functions may be called in parallel, we use this global
// lock to make sure that we don't mess up the colors for other threads.
static COLORS_LOCK: Lazy<Arc<ReentrantMutex<()>>> = Lazy::new(|| Arc::new(ReentrantMutex::new(())));
/**
Formats a Lua value into a pretty string using the given config.
*/
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock();
let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled);
let mut visited = HashSet::new();
let res = format_value_recursive(value, config, &mut visited, 0);
set_colors_enabled(were_colors_enabled);
res.expect("using fmt for writing into strings should never fail")
}
/**
Formats a Lua multi-value into a pretty string using the given config.
Each value will be separated by a space.
*/
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock();
let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled);
let mut visited = HashSet::new();
let res = values
.into_iter()
.map(|value| format_value_recursive(value, config, &mut visited, 0))
.collect::<Result<Vec<_>, _>>();
set_colors_enabled(were_colors_enabled);
res.expect("using fmt for writing into strings should never fail")
.join(" ")
}

View file

@ -1,184 +0,0 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt::{self, Write as _};
use mlua::prelude::*;
use super::metamethods::{call_table_tostring_metamethod, get_table_type_metavalue};
use super::{
basic::{format_value_styled, lua_value_as_plain_string_key},
config::ValueFormatConfig,
style::STYLE_DIM,
};
const INDENT: &str = " ";
/**
Representation of a pointer in memory to a Lua value.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct LuaValueId(usize);
impl From<&LuaValue<'_>> for LuaValueId {
fn from(value: &LuaValue<'_>) -> Self {
Self(value.to_pointer() as usize)
}
}
impl From<&LuaTable<'_>> for LuaValueId {
fn from(table: &LuaTable) -> Self {
Self(table.to_pointer() as usize)
}
}
/**
Formats the given value, recursively formatting tables
up to the maximum depth specified in the config.
NOTE: We return a result here but it's really just to make handling
of the `write!` calls easier. Writing into a string should never fail.
*/
pub(crate) fn format_value_recursive(
value: &LuaValue,
config: &ValueFormatConfig,
visited: &mut HashSet<LuaValueId>,
depth: usize,
) -> Result<String, fmt::Error> {
let mut buffer = String::new();
if let LuaValue::Table(ref t) = value {
if let Some(formatted) = format_typename_and_tostringed(
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
) {
write!(buffer, "{formatted}")?;
} else if depth >= config.max_depth {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if !visited.insert(LuaValueId::from(t)) {
write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?;
} else {
write!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
let mut values = t
.clone()
.pairs::<LuaValue, LuaValue>()
.map(|res| res.expect("conversion to LuaValue should never fail"))
.collect::<Vec<_>>();
sort_for_formatting(&mut values);
let is_empty = values.is_empty();
let is_array = values
.iter()
.enumerate()
.all(|(i, (key, _))| key.as_integer().is_some_and(|x| x == (i as i32) + 1));
let formatted_values = if is_array {
format_array(values, config, visited, depth)?
} else {
format_table(values, config, visited, depth)?
};
visited.remove(&LuaValueId::from(t));
if is_empty {
write!(buffer, " {}", STYLE_DIM.apply_to("}"))?;
} else {
write!(
buffer,
"\n{}\n{}{}",
formatted_values.join("\n"),
INDENT.repeat(depth),
STYLE_DIM.apply_to("}")
)?;
}
}
} else {
let prefer_plain = depth == 0;
write!(buffer, "{}", format_value_styled(value, prefer_plain))?;
}
Ok(buffer)
}
fn sort_for_formatting(values: &mut [(LuaValue, LuaValue)]) {
values.sort_by(|(a, _), (b, _)| {
if a.type_name() == b.type_name() {
// If we have the same type, sort either numerically or alphabetically
match (a, b) {
(LuaValue::Integer(a), LuaValue::Integer(b)) => a.cmp(b),
(LuaValue::Number(a), LuaValue::Number(b)) => a.partial_cmp(b).unwrap(),
(LuaValue::String(a), LuaValue::String(b)) => a.to_str().ok().cmp(&b.to_str().ok()),
_ => Ordering::Equal,
}
} else {
// If we have different types, sort numbers first, then strings, then others
a.is_number()
.cmp(&b.is_number())
.then_with(|| a.is_string().cmp(&b.is_string()))
}
});
}
fn format_array(
values: Vec<(LuaValue, LuaValue)>,
config: &ValueFormatConfig,
visited: &mut HashSet<LuaValueId>,
depth: usize,
) -> Result<Vec<String>, fmt::Error> {
values
.into_iter()
.map(|(_, value)| {
Ok(format!(
"{}{}{}",
INDENT.repeat(1 + depth),
format_value_recursive(&value, config, visited, depth + 1)?,
STYLE_DIM.apply_to(","),
))
})
.collect()
}
fn format_table(
values: Vec<(LuaValue, LuaValue)>,
config: &ValueFormatConfig,
visited: &mut HashSet<LuaValueId>,
depth: usize,
) -> Result<Vec<String>, fmt::Error> {
values
.into_iter()
.map(|(key, value)| {
if let Some(plain_key) = lua_value_as_plain_string_key(&key) {
Ok(format!(
"{}{plain_key} {} {}{}",
INDENT.repeat(1 + depth),
STYLE_DIM.apply_to("="),
format_value_recursive(&value, config, visited, depth + 1)?,
STYLE_DIM.apply_to(","),
))
} else {
Ok(format!(
"{}{}{}{} {} {}{}",
INDENT.repeat(1 + depth),
STYLE_DIM.apply_to("["),
format_value_recursive(&key, config, visited, depth + 1)?,
STYLE_DIM.apply_to("]"),
STYLE_DIM.apply_to("="),
format_value_recursive(&value, config, visited, depth + 1)?,
STYLE_DIM.apply_to(","),
))
}
})
.collect()
}
fn format_typename_and_tostringed(
typename: Option<String>,
tostringed: Option<String>,
) -> Option<String> {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => Some(format!("<{typename}({tostringed})>")),
(Some(typename), None) => Some(format!("<{typename}>")),
(None, Some(tostringed)) => Some(tostringed),
(None, None) => None,
}
}

View file

@ -1,9 +0,0 @@
use console::Style;
use once_cell::sync::Lazy;
pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
pub static COLOR_MAGENTA: Lazy<Style> = Lazy::new(|| Style::new().magenta());
pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());

View file

@ -1,30 +0,0 @@
#[derive(Debug, Clone, Copy, Default)]
pub struct JitStatus(bool);
impl JitStatus {
#[must_use]
pub fn new(enabled: bool) -> Self {
Self(enabled)
}
pub fn set_status(&mut self, enabled: bool) {
self.0 = enabled;
}
#[must_use]
pub fn enabled(self) -> bool {
self.0
}
}
impl From<JitStatus> for bool {
fn from(val: JitStatus) -> Self {
val.enabled()
}
}
impl From<bool> for JitStatus {
fn from(val: bool) -> Self {
Self::new(val)
}
}

View file

@ -1,11 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
mod table_builder;
mod version_string;
pub mod fmt;
pub mod jit;
pub mod path;
pub use self::table_builder::TableBuilder;
pub use self::version_string::get_version_string;

View file

@ -1,101 +0,0 @@
use std::{
env::{current_dir, current_exe},
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use once_cell::sync::Lazy;
use path_clean::PathClean;
static CWD: Lazy<Arc<Path>> = Lazy::new(create_cwd);
static EXE: Lazy<Arc<Path>> = Lazy::new(create_exe);
fn create_cwd() -> Arc<Path> {
let mut cwd = current_dir()
.expect("failed to find current working directory")
.to_str()
.expect("current working directory is not valid UTF-8")
.to_string();
if !cwd.ends_with(MAIN_SEPARATOR) {
cwd.push(MAIN_SEPARATOR);
}
dunce::canonicalize(cwd)
.expect("failed to canonicalize current working directory")
.into()
}
fn create_exe() -> Arc<Path> {
let exe = current_exe()
.expect("failed to find current executable")
.to_str()
.expect("current executable is not valid UTF-8")
.to_string();
dunce::canonicalize(exe)
.expect("failed to canonicalize current executable")
.into()
}
/**
Gets the current working directory as an absolute path.
This absolute path is canonicalized and does not contain any `.` or `..`
components, and it is also in a friendly (non-UNC) format.
This path is also guaranteed to:
- Be valid UTF-8.
- End with the platform's main path separator.
*/
#[must_use]
pub fn get_current_dir() -> Arc<Path> {
Arc::clone(&CWD)
}
/**
Gets the path to the current executable as an absolute path.
This absolute path is canonicalized and does not contain any `.` or `..`
components, and it is also in a friendly (non-UNC) format.
This path is also guaranteed to:
- Be valid UTF-8.
*/
#[must_use]
pub fn get_current_exe() -> Arc<Path> {
Arc::clone(&EXE)
}
/**
Diffs two paths against each other.
See the [`pathdiff`] crate for more information on what diffing paths does.
*/
pub fn diff_path(path: impl AsRef<Path>, base: impl AsRef<Path>) -> Option<PathBuf> {
pathdiff::diff_paths(path, base)
}
/**
Cleans a path.
See the [`path_clean`] crate for more information on what cleaning a path does.
*/
pub fn clean_path(path: impl AsRef<Path>) -> PathBuf {
path.as_ref().clean()
}
/**
Makes a path absolute and then cleans it.
Relative paths are resolved against the current working directory.
See the [`path_clean`] crate for more information on what cleaning a path does.
*/
pub fn clean_path_and_make_absolute(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
if path.is_relative() {
CWD.join(path).clean()
} else {
path.clean()
}
}

View file

@ -1,74 +0,0 @@
use std::sync::Arc;
use mlua::prelude::*;
use once_cell::sync::Lazy;
use semver::Version;
static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
/**
Returns a Lune version string, in the format `Lune x.y.z+luau`.
The version string passed should be the version of the Lune runtime,
obtained from `env!("CARGO_PKG_VERSION")` or a similar mechanism.
# Panics
Panics if the version string is empty or contains invalid characters.
*/
#[must_use]
pub fn get_version_string(lune_version: impl AsRef<str>) -> String {
let lune_version = lune_version.as_ref();
assert!(!lune_version.is_empty(), "Lune version string is empty");
match Version::parse(lune_version) {
Ok(semver) => format!("Lune {semver}+{}", *LUAU_VERSION),
Err(e) => panic!("Lune version string is not valid semver: {e}"),
}
}
fn create_luau_version_string() -> Arc<String> {
// Extract the current Luau version from a fresh Lua state / VM that can't be accessed externally.
let luau_version_full = {
let temp_lua = Lua::new();
let luau_version_full = temp_lua
.globals()
.get::<_, LuaString>("_VERSION")
.expect("Missing _VERSION global");
luau_version_full
.to_str()
.context("Invalid utf8 found in _VERSION global")
.expect("Expected _VERSION global to be a string")
.to_string()
};
// Luau version is expected to be in the format "Luau 0.x" and sometimes "Luau 0.x.y"
assert!(
luau_version_full.starts_with("Luau 0."),
"_VERSION global is formatted incorrectly\
\nFound string '{luau_version_full}'"
);
let luau_version_noprefix = luau_version_full.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_noprefix.is_empty() {
panic!(
"_VERSION global is missing version number\
\nFound string '{luau_version_full}'"
)
} else if !luau_version_noprefix.chars().all(is_valid_version_char) {
panic!(
"_VERSION global contains invalid characters\
\nFound string '{luau_version_full}'"
)
}
luau_version_noprefix.to_string().into()
}
fn is_valid_version_char(c: char) -> bool {
matches!(c, '0'..='9' | '.')
}

View file

@ -1,83 +0,0 @@
[package]
name = "lune"
version = "0.8.9"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "A standalone Luau runtime"
readme = "../../README.md"
keywords = ["cli", "lua", "luau", "runtime"]
categories = ["command-line-interface"]
[[bin]]
name = "lune"
path = "src/main.rs"
[lib]
name = "lune"
path = "src/lib.rs"
[features]
default = ["std", "cli"]
std-datetime = ["dep:lune-std", "lune-std/datetime"]
std-fs = ["dep:lune-std", "lune-std/fs"]
std-luau = ["dep:lune-std", "lune-std/luau"]
std-net = ["dep:lune-std", "lune-std/net"]
std-process = ["dep:lune-std", "lune-std/process"]
std-regex = ["dep:lune-std", "lune-std/regex"]
std-roblox = ["dep:lune-std", "lune-std/roblox", "dep:lune-roblox"]
std-serde = ["dep:lune-std", "lune-std/serde"]
std-stdio = ["dep:lune-std", "lune-std/stdio"]
std-task = ["dep:lune-std", "lune-std/task"]
std = [
"std-datetime",
"std-fs",
"std-luau",
"std-net",
"std-process",
"std-regex",
"std-roblox",
"std-serde",
"std-stdio",
"std-task",
]
cli = ["dep:clap", "dep:include_dir", "dep:rustyline", "dep:zip_next"]
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
anyhow = "1.0"
console = "0.15"
dialoguer = "0.11"
directories = "5.0"
futures-util = "0.3"
once_cell = "1.17"
self_cell = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
lune-std = { optional = true, version = "0.1.5", path = "../lune-std" }
lune-roblox = { optional = true, version = "0.1.4", path = "../lune-roblox" }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
### CLI
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" }

View file

@ -1,86 +0,0 @@
use std::{
io::{Cursor, Read},
path::PathBuf,
};
use tokio::{fs, task};
use crate::standalone::metadata::CURRENT_EXE;
use super::{
files::write_executable_file_to,
result::{BuildError, BuildResult},
target::{BuildTarget, CACHE_DIR},
};
/**
Discovers the path to the base executable to use for cross-compilation.
If the target is the same as the current system, the current executable is used.
If no binary exists at the target path, it will attempt to download it from the internet.
*/
pub async fn get_or_download_base_executable(target: BuildTarget) -> BuildResult<PathBuf> {
if target.is_current_system() {
return Ok(CURRENT_EXE.to_path_buf());
}
if target.cache_path().exists() {
return Ok(target.cache_path());
}
// The target is not cached, we must download it
println!("Requested target '{target}' does not exist in cache");
let version = env!("CARGO_PKG_VERSION");
let target_triple = format!("lune-{version}-{target}");
let release_url = format!(
"{base_url}/v{version}/{target_triple}.zip",
base_url = "https://github.com/lune-org/lune/releases/download",
);
// NOTE: This is not entirely accurate, but it is clearer for a user
println!("Downloading {target_triple}{}...", target.exe_suffix());
// Try to request to download the zip file from the target url,
// making sure transient errors are handled gracefully and
// with a different error message than "not found"
let response = reqwest::get(release_url).await?;
if !response.status().is_success() {
if response.status().as_u16() == 404 {
return Err(BuildError::ReleaseTargetNotFound(target));
}
return Err(BuildError::Download(
response.error_for_status().unwrap_err(),
));
}
// Receive the full zip file
let zip_bytes = response.bytes().await?.to_vec();
let zip_file = Cursor::new(zip_bytes);
// Look for and extract the binary file from the zip file
// NOTE: We use spawn_blocking here since reading a zip
// archive is a somewhat slow / blocking operation
let binary_file_name = format!("lune{}", target.exe_suffix());
let binary_file_handle = task::spawn_blocking(move || {
let mut archive = zip_next::ZipArchive::new(zip_file)?;
let mut binary = Vec::new();
archive
.by_name(&binary_file_name)
.or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))?
.read_to_end(&mut binary)?;
Ok::<_, BuildError>(binary)
});
let binary_file_contents = binary_file_handle.await??;
// Finally, write the extracted binary to the cache
if !CACHE_DIR.exists() {
fs::create_dir_all(CACHE_DIR.as_path()).await?;
}
write_executable_file_to(target.cache_path(), binary_file_contents).await?;
println!("Downloaded successfully and added to cache");
Ok(target.cache_path())
}

View file

@ -1,42 +0,0 @@
use std::path::{Path, PathBuf};
use anyhow::Result;
use tokio::{fs, io::AsyncWriteExt};
/**
Removes the source file extension from the given path, if it has one.
A source file extension is an extension such as `.lua` or `.luau`.
*/
pub fn remove_source_file_ext(path: &Path) -> PathBuf {
if path
.extension()
.is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau")))
{
path.with_extension("")
} else {
path.to_path_buf()
}
}
/**
Writes the given bytes to a file at the specified path,
and makes sure it has permissions to be executed.
*/
pub async fn write_executable_file_to(
path: impl AsRef<Path>,
bytes: impl AsRef<[u8]>,
) -> Result<(), std::io::Error> {
let mut options = fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
options.mode(0o755); // Read & execute for all, write for owner
}
let mut file = options.open(path).await?;
file.write_all(bytes.as_ref()).await?;
Ok(())
}

View file

@ -1,83 +0,0 @@
use std::{path::PathBuf, process::ExitCode};
use anyhow::{bail, Context, Result};
use clap::Parser;
use console::style;
use tokio::fs;
use crate::standalone::metadata::Metadata;
mod base_exe;
mod files;
mod result;
mod target;
use self::base_exe::get_or_download_base_executable;
use self::files::{remove_source_file_ext, write_executable_file_to};
use self::target::BuildTarget;
/// Build a standalone executable
#[derive(Debug, Clone, Parser)]
pub struct BuildCommand {
/// The path to the input file
pub input: PathBuf,
/// The path to the output file - defaults to the
/// input file path with an executable extension
#[clap(short, long)]
pub output: Option<PathBuf>,
/// The target to compile for in the format `os-arch` -
/// defaults to the os and arch of the current system
#[clap(short, long)]
pub target: Option<BuildTarget>,
}
impl BuildCommand {
pub async fn run(self) -> Result<ExitCode> {
// Derive target spec to use, or default to the current host system
let target = self.target.unwrap_or_else(BuildTarget::current_system);
// Derive paths to use, and make sure the output path is
// not the same as the input, so that we don't overwrite it
let output_path = self
.output
.clone()
.unwrap_or_else(|| remove_source_file_ext(&self.input));
let output_path = output_path.with_extension(target.exe_extension());
if output_path == self.input {
if self.output.is_some() {
bail!("output path cannot be the same as input path");
}
bail!("output path cannot be the same as input path, please specify a different output path");
}
// Try to read the given input file
// FUTURE: We should try and resolve a full require file graph using the input
// path here instead, see the notes in the `standalone` module for more details
let source_code = fs::read(&self.input)
.await
.context("failed to read input file")?;
// Derive the base executable path based on the arguments provided
let base_exe_path = get_or_download_base_executable(target).await?;
// Read the contents of the lune interpreter as our starting point
println!(
"Compiling standalone binary from {}",
style(self.input.display()).green()
);
let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code)
.await
.context("failed to create patched binary")?;
// And finally write the patched binary to the output file
println!(
"Writing standalone binary to {}",
style(output_path.display()).blue()
);
write_executable_file_to(output_path, patched_bin).await?; // Read & execute for all, write for owner
Ok(ExitCode::SUCCESS)
}
}

View file

@ -1,24 +0,0 @@
use thiserror::Error;
use super::target::BuildTarget;
/**
Errors that may occur when building a standalone binary
*/
#[derive(Debug, Error)]
pub enum BuildError {
#[error("failed to find lune target '{0}' in GitHub release")]
ReleaseTargetNotFound(BuildTarget),
#[error("failed to find lune binary '{0}' in downloaded zip file")]
ZippedBinaryNotFound(String),
#[error("failed to download lune binary: {0}")]
Download(#[from] reqwest::Error),
#[error("failed to unzip lune binary: {0}")]
Unzip(#[from] zip_next::result::ZipError),
#[error("panicked while unzipping lune binary: {0}")]
UnzipJoin(#[from] tokio::task::JoinError),
#[error("io error: {0}")]
IoError(#[from] std::io::Error),
}
pub type BuildResult<T, E = BuildError> = std::result::Result<T, E>;

View file

@ -1,177 +0,0 @@
use std::{env::consts::ARCH, fmt, path::PathBuf, str::FromStr};
use directories::BaseDirs;
use once_cell::sync::Lazy;
static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| {
BaseDirs::new()
.expect("could not find home directory")
.home_dir()
.to_path_buf()
});
pub static CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| HOME_DIR.join(".lune").join("target"));
/**
A target operating system supported by Lune
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildTargetOS {
Windows,
Linux,
MacOS,
}
impl BuildTargetOS {
fn current_system() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"linux" => Self::Linux,
"macos" => Self::MacOS,
_ => panic!("unsupported target OS"),
}
}
fn exe_extension(self) -> &'static str {
// NOTE: We can't use the constants from std since
// they are only accessible for the current target
match self {
Self::Windows => "exe",
_ => "",
}
}
fn exe_suffix(self) -> &'static str {
match self {
Self::Windows => ".exe",
_ => "",
}
}
}
impl fmt::Display for BuildTargetOS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Windows => write!(f, "windows"),
Self::Linux => write!(f, "linux"),
Self::MacOS => write!(f, "macos"),
}
}
}
impl FromStr for BuildTargetOS {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"win" | "windows" => Ok(Self::Windows),
"linux" => Ok(Self::Linux),
"mac" | "macos" | "darwin" => Ok(Self::MacOS),
_ => Err("invalid target OS"),
}
}
}
/**
A target architecture supported by Lune
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildTargetArch {
X86_64,
Aarch64,
}
impl BuildTargetArch {
fn current_system() -> Self {
match ARCH {
"x86_64" => Self::X86_64,
"aarch64" => Self::Aarch64,
_ => panic!("unsupported target architecture"),
}
}
}
impl fmt::Display for BuildTargetArch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::X86_64 => write!(f, "x86_64"),
Self::Aarch64 => write!(f, "aarch64"),
}
}
}
impl FromStr for BuildTargetArch {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"x86_64" | "x64" => Ok(Self::X86_64),
"aarch64" | "arm64" => Ok(Self::Aarch64),
_ => Err("invalid target architecture"),
}
}
}
/**
A full target description that Lune supports (OS + Arch)
This is used to determine the target to build for standalone binaries,
and to download the correct base executable for cross-compilation.
The target may be parsed from and displayed in the form `os-arch`.
Examples of valid targets are:
- `linux-aarch64`
- `linux-x86_64`
- `macos-aarch64`
- `macos-x86_64`
- `windows-x86_64`
*/
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildTarget {
pub os: BuildTargetOS,
pub arch: BuildTargetArch,
}
impl BuildTarget {
pub fn current_system() -> Self {
Self {
os: BuildTargetOS::current_system(),
arch: BuildTargetArch::current_system(),
}
}
pub fn is_current_system(&self) -> bool {
self.os == BuildTargetOS::current_system() && self.arch == BuildTargetArch::current_system()
}
pub fn exe_extension(&self) -> &'static str {
self.os.exe_extension()
}
pub fn exe_suffix(&self) -> &'static str {
self.os.exe_suffix()
}
pub fn cache_path(&self) -> PathBuf {
CACHE_DIR.join(format!("{self}{}", self.os.exe_extension()))
}
}
impl fmt::Display for BuildTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.os, self.arch)
}
}
impl FromStr for BuildTarget {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (left, right) = s
.split_once('-')
.ok_or("target must be in the form `os-arch`")?;
let os = left.parse()?;
let arch = right.parse()?;
Ok(Self { os, arch })
}
}

View file

@ -1,12 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
mod rt;
// TODO: Remove this in 0.9.0 since it is now available as a separate crate!
#[cfg(feature = "std-roblox")]
pub use lune_roblox as roblox;
#[cfg(test)]
mod tests;
pub use crate::rt::{Runtime, RuntimeError, RuntimeResult};

View file

@ -1,42 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::process::ExitCode;
#[cfg(feature = "cli")]
pub(crate) mod cli;
pub(crate) mod standalone;
use lune_utils::fmt::Label;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.compact()
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.with_target(true)
.with_timer(tracing_subscriber::fmt::time::uptime())
.with_level(true)
.init();
if let Some(bin) = standalone::check().await {
return standalone::run(bin).await.unwrap();
}
#[cfg(feature = "cli")]
{
match cli::Cli::new().run().await {
Ok(code) => code,
Err(err) => {
eprintln!("{}\n{err:?}", Label::Error);
ExitCode::FAILURE
}
}
}
#[cfg(not(feature = "cli"))]
{
eprintln!("{}\nCLI feature is disabled", Label::Error);
ExitCode::FAILURE
}
}

View file

@ -1,5 +0,0 @@
mod result;
mod runtime;
pub use self::result::{RuntimeError, RuntimeResult};
pub use self::runtime::Runtime;

View file

@ -1,195 +0,0 @@
#![allow(clippy::missing_panics_doc)]
use std::{
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use lune_utils::jit::JitStatus;
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
use self_cell::self_cell;
use super::{RuntimeError, RuntimeResult};
// NOTE: We need to use self_cell to create a self-referential
// struct storing both the Lua VM and the scheduler. The scheduler
// needs to be created at the same time so that we can also create
// and inject the scheduler functions which will be used across runs.
self_cell! {
struct RuntimeInner {
owner: Rc<Lua>,
#[covariant]
dependent: Scheduler,
}
}
impl RuntimeInner {
fn create() -> LuaResult<Self> {
let lua = Rc::new(Lua::new());
lua.set_app_data(Rc::downgrade(&lua));
lua.set_app_data(Vec::<String>::new());
Self::try_new(lua, |lua| {
let sched = Scheduler::new(lua);
let fns = Functions::new(lua)?;
// Overwrite some globals that are not compatible with our scheduler
let co = lua.globals().get::<_, LuaTable>("coroutine")?;
co.set("resume", fns.resume.clone())?;
co.set("wrap", fns.wrap.clone())?;
// Inject all the globals that are enabled
#[cfg(any(
feature = "std-datetime",
feature = "std-fs",
feature = "std-luau",
feature = "std-net",
feature = "std-process",
feature = "std-regex",
feature = "std-roblox",
feature = "std-serde",
feature = "std-stdio",
feature = "std-task",
))]
{
lune_std::set_global_version(lua, env!("CARGO_PKG_VERSION"));
lune_std::inject_globals(lua)?;
}
// Sandbox the Luau VM and make it go zooooooooom
lua.sandbox(true)?;
// _G table needs to be injected again after sandboxing,
// otherwise it will be read-only and completely unusable
#[cfg(any(
feature = "std-datetime",
feature = "std-fs",
feature = "std-luau",
feature = "std-net",
feature = "std-process",
feature = "std-regex",
feature = "std-roblox",
feature = "std-serde",
feature = "std-stdio",
feature = "std-task",
))]
{
let g_table = lune_std::LuneStandardGlobal::GTable;
lua.globals().set(g_table.name(), g_table.create(lua)?)?;
}
Ok(sched)
})
}
fn lua(&self) -> &Lua {
self.borrow_owner()
}
fn scheduler(&self) -> &Scheduler {
self.borrow_dependent()
}
}
/**
A Lune runtime.
*/
pub struct Runtime {
inner: RuntimeInner,
jit_status: JitStatus,
}
impl Runtime {
/**
Creates a new Lune runtime, with a new Luau VM.
Injects standard globals and libraries if any of the `std` features are enabled.
*/
#[must_use]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
inner: RuntimeInner::create().expect("Failed to create runtime"),
jit_status: JitStatus::default(),
}
}
/**
Sets arguments to give in `process.args` for Lune scripts.
*/
#[must_use]
pub fn with_args<A, S>(self, args: A) -> Self
where
A: IntoIterator<Item = S>,
S: Into<String>,
{
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
self.inner.lua().set_app_data(args);
self
}
/**
Enables or disables JIT compilation.
*/
#[must_use]
pub fn with_jit(mut self, jit_status: impl Into<JitStatus>) -> Self {
self.jit_status = jit_status.into();
self
}
/**
Runs a Lune script inside of the current runtime.
This will preserve any modifications to global values / context.
# Errors
This function will return an error if the script fails to run.
*/
pub async fn run(
&mut self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> RuntimeResult<(u8, Vec<LuaValue>)> {
let lua = self.inner.lua();
let sched = self.inner.scheduler();
// Add error callback to format errors nicely + store status
let got_any_error = Arc::new(AtomicBool::new(false));
let got_any_inner = Arc::clone(&got_any_error);
self.inner.scheduler().set_error_callback(move |e| {
got_any_inner.store(true, Ordering::SeqCst);
eprintln!("{}", RuntimeError::from(e));
});
// Enable / disable the JIT as requested and store the current status as AppData
lua.set_app_data(self.jit_status);
lua.enable_jit(self.jit_status.enabled());
// Load our "main" thread
let main = lua
.load(script_contents.as_ref())
.set_name(script_name.as_ref());
// Run it on our scheduler until it and any other spawned threads complete
let main_thread_id = sched.push_thread_back(main, ())?;
sched.run().await;
let main_thread_res = match sched.get_thread_result(main_thread_id) {
Some(res) => res,
None => LuaValue::Nil.into_lua_multi(lua),
}?;
Ok((
sched
.get_exit_code()
.unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
main_thread_res.into_vec(),
))
}
}

View file

@ -1,67 +0,0 @@
[package]
name = "mlua-luau-scheduler"
version = "0.0.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Luau-based async scheduler, using mlua and async-executor"
readme = "README.md"
keywords = ["async", "luau", "scheduler"]
categories = ["async"]
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
async-executor = "1.8"
blocking = "1.5"
concurrent-queue = "2.4"
derive_more = "0.99"
event-listener = "4.0"
futures-lite = "2.2"
rustc-hash = "1.1"
tracing = "0.1"
mlua = { version = "0.9.9", features = [
"luau",
"luau-jit",
"async",
"serialize",
] }
[dev-dependencies]
async-fs = "2.1"
async-io = "2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-tracy = "0.11"
[[example]]
name = "basic_sleep"
test = true
[[example]]
name = "basic_spawn"
test = true
[[example]]
name = "callbacks"
test = true
[[example]]
name = "exit_code"
test = true
[[example]]
name = "lots_of_threads"
test = true
[[example]]
name = "scheduler_ordering"
test = true
[[example]]
name = "tracy"
test = false

View file

@ -1,78 +0,0 @@
<!-- markdownlint-disable MD033 -->
<!-- markdownlint-disable MD041 -->
# `mlua-luau-scheduler`
An async scheduler for Luau, using [`mlua`][mlua] and built on top of [`async-executor`][async-executor].
This crate is runtime-agnostic and is compatible with any async runtime, including [Tokio][tokio], [smol][smol], [async-std][async-std], and others. </br>
However, since many dependencies are shared with [smol][smol], depending on it over other runtimes may be preferred.
[async-executor]: https://crates.io/crates/async-executor
[async-std]: https://async.rs
[mlua]: https://crates.io/crates/mlua
[smol]: https://github.com/smol-rs/smol
[tokio]: https://tokio.rs
## Example Usage
### 1. Import dependencies
```rs
use std::time::{Duration, Instant};
use std::io::ErrorKind;
use async_io::{block_on, Timer};
use async_fs::read_to_string;
use mlua::prelude::*;
use mlua_luau_scheduler::*;
```
### 2. Set up Lua environment
```rs
let lua = Lua::new();
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: f64| async move {
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
lua.globals().set(
"readFile",
lua.create_async_function(|lua, path: String| async move {
// Spawn background task that does not take up resources on the lua thread
// Normally, futures in mlua can not be shared across threads, but this can
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
task.await.into_lua_err()
})?,
)?;
```
### 3. Set up scheduler, run threads
```rs
let sched = Scheduler::new(&lua)?;
// We can create multiple lua threads ...
let sleepThread = lua.load("sleep(0.1)");
let fileThread = lua.load("readFile(\"Cargo.toml\")");
// ... spawn them both onto the scheduler ...
sched.push_thread_front(sleepThread, ());
sched.push_thread_front(fileThread, ());
// ... and run until they finish
block_on(sched.run());
```

View file

@ -1,45 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::Scheduler;
const MAIN_SCRIPT: &str = include_str!("./lua/basic_sleep.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: f64| async move {
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
// Load the main script into a scheduler
let sched = Scheduler::new(&lua);
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_basic_sleep() -> LuaResult<()> {
main()
}

View file

@ -1,64 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind;
use async_fs::read_to_string;
use async_io::block_on;
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSpawnExt, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/basic_spawn.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
lua.globals().set(
"readFile",
lua.create_async_function(|lua, path: String| async move {
// Spawn background task that does not take up resources on the Lua thread
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
// Wait for it to complete
let result = task.await.into_lua_err();
// We can also spawn local tasks that do take up resources
// on the Lua thread, but that do not have the Send bound
if result.is_ok() {
lua.spawn_local(async move {
println!("File read successfully!");
});
}
result
})?,
)?;
// Load the main script into a scheduler
let sched = Scheduler::new(&lua);
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_basic_spawn() -> LuaResult<()> {
main()
}

View file

@ -1,48 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use mlua_luau_scheduler::Scheduler;
use async_io::block_on;
const MAIN_SCRIPT: &str = include_str!("./lua/callbacks.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
// Create a new scheduler with custom callbacks
let sched = Scheduler::new(&lua);
sched.set_error_callback(|e| {
println!(
"Captured error from Lua!\n{}\n{e}\n{}",
"-".repeat(15),
"-".repeat(15)
);
});
// Load the main script into the scheduler, and keep track of the thread we spawn
let main = lua.load(MAIN_SCRIPT);
let id = sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// We should have gotten the error back from our script
assert!(sched.get_thread_result(id).unwrap().is_err());
Ok(())
}
#[test]
fn test_callbacks() -> LuaResult<()> {
main()
}

View file

@ -1,43 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use async_io::block_on;
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/exit_code.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("exit", fns.exit)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// Verify that we got a correct exit code
let code = sched.get_exit_code().unwrap_or_default();
assert_eq!(code, 1);
Ok(())
}
#[test]
fn test_exit_code() -> LuaResult<()> {
main()
}

View file

@ -1,51 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau");
const ONE_NANOSECOND: Duration = Duration::from_nanos(1);
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, ()| async move {
// Obviously we can't sleep for a single nanosecond since
// this uses OS scheduling under the hood, but we can try
Timer::after(ONE_NANOSECOND).await;
Ok(())
})?,
)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_lots_of_threads() -> LuaResult<()> {
main()
}

View file

@ -1,13 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
print("Sleeping for 3 seconds...")
sleep(1)
print("1 second passed")
sleep(1)
print("2 seconds passed")
sleep(1)
print("3 seconds passed")

View file

@ -1,17 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local _, err = pcall(function()
local file = readFile("Cargo.toml")
if file ~= nil then
print("Cargo.toml found!")
print("Contents:")
print(file)
else
print("Cargo.toml not found!")
end
end)
if err ~= nil then
print("Error while reading file: " .. err)
end

View file

@ -1,4 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
error("Oh no! Something went very very wrong!")

View file

@ -1,8 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
print("Setting exit code manually")
exit(1)
error("unreachable")

View file

@ -1,29 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local NUM_BATCHES = 10
local NUM_THREADS = 100_000
print(`Spawning {NUM_BATCHES * NUM_THREADS} threads split into {NUM_BATCHES} batches\n`)
local before = os.clock()
for i = 1, NUM_BATCHES do
print(`Batch {i} of {NUM_BATCHES}`)
local thread = coroutine.running()
local counter = 0
for j = 1, NUM_THREADS do
spawn(function()
sleep(0.1)
counter += 1
if counter == NUM_THREADS then
spawn(thread)
end
end)
end
coroutine.yield()
end
local after = os.clock()
print(`\nSpawned {NUM_BATCHES * NUM_THREADS} sleeping threads in {after - before}s`)

View file

@ -1,34 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local nums = {}
local function insert(n: number)
table.insert(nums, n)
print(n)
end
insert(1)
-- Defer will run at the end of the resumption cycle, but without yielding
defer(function()
insert(5)
end)
-- Spawn will instantly run up until the first yield, and must then be resumed manually ...
spawn(function()
insert(2)
coroutine.yield()
error("unreachable code")
end)
-- ... unless calling functions created using `lua.create_async_function(...)`,
-- which will resume their calling thread with their result automatically
spawn(function()
insert(3)
sleep(1)
insert(6)
end)
insert(4)
return nums

View file

@ -1,56 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/scheduler_ordering.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set("defer", fns.defer)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: Option<f64>| async move {
let duration = duration.unwrap_or_default().max(1.0 / 250.0);
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
// Load the main script into the scheduler, and keep track of the thread we spawn
let main = lua.load(MAIN_SCRIPT);
let id = sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// We should have gotten proper values back from our script
let res = sched.get_thread_result(id).unwrap().unwrap();
let nums = Vec::<usize>::from_lua_multi(res, &lua)?;
assert_eq!(nums, vec![1, 2, 3, 4, 5, 6]);
Ok(())
}
#[test]
fn test_scheduler_ordering() -> LuaResult<()> {
main()
}

View file

@ -1,61 +0,0 @@
/*
NOTE: This example is the same as "lots_of_threads", but with tracy set up for performance profiling.
How to run:
1. Install tracy
- Follow the instructions at https://github.com/wolfpld/tracy
- Or install via something like homebrew: `brew install tracy`
2. Run the server (`tracy`) in a terminal
3. Run the example in another terminal
- `export RUST_LOG=trace`
- `cargo run --example tracy`
*/
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use async_io::{block_on, Timer};
use tracing_subscriber::layer::SubscriberExt;
use tracing_tracy::{client::Client as TracyClient, TracyLayer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau");
const ONE_NANOSECOND: Duration = Duration::from_nanos(1);
pub fn main() -> LuaResult<()> {
let _client = TracyClient::start();
let _ = tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(TracyLayer::default()),
);
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, ()| async move {
// Obviously we can't sleep for a single nanosecond since
// this uses OS scheduling under the hood, but we can try
Timer::after(ONE_NANOSECOND).await;
Ok(())
})?,
)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}

View file

@ -1,45 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use mlua::prelude::*;
type ErrorCallback = Box<dyn Fn(LuaError) + Send + 'static>;
#[derive(Clone)]
pub(crate) struct ThreadErrorCallback {
inner: Rc<RefCell<Option<ErrorCallback>>>,
}
impl ThreadErrorCallback {
pub fn new() -> Self {
Self {
inner: Rc::new(RefCell::new(None)),
}
}
pub fn replace(&self, callback: impl Fn(LuaError) + Send + 'static) {
self.inner.borrow_mut().replace(Box::new(callback));
}
pub fn clear(&self) {
self.inner.borrow_mut().take();
}
pub fn call(&self, error: &LuaError) {
if let Some(cb) = &*self.inner.borrow() {
cb(error.clone());
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn default_error_callback(e: LuaError) {
eprintln!("{e}");
}
impl Default for ThreadErrorCallback {
fn default() -> Self {
let this = Self::new();
this.replace(default_error_callback);
this
}
}

View file

@ -1,31 +0,0 @@
use std::{cell::Cell, rc::Rc};
use event_listener::Event;
#[derive(Debug, Clone)]
pub(crate) struct Exit {
code: Rc<Cell<Option<u8>>>,
event: Rc<Event>,
}
impl Exit {
pub fn new() -> Self {
Self {
code: Rc::new(Cell::new(None)),
event: Rc::new(Event::new()),
}
}
pub fn set(&self, code: u8) {
self.code.set(Some(code));
self.event.notify(usize::MAX);
}
pub fn get(&self) -> Option<u8> {
self.code.get()
}
pub async fn listen(&self) {
self.event.listen().await;
}
}

Some files were not shown because too many files have changed in this diff Show more