Compare commits

..

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

371 changed files with 7554 additions and 17461 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

@ -2,16 +2,15 @@ name: CI
on:
push:
pull_request:
workflow_dispatch:
defaults:
run:
shell: bash
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: Check formatting
runs-on: ubuntu-latest
@ -24,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
@ -38,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
@ -59,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:
@ -80,26 +81,20 @@ jobs:
components: clippy
targets: ${{ matrix.cargo-target }}
- name: Install binstall
uses: cargo-bins/cargo-binstall@main
- name: Install nextest
run: cargo binstall cargo-nextest
- name: Build
run: |
cargo build --workspace \
cargo build \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Lint
run: |
cargo clippy --workspace \
cargo clippy \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Test
run: |
cargo nextest run --no-fail-fast \
cargo test \
--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 }}

5
.gitignore vendored
View file

@ -21,12 +21,7 @@ lune.yml
luneDocs.json
luneTypes.d.luau
# Dirs generated by runtime or build scripts
/types
# Files generated by runtime or build scripts
scripts/brick_color.rs
scripts/font_enum_map.rs
scripts/physical_properties_enum_map.rs

View file

@ -45,7 +45,7 @@ test-bin *ARGS:
fmt:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune crates scripts tests \
stylua .lune scripts tests types \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt
@ -55,7 +55,7 @@ fmt:
fmt-check:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune crates scripts tests \
stylua .lune scripts tests types \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt --check
@ -65,11 +65,10 @@ fmt-check:
analyze:
#!/usr/bin/env bash
set -euo pipefail
lune run scripts/analyze_copy_typedefs
luau-lsp analyze \
--settings=".vscode/settings.json" \
--ignore="tests/roblox/rbx-test-files/**" \
.lune crates scripts tests
.lune scripts tests types
# Zips up the built binary into a single zip file
[no-exit-message]

View file

@ -1,7 +1,6 @@
local fs = require("@lune/fs")
local net = require("@lune/net")
local process = require("@lune/process")
local serde = require("@lune/serde")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
@ -130,7 +129,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.exec("ping", {
local result = process.spawn("ping", {
"google.com",
"-c 4",
})
@ -146,8 +145,10 @@ local result = process.exec("ping", {
if result.ok then
assert(#result.stdout > 0, "Result output was empty")
local min, avg, max, stddev =
string.match(result.stdout, "min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms")
local min, avg, max, stddev = string.match(
result.stdout,
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
)
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
@ -171,7 +172,7 @@ local apiResult = net.request({
headers = {
["Content-Type"] = "application/json",
} :: { [string]: string },
body = serde.encode("json", {
body = net.jsonEncode({
title = "foo",
body = "bar",
}),
@ -191,7 +192,7 @@ type ApiResponse = {
userId: number,
}
local apiResponse: ApiResponse = serde.decode("json", apiResult.body)
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
assert(apiResponse.title == "foo", "Invalid json response")
assert(apiResponse.body == "bar", "Invalid json response")
print("Got valid JSON response with changes applied")

View file

@ -3,6 +3,7 @@
local net = require("@lune/net")
local process = require("@lune/process")
local task = require("@lune/task")
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
@ -10,10 +11,6 @@ local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
-- Create our responder functions
local function root(_request: net.ServeRequest): string
return `Hello from Lune server!`
end
local function pong(request: net.ServeRequest): string
return `Pong!\n{request.path}\n{request.body}`
end
@ -32,12 +29,10 @@ local function notFound(_request: net.ServeRequest): net.ServeResponse
}
end
-- Run the server on the port forever
-- Run the server on port 8080
net.serve(PORT, function(request)
if request.path == "/" then
return root(request)
elseif string.sub(request.path, 1, 5) == "/ping" then
local handle = net.serve(PORT, function(request)
if string.sub(request.path, 1, 5) == "/ping" then
return pong(request)
elseif string.sub(request.path, 1, 7) == "/teapot" then
return teapot(request)
@ -47,4 +42,12 @@ net.serve(PORT, function(request)
end)
print(`Listening on port {PORT} 🚀`)
print("Press Ctrl+C to stop")
-- Exit our example after a small delay, if you copy this
-- example just remove this part to keep the server running
task.delay(2, function()
print("Shutting down...")
task.wait(1)
handle.stop()
end)

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,361 +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).
## Unreleased
### Added
- Added support for non-UTF8 strings in arguments to `process.exec` and `process.spawn`
### Changed
- Improved cross-platform compatibility and correctness for values in `process.args` and `process.env`, especially on Windows
### Fixed
- Fixed various crashes during require that had the error `cannot mutably borrow app data container`
## `0.9.2` - April 30th, 2025
### Changed
- Improved performance of `net.request` and `net.serve` when handling large request bodies
- Improved performance and memory usage of `task.spawn`, `task.defer`, and `task.delay`
### Fixed
- Fixed accidental breakage of `net.request` in version `0.9.1`
## `0.9.1` - April 29th, 2025
### Added
- Added support for automatic decompression of HTTP requests in `net.serve` ([#310])
### Fixed
- Fixed `net.serve` no longer serving requests if the returned `ServeHandle` is discarded ([#310])
- Fixed `net.serve` having various performance issues ([#310])
- Fixed Lune still running after cancelling a task such as `task.delay(5, ...)` and all tasks having completed
[#310]: https://github.com/lune-org/lune/pull/310
## `0.9.0` - April 25th, 2025
The next major version of Lune has finally been released!
This release has been a long time coming, and many breaking changes have been made.
If you are an existing Lune user upgrading to this version, you will **most likely** be affected.
The full list of breaking changes can be found on below.
### Breaking changes & additions
- The behavior of `require` has changed, according to the latest Luau RFCs and specifications.
For the full details, feel free to read documentation [here](https://github.com/luau-lang/rfcs), otherwise, the most notable changes here are:
- Paths passed to require must start with either `./`, `../` or `@` - require statements such as `require("foo")` **will now error** and must be changed to `require("./foo")`.
- The behavior of require from within `init.luau` and `init.lua` files has changed - previously `require("./foo")` would resolve
to the file or directory `foo` _as a **sibling** of the init file_, but will now resolve to the file or directory `foo` _which is a sibling of the **parent directory** of the init file_.
To require files inside of the same directory as the init file, the new `@self` alias must be used - like `require("@self/foo")`.
- The main `lune run` subcommand will no longer sink flags passed to it - `lune run --` will now *literally* pass the string `--` as the first
value in `process.args`, and `--` is no longer necessary to be able to pass flag arguments such as `--foo` and `-b` properly to your Lune programs.
- Two new process spawning functions - `process.create` and `process.exec` - replace the previous `process.spawn` API. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function, with slight changes in how the `stdin` option is passed.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write to standard input and output streams of the child process.
```lua
local child = process.create("program", {
"first-argument",
"second-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading partial data from stdout
local data = child.stdout:read()
print(data)
-- Reading the full stdout
local full = child.stdout:readToEnd()
print(full)
```
- Removed `net.jsonEncode` and `net.jsonDecode` - please use the equivalent `serde.encode("json", ...)` and `serde.decode("json", ...)` instead
- 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.
- Various changes have been made to the Lune Rust crates:
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
- All Lune standard library crates now export a `typedefs` function that returns the source code for the respective standard library module type definitions
- All Lune crates now depend on `mlua` version `0.10` or above
- Most Lune crates have been migrated to the `smol` and `async-*` ecosystem instead of `tokio`, with a full migration expected soon (this will not break public types)
- The `roblox` crate re-export has been removed from the main `lune` crate - please depend on `lune-roblox` crate directly instead
### Added
- Added functions for getting Roblox Studio locations to the `roblox` standard library ([#284])
- Added support for the `Content` datatype in the `roblox` standard library ([#305])
- Added support for `EnumItem` instance attributes in the `roblox` standard library ([#306])
- Added support for RFC 2822 dates in the `datetime` standard library using `fromRfc2822` ([#285]) - the `fromIsoDate`
function has also been deprecated (not removed yet) and `fromRfc3339` should instead be preferred for any new work.
- Added a `readLine` function to the `stdio` standard library for reading line-by-line from stdin.
- Added a way to disable JIT by setting the `LUNE_LUAU_JIT` environment variable to `false` before running Lune.
- Added `process.endianness` constant ([#267])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
- Updated to Luau version `0.663`
- Updated to rbx-dom database version `0.670`
### Fixed
- Fixed deadlock in `stdio.format` calls in `__tostring` metamethods ([#288])
- Fixed `task.wait` and `task.delay` not being guaranteed to yield when duration is set to zero or very small values
- Fixed `__tostring` metamethods sometimes not being respected in `print` and `stdio.format` calls
[#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
[#265]: https://github.com/lune-org/lune/pull/265
[#267]: https://github.com/lune-org/lune/pull/267
[#284]: https://github.com/lune-org/lune/pull/284
[#285]: https://github.com/lune-org/lune/pull/285
[#288]: https://github.com/lune-org/lune/pull/288
[#305]: https://github.com/lune-org/lune/pull/305
[#306]: https://github.com/lune-org/lune/pull/306
## `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

3130
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,28 +0,0 @@
[package]
name = "lune-roblox"
version = "0.2.2"
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.10.3", features = ["luau"] }
glam = "0.30"
rand = "0.9"
thiserror = "2.0"
rbx_binary = "1.0"
rbx_dom_weak = "3.0"
rbx_reflection = "5.0"
rbx_reflection_database = "1.0"
rbx_xml = "1.0"
lune-utils = { version = "0.2.2", 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 = |_: &Lua, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_: &Lua, 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<F: LuaUserDataFields<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<M: LuaUserDataMethods<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,72 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::TableBuilder;
pub mod datatypes;
pub mod document;
pub mod instance;
pub mod reflection;
pub(crate) mod exports;
pub(crate) mod shared;
use exports::export;
fn create_all_exports(lua: Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
use datatypes::types::*;
use instance::Instance;
Ok(vec![
// Datatypes
export::<Axes>(lua.clone())?,
export::<BrickColor>(lua.clone())?,
export::<CFrame>(lua.clone())?,
export::<Color3>(lua.clone())?,
export::<ColorSequence>(lua.clone())?,
export::<ColorSequenceKeypoint>(lua.clone())?,
export::<Content>(lua.clone())?,
export::<Faces>(lua.clone())?,
export::<Font>(lua.clone())?,
export::<NumberRange>(lua.clone())?,
export::<NumberSequence>(lua.clone())?,
export::<NumberSequenceKeypoint>(lua.clone())?,
export::<PhysicalProperties>(lua.clone())?,
export::<Ray>(lua.clone())?,
export::<Rect>(lua.clone())?,
export::<UDim>(lua.clone())?,
export::<UDim2>(lua.clone())?,
export::<Region3>(lua.clone())?,
export::<Region3int16>(lua.clone())?,
export::<Vector2>(lua.clone())?,
export::<Vector2int16>(lua.clone())?,
export::<Vector3>(lua.clone())?,
export::<Vector3int16>(lua.clone())?,
// Classes
export::<Instance>(lua.clone())?,
// Singletons
("Enum", Enums.into_lua(&lua)?),
])
}
/**
Creates a table containing all the Roblox datatypes, classes, and singletons.
Note that this is not guaranteed to contain any value unless indexed directly,
it may be optimized to use lazy initialization in the future.
# Errors
Errors when out of memory or when a value cannot be created.
*/
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
// FUTURE: We can probably create these lazily as users
// index the main exports (this return value) table and
// save some memory and startup time. The full exports
// table is quite big and probably won't get any smaller
// since we impl all roblox constructors for each datatype.
let exports = create_all_exports(lua.clone())?;
TableBuilder::new(lua)?
.with_values(exports)?
.build_readonly()
}

View file

@ -1,22 +0,0 @@
[package]
name = "lune-std-datetime"
version = "0.2.2"
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.10.3", features = ["luau"] }
thiserror = "2.0"
chrono = "0.4.38"
chrono_lc = "0.1.6"
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,52 +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;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `datetime` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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", |_, date: String| {
Ok(DateTime::from_rfc_3339(date)?) // FUTURE: Remove this rfc3339 alias method
})?
.with_function("fromRfc3339", |_, date: String| {
Ok(DateTime::from_rfc_3339(date)?)
})?
.with_function("fromRfc2822", |_, date: String| {
Ok(DateTime::from_rfc_2822(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.2.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.10.3", features = ["luau"] }
async-fs = "2.1"
bstr = "1.9"
futures-lite = "2.6"
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-std-datetime = { version = "0.2.2", path = "../lune-std-datetime" }

View file

@ -1,18 +0,0 @@
[package]
name = "lune-std-luau"
version = "0.2.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.10.3", features = ["luau", "luau-jit"] }
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,95 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::{jit::JitEnablement, TableBuilder};
mod options;
use self::options::{LuauCompileOptions, LuauLoadOptions};
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `luau` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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,
(source, options): (LuaString, LuauCompileOptions),
) -> LuaResult<LuaString> {
options
.into_compiler()
.compile(source.as_bytes())
.and_then(|s| lua.create_string(s))
}
fn load_source(
lua: &Lua,
(source, options): (LuaString, LuauLoadOptions),
) -> LuaResult<LuaFunction> {
let mut chunk = lua
.load(source.as_bytes().to_vec())
.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().metatable() {
environment.set_metatable(Some(global_metatable));
}
} else if let Some(custom_metatable) = custom_environment.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::<JitEnablement>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function)
}

View file

@ -1,42 +0,0 @@
[package]
name = "lune-std-net"
version = "0.2.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.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
async-channel = "2.3"
async-executor = "1.13"
async-io = "2.4"
async-lock = "3.4"
async-net = "2.0"
async-tungstenite = "0.29"
blocking = "1.6"
bstr = "1.9"
form_urlencoded = "1.2"
futures = { version = "0.3", default-features = false, features = ["std"] }
futures-lite = "2.6"
futures-rustls = "0.26"
http-body-util = "0.1"
hyper = { version = "1.6", default-features = false, features = ["http1", "client", "server"] }
pin-project-lite = "0.2"
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
rustls-pki-types = "1.11"
url = "2.5"
urlencoding = "2.1"
webpki = "0.22"
webpki-roots = "0.26"
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-std-serde = { version = "0.2.2", path = "../lune-std-serde" }

View file

@ -1,59 +0,0 @@
use hyper::body::{Buf, Bytes};
use super::inner::ReadableBodyInner;
/**
The cursor keeping track of inner data and its position for a readable body.
*/
#[derive(Debug, Clone)]
pub struct ReadableBodyCursor {
inner: ReadableBodyInner,
start: usize,
}
impl ReadableBodyCursor {
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn as_slice(&self) -> &[u8] {
&self.inner.as_slice()[self.start..]
}
pub fn advance(&mut self, cnt: usize) {
self.start += cnt;
if self.start > self.inner.len() {
self.start = self.inner.len();
}
}
pub fn into_bytes(self) -> Bytes {
self.inner.into_bytes()
}
}
impl Buf for ReadableBodyCursor {
fn remaining(&self) -> usize {
self.len().saturating_sub(self.start)
}
fn chunk(&self) -> &[u8] {
self.as_slice()
}
fn advance(&mut self, cnt: usize) {
self.advance(cnt);
}
}
impl<T> From<T> for ReadableBodyCursor
where
T: Into<ReadableBodyInner>,
{
fn from(value: T) -> Self {
Self {
inner: value.into(),
start: 0,
}
}
}

View file

@ -1,35 +0,0 @@
use http_body_util::BodyExt;
use hyper::{
body::{Bytes, Incoming},
header::CONTENT_ENCODING,
HeaderMap,
};
use mlua::prelude::*;
use lune_std_serde::{decompress, CompressDecompressFormat};
pub async fn handle_incoming_body(
headers: &HeaderMap,
body: Incoming,
should_decompress: bool,
) -> LuaResult<(Bytes, bool)> {
let mut body = body.collect().await.into_lua_err()?.to_bytes();
let was_decompressed = if should_decompress {
let decompress_format = headers
.get(CONTENT_ENCODING)
.and_then(|value| value.to_str().ok())
.and_then(CompressDecompressFormat::detect_from_header_str);
if let Some(format) = decompress_format {
body = Bytes::from(decompress(body, format).await?);
true
} else {
false
}
} else {
false
};
Ok((body, was_decompressed))
}

View file

@ -1,110 +0,0 @@
use hyper::body::{Buf as _, Bytes};
use mlua::{prelude::*, Buffer as LuaBuffer};
/**
The inner data for a readable body.
*/
#[derive(Debug, Clone)]
pub enum ReadableBodyInner {
Bytes(Bytes),
String(String),
LuaString(LuaString),
LuaBuffer(LuaBuffer),
}
impl ReadableBodyInner {
pub fn len(&self) -> usize {
match self {
Self::Bytes(b) => b.len(),
Self::String(s) => s.len(),
Self::LuaString(s) => s.as_bytes().len(),
Self::LuaBuffer(b) => b.len(),
}
}
pub fn as_slice(&self) -> &[u8] {
/*
SAFETY: Reading lua strings and lua buffers as raw slices is safe while we can
guarantee that the inner Lua value + main lua struct has not yet been dropped
1. Buffers are fixed-size and guaranteed to never resize
2. We do not expose any method for writing to the body, only reading
3. We guarantee that net.request and net.serve futures are only driven forward
while we also know that the Lua + scheduler pair have not yet been dropped
4. Any writes from within lua to a buffer, are considered user error,
and are not unsafe, since the only possible outcome with the above
guarantees is invalid / mangled contents in request / response bodies
*/
match self {
Self::Bytes(b) => b.chunk(),
Self::String(s) => s.as_bytes(),
Self::LuaString(s) => unsafe {
// BorrowedBytes would not let us return a plain slice here,
// which is what the Buf implementation below needs - we need to
// do a little hack here to re-create the slice without a lifetime
let b = s.as_bytes();
let ptr = b.as_ptr();
let len = b.len();
std::slice::from_raw_parts(ptr, len)
},
Self::LuaBuffer(b) => unsafe {
// Similar to above, we need to get the raw slice for the buffer,
// which is a bit trickier here because Buffer has a read + write
// interface instead of using slices for some unknown reason
let v = LuaValue::Buffer(b.clone());
let ptr = v.to_pointer().cast::<u8>();
let len = b.len();
std::slice::from_raw_parts(ptr, len)
},
}
}
pub fn into_bytes(self) -> Bytes {
match self {
Self::Bytes(b) => b,
Self::String(s) => Bytes::from(s),
Self::LuaString(s) => Bytes::from(s.as_bytes().to_vec()),
Self::LuaBuffer(b) => Bytes::from(b.to_vec()),
}
}
}
impl From<&'static str> for ReadableBodyInner {
fn from(value: &'static str) -> Self {
Self::Bytes(Bytes::from(value))
}
}
impl From<Vec<u8>> for ReadableBodyInner {
fn from(value: Vec<u8>) -> Self {
Self::Bytes(Bytes::from(value))
}
}
impl From<Bytes> for ReadableBodyInner {
fn from(value: Bytes) -> Self {
Self::Bytes(value)
}
}
impl From<String> for ReadableBodyInner {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<LuaString> for ReadableBodyInner {
fn from(value: LuaString) -> Self {
Self::LuaString(value)
}
}
impl From<LuaBuffer> for ReadableBodyInner {
fn from(value: LuaBuffer) -> Self {
Self::LuaBuffer(value)
}
}

View file

@ -1,11 +0,0 @@
#![allow(unused_imports)]
mod cursor;
mod incoming;
mod inner;
mod readable;
pub use self::cursor::ReadableBodyCursor;
pub use self::incoming::handle_incoming_body;
pub use self::inner::ReadableBodyInner;
pub use self::readable::ReadableBody;

View file

@ -1,105 +0,0 @@
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};
use hyper::body::{Body, Bytes, Frame, SizeHint};
use mlua::prelude::*;
use super::cursor::ReadableBodyCursor;
/**
Zero-copy wrapper for a readable body.
Provides methods to read bytes that can be safely used if, and only
if, the respective Lua struct for the body has not yet been dropped.
If the body was created from a `Vec<u8>`, `Bytes`, or a `String`, reading
bytes is always safe and does not go through any additional indirections.
*/
#[derive(Debug, Clone)]
pub struct ReadableBody {
cursor: Option<ReadableBodyCursor>,
}
impl ReadableBody {
pub const fn empty() -> Self {
Self { cursor: None }
}
pub fn as_slice(&self) -> &[u8] {
match self.cursor.as_ref() {
Some(cursor) => cursor.as_slice(),
None => &[],
}
}
pub fn into_bytes(self) -> Bytes {
match self.cursor {
Some(cursor) => cursor.into_bytes(),
None => Bytes::new(),
}
}
}
impl Body for ReadableBody {
type Data = ReadableBodyCursor;
type Error = Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
Poll::Ready(self.cursor.take().map(|d| Ok(Frame::data(d))))
}
fn is_end_stream(&self) -> bool {
self.cursor.is_none()
}
fn size_hint(&self) -> SizeHint {
self.cursor.as_ref().map_or_else(
|| SizeHint::with_exact(0),
|c| SizeHint::with_exact(c.len() as u64),
)
}
}
impl<T> From<T> for ReadableBody
where
T: Into<ReadableBodyCursor>,
{
fn from(value: T) -> Self {
Self {
cursor: Some(value.into()),
}
}
}
impl<T> From<Option<T>> for ReadableBody
where
T: Into<ReadableBodyCursor>,
{
fn from(value: Option<T>) -> Self {
Self {
cursor: value.map(Into::into),
}
}
}
impl FromLua for ReadableBody {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
match value {
LuaValue::Nil => Ok(Self::empty()),
LuaValue::String(str) => Ok(Self::from(str)),
LuaValue::Buffer(buf) => Ok(Self::from(buf)),
v => Err(LuaError::FromLuaConversionError {
from: v.type_name(),
to: "Body".to_string(),
message: Some(format!(
"Invalid body - expected string or buffer, got {}",
v.type_name()
)),
}),
}
}
}

View file

@ -1,95 +0,0 @@
use std::{
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use async_net::TcpStream;
use futures_lite::prelude::*;
use futures_rustls::{TlsConnector, TlsStream};
use rustls_pki_types::ServerName;
use url::Url;
use crate::client::rustls::CLIENT_CONFIG;
#[derive(Debug)]
pub enum HttpStream {
Plain(TcpStream),
Tls(TlsStream<TcpStream>),
}
impl HttpStream {
pub async fn connect(url: Url) -> Result<Self, io::Error> {
let Some(host) = url.host() else {
return Err(make_err("unknown or missing host"));
};
let Some(port) = url.port_or_known_default() else {
return Err(make_err("unknown or missing port"));
};
let use_tls = match url.scheme() {
"http" => false,
"https" => true,
s => return Err(make_err(format!("unsupported scheme: {s}"))),
};
let host = host.to_string();
let stream = TcpStream::connect((host.clone(), port)).await?;
let stream = if use_tls {
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
let stream = connector.connect(servname, stream).await?;
Self::Tls(TlsStream::Client(stream))
} else {
Self::Plain(stream)
};
Ok(stream)
}
}
impl AsyncRead for HttpStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_read(cx, buf),
HttpStream::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
impl AsyncWrite for HttpStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_write(cx, buf),
HttpStream::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_close(cx),
HttpStream::Tls(stream) => Pin::new(stream).poll_close(cx),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
HttpStream::Plain(stream) => Pin::new(stream).poll_flush(cx),
HttpStream::Tls(stream) => Pin::new(stream).poll_flush(cx),
}
}
}
fn make_err(e: impl ToString) -> io::Error {
io::Error::new(io::ErrorKind::Other, e.to_string())
}

View file

@ -1,125 +0,0 @@
use http_body_util::Full;
use hyper::{
body::Incoming,
client::conn::http1::handshake,
header::{HeaderValue, ACCEPT, CONTENT_LENGTH, HOST, LOCATION, USER_AGENT},
Method, Request as HyperRequest, Response as HyperResponse, Uri,
};
use mlua::prelude::*;
use url::Url;
use crate::{
body::ReadableBody,
client::{http_stream::HttpStream, ws_stream::WsStream},
shared::{
headers::create_user_agent_header,
hyper::{HyperExecutor, HyperIo},
request::Request,
response::Response,
websocket::Websocket,
},
};
pub mod http_stream;
pub mod rustls;
pub mod ws_stream;
const MAX_REDIRECTS: usize = 10;
/**
Connects to a websocket at the given URL.
*/
pub async fn connect_websocket(url: Url) -> LuaResult<Websocket<WsStream>> {
let stream = WsStream::connect(url).await?;
Ok(Websocket::from(stream))
}
/**
Sends the request and returns the final response.
This will follow any redirects returned by the server,
modifying the request method and body as necessary.
*/
pub async fn send_request(mut request: Request, lua: Lua) -> LuaResult<Response> {
let url = request
.inner
.uri()
.to_string()
.parse::<Url>()
.into_lua_err()?;
// Some headers are required by most if not
// all servers, make sure those are present...
if !request.headers().contains_key(HOST.as_str()) {
if let Some(host) = url.host_str() {
let host = HeaderValue::from_str(host).into_lua_err()?;
request.inner.headers_mut().insert(HOST, host);
}
}
if !request.headers().contains_key(USER_AGENT.as_str()) {
let ua = create_user_agent_header(&lua)?;
let ua = HeaderValue::from_str(&ua).into_lua_err()?;
request.inner.headers_mut().insert(USER_AGENT, ua);
}
if !request.headers().contains_key(CONTENT_LENGTH.as_str()) && request.method() != Method::GET {
let len = request.body().len().to_string();
let len = HeaderValue::from_str(&len).into_lua_err()?;
request.inner.headers_mut().insert(CONTENT_LENGTH, len);
}
if !request.headers().contains_key(ACCEPT.as_str()) {
let accept = HeaderValue::from_static("*/*");
request.inner.headers_mut().insert(ACCEPT, accept);
}
// ... we can now safely continue and send the request
loop {
let stream = HttpStream::connect(url.clone()).await?;
let (mut sender, conn) = handshake(HyperIo::from(stream)).await.into_lua_err()?;
HyperExecutor::execute(lua.clone(), conn);
let (parts, body) = request.clone_inner().into_parts();
let data = HyperRequest::from_parts(parts, Full::new(body.into_bytes()));
let incoming = sender.send_request(data).await.into_lua_err()?;
if let Some((new_method, new_uri)) =
check_redirect(request.inner.method().clone(), &incoming)
{
if request.redirects.is_some_and(|r| r >= MAX_REDIRECTS) {
return Err(LuaError::external("Too many redirects"));
}
if new_method == Method::GET {
*request.inner.body_mut() = ReadableBody::empty();
}
*request.inner.method_mut() = new_method;
*request.inner.uri_mut() = new_uri;
*request.redirects.get_or_insert_default() += 1;
continue;
}
break Response::from_incoming(incoming, request.decompress).await;
}
}
fn check_redirect(method: Method, response: &HyperResponse<Incoming>) -> Option<(Method, Uri)> {
if !response.status().is_redirection() {
return None;
}
let location = response.headers().get(LOCATION)?;
let location = location.to_str().ok()?;
let location = location.parse().ok()?;
let method = match response.status().as_u16() {
301..=303 => Method::GET,
_ => method,
};
Some((method, location))
}

View file

@ -1,26 +0,0 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, LazyLock,
};
use rustls::{crypto::ring, ClientConfig};
static PROVIDER_INITIALIZED: AtomicBool = AtomicBool::new(false);
pub fn initialize_provider() {
if !PROVIDER_INITIALIZED.load(Ordering::Relaxed) {
PROVIDER_INITIALIZED.store(true, Ordering::Relaxed);
// Only errors if already installed, which is fine
ring::default_provider().install_default().ok();
}
}
pub static CLIENT_CONFIG: LazyLock<Arc<ClientConfig>> = LazyLock::new(|| {
initialize_provider();
rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
})
.with_no_client_auth()
.into()
});

View file

@ -1,114 +0,0 @@
use std::{
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use async_net::TcpStream;
use async_tungstenite::{
tungstenite::{Error as TungsteniteError, Message, Result as TungsteniteResult},
WebSocketStream as TungsteniteStream,
};
use futures::Sink;
use futures_lite::prelude::*;
use futures_rustls::{TlsConnector, TlsStream};
use rustls_pki_types::ServerName;
use url::Url;
use crate::client::rustls::CLIENT_CONFIG;
#[derive(Debug)]
pub enum WsStream {
Plain(TungsteniteStream<TcpStream>),
Tls(TungsteniteStream<TlsStream<TcpStream>>),
}
impl WsStream {
pub async fn connect(url: Url) -> Result<Self, io::Error> {
let Some(host) = url.host() else {
return Err(make_err("unknown or missing host"));
};
let Some(port) = url.port_or_known_default() else {
return Err(make_err("unknown or missing port"));
};
let use_tls = match url.scheme() {
"ws" => false,
"wss" => true,
s => return Err(make_err(format!("unsupported scheme: {s}"))),
};
let host = host.to_string();
let stream = TcpStream::connect((host.clone(), port)).await?;
let stream = if use_tls {
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
let stream = connector.connect(servname, stream).await?;
let stream = TlsStream::Client(stream);
let stream = async_tungstenite::client_async(url.to_string(), stream)
.await
.map_err(make_err)?
.0;
Self::Tls(stream)
} else {
let stream = async_tungstenite::client_async(url.to_string(), stream)
.await
.map_err(make_err)?
.0;
Self::Plain(stream)
};
Ok(stream)
}
}
impl Sink<Message> for WsStream {
type Error = TungsteniteError;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_ready(cx),
WsStream::Tls(s) => Pin::new(s).poll_ready(cx),
}
}
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).start_send(item),
WsStream::Tls(s) => Pin::new(s).start_send(item),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_flush(cx),
WsStream::Tls(s) => Pin::new(s).poll_flush(cx),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_close(cx),
WsStream::Tls(s) => Pin::new(s).poll_close(cx),
}
}
}
impl Stream for WsStream {
type Item = TungsteniteResult<Message>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match &mut *self {
WsStream::Plain(s) => Pin::new(s).poll_next(cx),
WsStream::Tls(s) => Pin::new(s).poll_next(cx),
}
}
}
fn make_err(e: impl ToString) -> io::Error {
io::Error::new(io::ErrorKind::Other, e.to_string())
}

View file

@ -1,78 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use lune_utils::TableBuilder;
use mlua::prelude::*;
pub(crate) mod body;
pub(crate) mod client;
pub(crate) mod server;
pub(crate) mod shared;
pub(crate) mod url;
use self::{
client::ws_stream::WsStream,
server::config::ServeConfig,
shared::{request::Request, response::Response, websocket::Websocket},
};
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `net` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `net` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
// No initial rustls setup is necessary, the respective
// functions lazily initialize anything there as needed
TableBuilder::new(lua)?
.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()
}
async fn net_request(lua: Lua, req: Request) -> LuaResult<Response> {
self::client::send_request(req, lua).await
}
async fn net_socket(_: Lua, url: String) -> LuaResult<Websocket<WsStream>> {
let url = url.parse().into_lua_err()?;
self::client::connect_websocket(url).await
}
async fn net_serve(lua: Lua, (port, config): (u16, ServeConfig)) -> LuaResult<LuaTable> {
self::server::serve(lua.clone(), port, config)
.await?
.into_lua_table(lua)
}
fn net_url_encode(
lua: &Lua,
(lua_string, as_binary): (LuaString, Option<bool>),
) -> LuaResult<LuaString> {
let as_binary = as_binary.unwrap_or_default();
let bytes = self::url::encode(lua_string, as_binary)?;
lua.create_string(bytes)
}
fn net_url_decode(
lua: &Lua,
(lua_string, as_binary): (LuaString, Option<bool>),
) -> LuaResult<LuaString> {
let as_binary = as_binary.unwrap_or_default();
let bytes = self::url::decode(lua_string, as_binary)?;
lua.create_string(bytes)
}

View file

@ -1,87 +0,0 @@
use std::net::{IpAddr, Ipv4Addr};
use mlua::prelude::*;
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",
},
}
"#;
#[derive(Debug, Clone)]
pub struct ServeConfig {
pub address: IpAddr,
pub handle_request: LuaFunction,
pub handle_web_socket: Option<LuaFunction>,
}
impl FromLua for ServeConfig {
fn from_lua(value: LuaValue, 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".to_string(),
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".to_string(),
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".to_string(),
message: None,
})
}
}
}

View file

@ -1,72 +0,0 @@
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use async_channel::{unbounded, Receiver, Sender};
use lune_utils::TableBuilder;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub struct ServeHandle {
addr: SocketAddr,
shutdown: Arc<AtomicBool>,
sender: Sender<()>,
}
impl ServeHandle {
pub fn new(addr: SocketAddr) -> (Self, Receiver<()>) {
let (sender, receiver) = unbounded();
let this = Self {
addr,
shutdown: Arc::new(AtomicBool::new(false)),
sender,
};
(this, receiver)
}
// TODO: Remove this in the next major release to use colon/self
// based call syntax and userdata implementation below instead
pub fn into_lua_table(self, lua: Lua) -> LuaResult<LuaTable> {
let shutdown = self.shutdown.clone();
let sender = self.sender.clone();
TableBuilder::new(lua)?
.with_value("ip", self.addr.ip().to_string())?
.with_value("port", self.addr.port())?
.with_function("stop", move |_, ()| {
if shutdown.load(Ordering::SeqCst) {
Err(LuaError::runtime("Server already stopped"))
} else {
shutdown.store(true, Ordering::SeqCst);
sender.try_send(()).ok();
sender.close();
Ok(())
}
})?
.build()
}
}
impl LuaUserData for ServeHandle {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ip", |_, this| Ok(this.addr.ip().to_string()));
fields.add_field_method_get("port", |_, this| Ok(this.addr.port()));
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_method("stop", |_, this, ()| {
if this.shutdown.load(Ordering::SeqCst) {
Err(LuaError::runtime("Server already stopped"))
} else {
this.shutdown.store(true, Ordering::SeqCst);
this.sender.try_send(()).ok();
this.sender.close();
Ok(())
}
});
}
}

View file

@ -1,121 +0,0 @@
use std::{cell::Cell, net::SocketAddr, rc::Rc};
use async_net::TcpListener;
use futures_lite::pin;
use hyper::server::conn::http1::Builder as Http1Builder;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use crate::{
server::{config::ServeConfig, handle::ServeHandle, service::Service},
shared::{
futures::{either, Either},
hyper::{HyperIo, HyperTimer},
},
};
pub mod config;
pub mod handle;
pub mod service;
pub mod upgrade;
/**
Starts an HTTP server using the given port and configuration.
Returns a `ServeHandle` that can be used to gracefully stop the server.
*/
pub async fn serve(lua: Lua, port: u16, config: ServeConfig) -> LuaResult<ServeHandle> {
let address = SocketAddr::from((config.address, port));
let service = Service {
lua: lua.clone(),
address,
config,
};
let listener = TcpListener::bind(address).await?;
let (handle, shutdown_rx) = ServeHandle::new(address);
lua.spawn_local({
let lua = lua.clone();
async move {
let handle_dropped = Rc::new(Cell::new(false));
loop {
// 1. Keep accepting new connections until we should shutdown
let (conn, addr) = if handle_dropped.get() {
// 1a. Handle has been dropped, and we don't need to listen for shutdown
match listener.accept().await {
Ok(acc) => acc,
Err(_err) => {
// TODO: Propagate error somehow
continue;
}
}
} else {
// 1b. Handle is possibly active, we must listen for shutdown
match either(shutdown_rx.recv(), listener.accept()).await {
Either::Left(Ok(())) => break,
Either::Left(Err(_)) => {
// NOTE #1: We will only get a RecvError 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.
handle_dropped.set(true);
continue;
}
Either::Right(Ok(acc)) => acc,
Either::Right(Err(_err)) => {
// TODO: Propagate error somehow
continue;
}
}
};
// 2. For each connection, spawn a new task to handle it
lua.spawn_local({
let rx = shutdown_rx.clone();
let io = HyperIo::from(conn);
let mut svc = service.clone();
svc.address = addr;
let handle_dropped = Rc::clone(&handle_dropped);
async move {
let conn = Http1Builder::new()
.writev(false)
.timer(HyperTimer)
.keep_alive(true)
.serve_connection(io, svc)
.with_upgrades();
if handle_dropped.get() {
if let Err(_err) = conn.await {
// TODO: Propagate error somehow
}
} else {
// NOTE #2: Because we use keep_alive for websockets above, we need to
// also manually poll this future and handle the graceful shutdown,
// otherwise the already accepted connection will linger and run
// even if the stop method has been called on the serve handle
pin!(conn);
match either(rx.recv(), conn.as_mut()).await {
Either::Left(Ok(())) => conn.as_mut().graceful_shutdown(),
Either::Left(Err(_)) => {
// Same as note #1
handle_dropped.set(true);
if let Err(_err) = conn.await {
// TODO: Propagate error somehow
}
}
Either::Right(Ok(())) => {}
Either::Right(Err(_err)) => {
// TODO: Propagate error somehow
}
}
}
}
});
}
}
});
Ok(handle)
}

View file

@ -1,116 +0,0 @@
use std::{future::Future, net::SocketAddr, pin::Pin};
use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
use hyper::{
body::Incoming, service::Service as HyperService, Request as HyperRequest,
Response as HyperResponse, StatusCode,
};
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
use crate::{
body::ReadableBody,
server::{
config::ServeConfig,
upgrade::{is_upgrade_request, make_upgrade_response},
},
shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket},
};
#[derive(Debug, Clone)]
pub(super) struct Service {
pub(super) lua: Lua,
pub(super) address: SocketAddr, // NOTE: This must be the remote address of the connected client
pub(super) config: ServeConfig,
}
impl HyperService<HyperRequest<Incoming>> for Service {
type Response = HyperResponse<ReadableBody>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: HyperRequest<Incoming>) -> Self::Future {
if is_upgrade_request(&req) {
if let Some(handler) = self.config.handle_web_socket.clone() {
let lua = self.lua.clone();
return Box::pin(async move {
let response = match make_upgrade_response(&req) {
Ok(res) => res,
Err(err) => {
return Ok(HyperResponse::builder()
.status(StatusCode::BAD_REQUEST)
.body(ReadableBody::from(err.to_string()))
.unwrap())
}
};
lua.spawn_local({
let lua = lua.clone();
async move {
if let Err(_err) = handle_websocket(lua, handler, req).await {
// TODO: Propagate the error somehow?
}
}
});
Ok(response)
});
}
}
let lua = self.lua.clone();
let address = self.address;
let handler = self.config.handle_request.clone();
Box::pin(async move {
match handle_request(lua, handler, req, address).await {
Ok(response) => Ok(response),
Err(_err) => {
// TODO: Propagate the error somehow?
Ok(HyperResponse::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(ReadableBody::from("Lune: Internal server error"))
.unwrap())
}
}
})
}
}
async fn handle_request(
lua: Lua,
handler: LuaFunction,
request: HyperRequest<Incoming>,
address: SocketAddr,
) -> LuaResult<HyperResponse<ReadableBody>> {
let request = Request::from_incoming(request, true)
.await?
.with_address(address);
let thread_id = lua.push_thread_back(handler, request)?;
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")?;
let response = Response::from_lua_multi(thread_res, &lua)?;
Ok(response.into_inner())
}
async fn handle_websocket(
lua: Lua,
handler: LuaFunction,
request: HyperRequest<Incoming>,
) -> LuaResult<()> {
let upgraded = hyper::upgrade::on(request).await.into_lua_err()?;
let stream =
WebSocketStream::from_raw_socket(HyperIo::from(upgraded), Role::Server, None).await;
let websocket = Websocket::from(stream);
lua.push_thread_back(handler, websocket)?;
Ok(())
}

View file

@ -1,56 +0,0 @@
use async_tungstenite::tungstenite::{error::ProtocolError, handshake::derive_accept_key};
use hyper::{
body::Incoming,
header::{HeaderName, CONNECTION, UPGRADE},
HeaderMap, Request as HyperRequest, Response as HyperResponse, StatusCode,
};
use crate::body::ReadableBody;
const SEC_WEBSOCKET_VERSION: HeaderName = HeaderName::from_static("sec-websocket-version");
const SEC_WEBSOCKET_KEY: HeaderName = HeaderName::from_static("sec-websocket-key");
const SEC_WEBSOCKET_ACCEPT: HeaderName = HeaderName::from_static("sec-websocket-accept");
pub fn is_upgrade_request(request: &HyperRequest<Incoming>) -> bool {
fn check_header_contains(headers: &HeaderMap, header_name: HeaderName, value: &str) -> bool {
headers.get(header_name).is_some_and(|header| {
header.to_str().map_or_else(
|_| false,
|header_str| {
header_str
.split(',')
.any(|part| part.trim().eq_ignore_ascii_case(value))
},
)
})
}
check_header_contains(request.headers(), CONNECTION, "Upgrade")
&& check_header_contains(request.headers(), UPGRADE, "websocket")
}
pub fn make_upgrade_response(
request: &HyperRequest<Incoming>,
) -> Result<HyperResponse<ReadableBody>, ProtocolError> {
let key = request
.headers()
.get(SEC_WEBSOCKET_KEY)
.ok_or(ProtocolError::MissingSecWebSocketKey)?;
if request
.headers()
.get(SEC_WEBSOCKET_VERSION)
.is_none_or(|v| v.as_bytes() != b"13")
{
return Err(ProtocolError::MissingSecWebSocketVersionHeader);
}
Ok(HyperResponse::builder()
.status(StatusCode::SWITCHING_PROTOCOLS)
.header(CONNECTION, "upgrade")
.header(UPGRADE, "websocket")
.header(SEC_WEBSOCKET_ACCEPT, derive_accept_key(key.as_bytes()))
.body(ReadableBody::from("switching to websocket protocol"))
.unwrap())
}

View file

@ -1,19 +0,0 @@
use futures_lite::prelude::*;
pub use http_body_util::Either;
/**
Combines the left and right futures into a single future
that resolves to either the left or right output.
This combinator is biased - if both futures resolve at
the same time, the left future's output is returned.
*/
pub fn either<L: Future, R: Future>(
left: L,
right: R,
) -> impl Future<Output = Either<L::Output, R::Output>> {
let fut_left = async move { Either::Left(left.await) };
let fut_right = async move { Either::Right(right.await) };
fut_left.or(fut_right)
}

View file

@ -1,88 +0,0 @@
use std::collections::HashMap;
use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
HeaderMap,
};
use lune_utils::TableBuilder;
use mlua::prelude::*;
pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
let version_global = lua
.globals()
.get::<LuaString>("_VERSION")
.expect("Missing _VERSION global");
let version_global_str = version_global
.to_str()
.context("Invalid utf8 found in _VERSION global")?;
let (package_name, full_version) = version_global_str.split_once(' ').unwrap();
Ok(format!("{}/{}", package_name.to_lowercase(), full_version))
}
pub fn header_map_to_table(
lua: &Lua,
headers: HeaderMap,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, value) in headers {
if let Some(name) = name {
if let Ok(value) = value.to_str() {
string_map
.entry(name.to_string())
.or_default()
.push(value.to_owned());
}
}
}
hash_map_to_table(lua, string_map, remove_content_headers)
}
pub fn hash_map_to_table(
lua: &Lua,
map: impl IntoIterator<Item = (String, Vec<String>)>,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, values) in map {
let name = name.as_str();
if remove_content_headers {
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
if name == content_encoding_header_str || name == content_length_header_str {
continue;
}
}
for value in values {
let value = value.as_str();
string_map
.entry(name.to_owned())
.or_default()
.push(value.to_owned());
}
}
let mut builder = TableBuilder::new(lua.clone())?;
for (name, mut values) in string_map {
if values.len() == 1 {
let value = values.pop().unwrap().into_lua(lua)?;
builder = builder.with_value(name, value)?;
} else {
let values = TableBuilder::new(lua.clone())?
.with_sequential_values(values)?
.build_readonly()?
.into_lua(lua)?;
builder = builder.with_value(name, values)?;
}
}
builder.build_readonly()
}

View file

@ -1,198 +0,0 @@
use std::{
future::Future,
io,
pin::Pin,
slice,
task::{Context, Poll},
time::{Duration, Instant},
};
use async_io::Timer;
use futures_lite::{prelude::*, ready};
use hyper::rt::{self, Executor, ReadBuf, ReadBufCursor};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
// Hyper executor that spawns futures onto our Lua scheduler
#[derive(Debug, Clone)]
pub struct HyperExecutor {
lua: Lua,
}
#[allow(dead_code)]
impl HyperExecutor {
pub fn execute<Fut>(lua: Lua, fut: Fut)
where
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
let exec = if let Some(exec) = lua.app_data_ref::<Self>() {
exec
} else {
lua.set_app_data(Self { lua: lua.clone() });
lua.app_data_ref::<Self>().unwrap()
};
exec.execute(fut);
}
}
impl<Fut: Future + Send + 'static> rt::Executor<Fut> for HyperExecutor
where
Fut::Output: Send + 'static,
{
fn execute(&self, fut: Fut) {
self.lua.spawn(fut).detach();
}
}
// Hyper timer & sleep future wrapper for async-io
#[derive(Debug)]
pub struct HyperTimer;
impl rt::Timer for HyperTimer {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn rt::Sleep>> {
Box::pin(HyperSleep::from(Timer::after(duration)))
}
fn sleep_until(&self, at: Instant) -> Pin<Box<dyn rt::Sleep>> {
Box::pin(HyperSleep::from(Timer::at(at)))
}
fn reset(&self, sleep: &mut Pin<Box<dyn rt::Sleep>>, new_deadline: Instant) {
if let Some(mut sleep) = sleep.as_mut().downcast_mut_pin::<HyperSleep>() {
sleep.inner.set_at(new_deadline);
} else {
*sleep = Box::pin(HyperSleep::from(Timer::at(new_deadline)));
}
}
}
#[derive(Debug)]
pub struct HyperSleep {
inner: Timer,
}
impl From<Timer> for HyperSleep {
fn from(inner: Timer) -> Self {
Self { inner }
}
}
impl Future for HyperSleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match Pin::new(&mut self.inner).poll(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
}
impl rt::Sleep for HyperSleep {}
// Hyper I/O wrapper for bidirectional compatibility
// between hyper & futures-lite async read/write traits
pin_project_lite::pin_project! {
#[derive(Debug)]
pub struct HyperIo<T> {
#[pin]
inner: T
}
}
impl<T> From<T> for HyperIo<T> {
fn from(inner: T) -> Self {
Self { inner }
}
}
impl<T> HyperIo<T> {
pub fn pin_mut(self: Pin<&mut Self>) -> Pin<&mut T> {
self.project().inner
}
}
// Compat for futures-lite -> hyper runtime
impl<T: AsyncRead> rt::Read for HyperIo<T> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: ReadBufCursor<'_>,
) -> Poll<io::Result<()>> {
// Fill the read buffer with initialized data
let read_slice = unsafe {
let buffer = buf.as_mut();
buffer.as_mut_ptr().write_bytes(0, buffer.len());
slice::from_raw_parts_mut(buffer.as_mut_ptr().cast::<u8>(), buffer.len())
};
// Read bytes from the underlying source
let n = match self.pin_mut().poll_read(cx, read_slice) {
Poll::Ready(Ok(n)) => n,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
};
unsafe {
buf.advance(n);
}
Poll::Ready(Ok(()))
}
}
impl<T: AsyncWrite> rt::Write for HyperIo<T> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_close(cx)
}
}
// Compat for hyper runtime -> futures-lite
impl<T: rt::Read> AsyncRead for HyperIo<T> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let mut buf = ReadBuf::new(buf);
ready!(self.pin_mut().poll_read(cx, buf.unfilled()))?;
Poll::Ready(Ok(buf.filled().len()))
}
}
impl<T: rt::Write> AsyncWrite for HyperIo<T> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
self.pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
self.pin_mut().poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.pin_mut().poll_shutdown(cx)
}
}

View file

@ -1,41 +0,0 @@
use hyper::{
header::{HeaderName, HeaderValue},
HeaderMap, Method,
};
use mlua::prelude::*;
pub fn lua_value_to_method(value: &LuaValue) -> LuaResult<Method> {
match value {
LuaValue::Nil => Ok(Method::GET),
LuaValue::String(str) => {
let bytes = str.as_bytes().trim_ascii().to_ascii_uppercase();
Method::from_bytes(&bytes).into_lua_err()
}
LuaValue::Buffer(buf) => {
let bytes = buf.to_vec().trim_ascii().to_ascii_uppercase();
Method::from_bytes(&bytes).into_lua_err()
}
v => Err(LuaError::FromLuaConversionError {
from: v.type_name(),
to: "Method".to_string(),
message: Some(format!(
"Invalid method - expected string or buffer, got {}",
v.type_name()
)),
}),
}
}
pub fn lua_table_to_header_map(table: &LuaTable) -> LuaResult<HeaderMap> {
let mut headers = HeaderMap::new();
for pair in table.pairs::<LuaString, LuaString>() {
let (key, val) = pair?;
let key = HeaderName::from_bytes(&key.as_bytes()).into_lua_err()?;
let val = HeaderValue::from_bytes(&val.as_bytes()).into_lua_err()?;
headers.insert(key, val);
}
Ok(headers)
}

View file

@ -1,7 +0,0 @@
pub mod futures;
pub mod headers;
pub mod hyper;
pub mod lua;
pub mod request;
pub mod response;
pub mod websocket;

View file

@ -1,256 +0,0 @@
use std::{collections::HashMap, net::SocketAddr};
use url::Url;
use hyper::{body::Incoming, HeaderMap, Method, Request as HyperRequest};
use mlua::prelude::*;
use crate::{
body::{handle_incoming_body, ReadableBody},
shared::{
headers::{hash_map_to_table, header_map_to_table},
lua::{lua_table_to_header_map, lua_value_to_method},
},
};
#[derive(Debug, Clone)]
pub struct RequestOptions {
pub decompress: bool,
}
impl Default for RequestOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl FromLua for RequestOptions {
fn from_lua(value: LuaValue, _: &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 options".to_string(),
)),
}?;
Ok(Self { decompress })
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestOptions".to_string(),
message: Some(format!(
"Invalid request options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
}
#[derive(Debug, Clone)]
pub struct Request {
pub(crate) inner: HyperRequest<ReadableBody>,
pub(crate) address: Option<SocketAddr>,
pub(crate) redirects: Option<usize>,
pub(crate) decompress: bool,
}
impl Request {
/**
Creates a new request from a raw incoming request.
*/
pub async fn from_incoming(
incoming: HyperRequest<Incoming>,
decompress: bool,
) -> LuaResult<Self> {
let (parts, body) = incoming.into_parts();
let (body, decompress) = handle_incoming_body(&parts.headers, body, decompress).await?;
Ok(Self {
inner: HyperRequest::from_parts(parts, ReadableBody::from(body)),
address: None,
redirects: None,
decompress,
})
}
/**
Attaches a socket address to the request.
This will make the `ip` and `port` fields available on the request.
*/
pub fn with_address(mut self, address: SocketAddr) -> Self {
self.address = Some(address);
self
}
/**
Returns the method of the request.
*/
pub fn method(&self) -> Method {
self.inner.method().clone()
}
/**
Returns the path of the request.
*/
pub fn path(&self) -> &str {
self.inner.uri().path()
}
/**
Returns the query parameters of the request.
*/
pub fn query(&self) -> HashMap<String, Vec<String>> {
let uri = self.inner.uri();
let mut result = HashMap::<String, Vec<String>>::new();
if let Some(query) = uri.query() {
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
result
.entry(key.to_string())
.or_default()
.push(value.to_string());
}
}
result
}
/**
Returns the headers of the request.
*/
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
/**
Returns the body of the request.
*/
pub fn body(&self) -> &[u8] {
self.inner.body().as_slice()
}
/**
Clones the inner `hyper` request.
*/
#[allow(dead_code)]
pub fn clone_inner(&self) -> HyperRequest<ReadableBody> {
self.inner.clone()
}
/**
Takes the inner `hyper` request by ownership.
*/
#[allow(dead_code)]
pub fn into_inner(self) -> HyperRequest<ReadableBody> {
self.inner
}
}
impl FromLua for Request {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
// If we just got a string we assume
// its a GET request to a given url
let uri = s.to_str()?;
let uri = uri.parse().into_lua_err()?;
let mut request = HyperRequest::new(ReadableBody::empty());
*request.uri_mut() = uri;
Ok(Self {
inner: request,
address: None,
redirects: None,
decompress: RequestOptions::default().decompress,
})
} else if let LuaValue::Table(tab) = value {
// If we got a table we are able to configure the
// entire request, maybe with extra options too
let options = match tab.get::<LuaValue>("options") {
Ok(opts) => RequestOptions::from_lua(opts, lua)?,
Err(_) => RequestOptions::default(),
};
// Extract url (required) + optional structured query params
let url = tab.get::<LuaString>("url")?;
let mut url = url.to_str()?.parse::<Url>().into_lua_err()?;
if let Some(t) = tab.get::<Option<LuaTable>>("query")? {
let mut query = url.query_pairs_mut();
for pair in t.pairs::<LuaString, LuaString>() {
let (key, value) = pair?;
let key = key.to_str()?;
let value = value.to_str()?;
query.append_pair(&key, &value);
}
}
// Extract method
let method = tab.get::<LuaValue>("method")?;
let method = lua_value_to_method(&method)?;
// Extract headers
let headers = tab.get::<Option<LuaTable>>("headers")?;
let headers = headers
.map(|t| lua_table_to_header_map(&t))
.transpose()?
.unwrap_or_default();
// Extract body
let body = tab.get::<ReadableBody>("body")?;
// Build the full request
let mut request = HyperRequest::new(body);
request.headers_mut().extend(headers);
*request.uri_mut() = url.to_string().parse().unwrap();
*request.method_mut() = method;
// All good, validated and we got what we need
Ok(Self {
inner: request,
address: None,
redirects: None,
decompress: options.decompress,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "Request".to_string(),
message: Some(format!(
"Invalid request - expected string or table, got {}",
value.type_name()
)),
})
}
}
}
impl LuaUserData for Request {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ip", |_, this| {
Ok(this.address.map(|address| address.ip().to_string()))
});
fields.add_field_method_get("port", |_, this| {
Ok(this.address.map(|address| address.port()))
});
fields.add_field_method_get("method", |_, this| Ok(this.method().to_string()));
fields.add_field_method_get("path", |_, this| Ok(this.path().to_string()));
fields.add_field_method_get("query", |lua, this| {
hash_map_to_table(lua, this.query(), false)
});
fields.add_field_method_get("headers", |lua, this| {
header_map_to_table(lua, this.headers().clone(), this.decompress)
});
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
}
}

View file

@ -1,153 +0,0 @@
use hyper::{
body::Incoming,
header::{HeaderValue, CONTENT_TYPE},
HeaderMap, Response as HyperResponse, StatusCode,
};
use mlua::prelude::*;
use crate::{
body::{handle_incoming_body, ReadableBody},
shared::{headers::header_map_to_table, lua::lua_table_to_header_map},
};
#[derive(Debug, Clone)]
pub struct Response {
pub(crate) inner: HyperResponse<ReadableBody>,
pub(crate) decompressed: bool,
}
impl Response {
/**
Creates a new response from a raw incoming response.
*/
pub async fn from_incoming(
incoming: HyperResponse<Incoming>,
decompress: bool,
) -> LuaResult<Self> {
let (parts, body) = incoming.into_parts();
let (body, decompressed) = handle_incoming_body(&parts.headers, body, decompress).await?;
Ok(Self {
inner: HyperResponse::from_parts(parts, ReadableBody::from(body)),
decompressed,
})
}
/**
Returns whether the request was successful or not.
*/
pub fn status_ok(&self) -> bool {
self.inner.status().is_success()
}
/**
Returns the status code of the response.
*/
pub fn status_code(&self) -> u16 {
self.inner.status().as_u16()
}
/**
Returns the status message of the response.
*/
pub fn status_message(&self) -> &str {
self.inner.status().canonical_reason().unwrap_or_default()
}
/**
Returns the headers of the response.
*/
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
/**
Returns the body of the response.
*/
pub fn body(&self) -> &[u8] {
self.inner.body().as_slice()
}
/**
Clones the inner `hyper` response.
*/
#[allow(dead_code)]
pub fn clone_inner(&self) -> HyperResponse<ReadableBody> {
self.inner.clone()
}
/**
Takes the inner `hyper` response by ownership.
*/
#[allow(dead_code)]
pub fn into_inner(self) -> HyperResponse<ReadableBody> {
self.inner
}
}
impl FromLua for Response {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
if let Ok(body) = ReadableBody::from_lua(value.clone(), lua) {
// String or buffer is always a 200 text/plain response
let mut response = HyperResponse::new(body);
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
Ok(Self {
inner: response,
decompressed: false,
})
} else if let LuaValue::Table(tab) = value {
// Extract status (required)
let status = tab.get::<u16>("status")?;
let status = StatusCode::from_u16(status).into_lua_err()?;
// Extract headers
let headers = tab.get::<Option<LuaTable>>("headers")?;
let headers = headers
.map(|t| lua_table_to_header_map(&t))
.transpose()?
.unwrap_or_default();
// Extract body
let body = tab.get::<ReadableBody>("body")?;
// Build the full response
let mut response = HyperResponse::new(body);
response.headers_mut().extend(headers);
*response.status_mut() = status;
// All good, validated and we got what we need
Ok(Self {
inner: response,
decompressed: false,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "Response".to_string(),
message: Some(format!(
"Invalid response - expected table/string/buffer, got {}",
value.type_name()
)),
})
}
}
}
impl LuaUserData for Response {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ok", |_, this| Ok(this.status_ok()));
fields.add_field_method_get("statusCode", |_, this| Ok(this.status_code()));
fields.add_field_method_get("statusMessage", |lua, this| {
lua.create_string(this.status_message())
});
fields.add_field_method_get("headers", |lua, this| {
header_map_to_table(lua, this.headers().clone(), this.decompressed)
});
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
}
}

View file

@ -1,143 +0,0 @@
use std::{
error::Error,
sync::{
atomic::{AtomicBool, AtomicU16, Ordering},
Arc,
},
};
use async_lock::Mutex as AsyncMutex;
use async_tungstenite::tungstenite::{
protocol::{frame::coding::CloseCode, CloseFrame},
Message as TungsteniteMessage, Result as TungsteniteResult, Utf8Bytes,
};
use bstr::{BString, ByteSlice};
use futures::{
stream::{SplitSink, SplitStream},
Sink, SinkExt, Stream, StreamExt,
};
use hyper::body::Bytes;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub struct Websocket<T> {
close_code_exists: Arc<AtomicBool>,
close_code_value: Arc<AtomicU16>,
read_stream: Arc<AsyncMutex<SplitStream<T>>>,
write_stream: Arc<AsyncMutex<SplitSink<T, TungsteniteMessage>>>,
}
impl<T> Websocket<T>
where
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
{
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: TungsteniteMessage) -> LuaResult<()> {
let mut ws = self.write_stream.lock().await;
ws.send(msg).await.into_lua_err()
}
pub async fn next(&self) -> LuaResult<Option<TungsteniteMessage>> {
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(TungsteniteMessage::Close(Some(CloseFrame {
code: match code {
Some(code) if (1000..=4999).contains(&code) => CloseCode::from(code),
Some(code) => {
return Err(LuaError::runtime(format!(
"Close code must be between 1000 and 4999, got {code}"
)))
}
None => CloseCode::Normal,
},
reason: "".into(),
})))
.await?;
let mut ws = self.write_stream.lock().await;
ws.close().await.into_lua_err()
}
}
impl<T> From<T> for Websocket<T>
where
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
{
fn from(value: 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)),
}
}
}
impl<T> LuaUserData for Websocket<T>
where
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
{
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code()));
}
fn add_methods<M: LuaUserDataMethods<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() {
TungsteniteMessage::Binary(Bytes::from(string.to_vec()))
} else {
let s = string.to_str().into_lua_err()?;
TungsteniteMessage::Text(Utf8Bytes::from(s))
})
.await
},
);
methods.add_async_method("next", |lua, this, (): ()| async move {
let msg = this.next().await?;
if let Some(TungsteniteMessage::Close(Some(frame))) = msg.as_ref() {
this.set_close_code(frame.code.into());
}
Ok(match msg {
Some(TungsteniteMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?),
Some(TungsteniteMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?),
Some(TungsteniteMessage::Close(_)) | None => LuaValue::Nil,
// Ignore ping/pong/frame messages, they are handled by tungstenite
msg => unreachable!("Unhandled message: {:?}", msg),
})
});
}
}

View file

@ -1,12 +0,0 @@
use mlua::prelude::*;
pub fn decode(lua_string: LuaString, as_binary: bool) -> LuaResult<Vec<u8>> {
if as_binary {
Ok(urlencoding::decode_binary(&lua_string.as_bytes()).into_owned())
} else {
Ok(urlencoding::decode(&lua_string.to_str()?)
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
.into_owned()
.into_bytes())
}
}

View file

@ -1,13 +0,0 @@
use mlua::prelude::*;
pub fn encode(lua_string: LuaString, as_binary: bool) -> LuaResult<Vec<u8>> {
if as_binary {
Ok(urlencoding::encode_binary(&lua_string.as_bytes())
.into_owned()
.into_bytes())
} else {
Ok(urlencoding::encode(&lua_string.to_str()?)
.into_owned()
.into_bytes())
}
}

View file

@ -1,5 +0,0 @@
mod decode;
mod encode;
pub use self::decode::decode;
pub use self::encode::encode;

View file

@ -1,32 +0,0 @@
[package]
name = "lune-std-process"
version = "0.2.2"
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.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
directories = "6.0"
pin-project = "1.0"
bstr = "1.9"
bytes = "1.6.0"
async-channel = "2.3"
async-lock = "3.4"
async-process = "2.3"
blocking = "1.6"
futures-lite = "2.6"
futures-util = "0.3" # Needed for select! macro...
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,86 +0,0 @@
use std::process::ExitStatus;
use async_channel::{unbounded, Receiver, Sender};
use async_process::Child as AsyncChild;
use futures_util::{select, FutureExt};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use lune_utils::TableBuilder;
use super::{ChildReader, ChildWriter};
#[derive(Debug, Clone)]
pub struct Child {
stdin: ChildWriter,
stdout: ChildReader,
stderr: ChildReader,
kill_tx: Sender<()>,
status_rx: Receiver<Option<ExitStatus>>,
}
impl Child {
pub fn new(lua: &Lua, mut child: AsyncChild) -> Self {
let stdin = ChildWriter::from(child.stdin.take());
let stdout = ChildReader::from(child.stdout.take());
let stderr = ChildReader::from(child.stderr.take());
// NOTE: Kill channel is zero size, status is very small
// and implements Copy, unbounded will be just fine here
let (kill_tx, kill_rx) = unbounded();
let (status_tx, status_rx) = unbounded();
lua.spawn(handle_child(child, kill_rx, status_tx)).detach();
Self {
stdin,
stdout,
stderr,
kill_tx,
status_rx,
}
}
}
impl LuaUserData for Child {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("stdin", |_, this| Ok(this.stdin.clone()));
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.clone()));
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.clone()));
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_method("kill", |_, this, (): ()| {
let _ = this.kill_tx.try_send(());
Ok(())
});
methods.add_async_method("status", |lua, this, (): ()| {
let rx = this.status_rx.clone();
async move {
let status = rx.recv().await.ok().flatten();
let code = status.and_then(|c| c.code()).unwrap_or(9);
TableBuilder::new(lua.clone())?
.with_value("ok", code == 0)?
.with_value("code", code)?
.build_readonly()
}
});
}
}
async fn handle_child(
mut child: AsyncChild,
kill_rx: Receiver<()>,
status_tx: Sender<Option<ExitStatus>>,
) {
let status = select! {
s = child.status().fuse() => s.ok(), // FUTURE: Propagate this error somehow?
_ = kill_rx.recv().fuse() => {
let _ = child.kill(); // Will only error if already killed
None
}
};
// Will only error if there are no receivers waiting for the status
let _ = status_tx.send(status).await;
}

View file

@ -1,117 +0,0 @@
use std::sync::Arc;
use async_lock::Mutex as AsyncMutex;
use async_process::{ChildStderr as AsyncChildStderr, ChildStdout as AsyncChildStdout};
use futures_lite::prelude::*;
use mlua::prelude::*;
const DEFAULT_BUFFER_SIZE: usize = 1024;
// Inner (plumbing) implementation
#[derive(Debug)]
enum ChildReaderInner {
None,
Stdout(AsyncChildStdout),
Stderr(AsyncChildStderr),
}
impl ChildReaderInner {
async fn read(&mut self, size: usize) -> Result<Vec<u8>, std::io::Error> {
if matches!(self, ChildReaderInner::None) {
return Ok(Vec::new());
}
let mut buf = vec![0; size];
let read = match self {
ChildReaderInner::None => unreachable!(),
ChildReaderInner::Stdout(stdout) => stdout.read(&mut buf).await?,
ChildReaderInner::Stderr(stderr) => stderr.read(&mut buf).await?,
};
buf.truncate(read);
Ok(buf)
}
async fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
let mut buf = Vec::new();
let read = match self {
ChildReaderInner::None => 0,
ChildReaderInner::Stdout(stdout) => stdout.read_to_end(&mut buf).await?,
ChildReaderInner::Stderr(stderr) => stderr.read_to_end(&mut buf).await?,
};
buf.truncate(read);
Ok(buf)
}
}
impl From<AsyncChildStdout> for ChildReaderInner {
fn from(stdout: AsyncChildStdout) -> Self {
Self::Stdout(stdout)
}
}
impl From<AsyncChildStderr> for ChildReaderInner {
fn from(stderr: AsyncChildStderr) -> Self {
Self::Stderr(stderr)
}
}
impl From<Option<AsyncChildStdout>> for ChildReaderInner {
fn from(stdout: Option<AsyncChildStdout>) -> Self {
stdout.map_or(Self::None, Into::into)
}
}
impl From<Option<AsyncChildStderr>> for ChildReaderInner {
fn from(stderr: Option<AsyncChildStderr>) -> Self {
stderr.map_or(Self::None, Into::into)
}
}
// Outer (lua-accessible, clonable) implementation
#[derive(Debug, Clone)]
pub struct ChildReader {
inner: Arc<AsyncMutex<ChildReaderInner>>,
}
impl LuaUserData for ChildReader {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("read", |lua, this, size: Option<usize>| {
let inner = this.inner.clone();
let size = size.unwrap_or(DEFAULT_BUFFER_SIZE);
async move {
let mut inner = inner.lock().await;
let bytes = inner.read(size).await.into_lua_err()?;
if bytes.is_empty() {
Ok(LuaValue::Nil)
} else {
Ok(LuaValue::String(lua.create_string(bytes)?))
}
}
});
methods.add_async_method("readToEnd", |lua, this, (): ()| {
let inner = this.inner.clone();
async move {
let mut inner = inner.lock().await;
let bytes = inner.read_to_end().await.into_lua_err()?;
Ok(lua.create_string(bytes))
}
});
}
}
impl<T: Into<ChildReaderInner>> From<T> for ChildReader {
fn from(inner: T) -> Self {
Self {
inner: Arc::new(AsyncMutex::new(inner.into())),
}
}
}

View file

@ -1,79 +0,0 @@
use std::sync::Arc;
use async_lock::Mutex as AsyncMutex;
use async_process::ChildStdin as AsyncChildStdin;
use futures_lite::prelude::*;
use bstr::BString;
use mlua::prelude::*;
// Inner (plumbing) implementation
#[derive(Debug)]
enum ChildWriterInner {
None,
Stdin(AsyncChildStdin),
}
impl ChildWriterInner {
async fn write(&mut self, data: Vec<u8>) -> Result<(), std::io::Error> {
match self {
ChildWriterInner::None => Ok(()),
ChildWriterInner::Stdin(stdin) => stdin.write_all(&data).await,
}
}
async fn close(&mut self) -> Result<(), std::io::Error> {
match self {
ChildWriterInner::None => Ok(()),
ChildWriterInner::Stdin(stdin) => stdin.close().await,
}
}
}
impl From<AsyncChildStdin> for ChildWriterInner {
fn from(stdin: AsyncChildStdin) -> Self {
ChildWriterInner::Stdin(stdin)
}
}
impl From<Option<AsyncChildStdin>> for ChildWriterInner {
fn from(stdin: Option<AsyncChildStdin>) -> Self {
stdin.map_or(Self::None, Into::into)
}
}
// Outer (lua-accessible, clonable) implementation
#[derive(Debug, Clone)]
pub struct ChildWriter {
inner: Arc<AsyncMutex<ChildWriterInner>>,
}
impl LuaUserData for ChildWriter {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("write", |_, this, data: BString| {
let inner = this.inner.clone();
let data = data.to_vec();
async move {
let mut inner = inner.lock().await;
inner.write(data).await.into_lua_err()
}
});
methods.add_async_method("close", |_, this, (): ()| {
let inner = this.inner.clone();
async move {
let mut inner = inner.lock().await;
inner.close().await.into_lua_err()
}
});
}
}
impl<T: Into<ChildWriterInner>> From<T> for ChildWriter {
fn from(inner: T) -> Self {
Self {
inner: Arc::new(AsyncMutex::new(inner.into())),
}
}
}

View file

@ -1,7 +0,0 @@
mod child;
mod child_reader;
mod child_writer;
pub use self::child::Child;
pub use self::child_reader::ChildReader;
pub use self::child_writer::ChildWriter;

View file

@ -1,51 +0,0 @@
use async_process::Child;
use futures_lite::prelude::*;
use mlua::prelude::*;
use lune_utils::TableBuilder;
use super::options::ProcessSpawnOptionsStdioKind;
mod tee_writer;
mod wait_for_child;
use self::wait_for_child::wait_for_child;
pub async fn exec(
lua: Lua,
mut child: Child,
stdin: Option<Vec<u8>>,
stdout: ProcessSpawnOptionsStdioKind,
stderr: ProcessSpawnOptionsStdioKind,
) -> LuaResult<LuaTable> {
// Write to stdin before anything else - if we got it
if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?;
}
let res = wait_for_child(child, stdout, stderr).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
let stdout = lua.create_string(&res.stdout)?;
let stderr = lua.create_string(&res.stderr)?;
TableBuilder::new(lua)?
.with_value("ok", code == 0)?
.with_value("code", code)?
.with_value("stdout", stdout)?
.with_value("stderr", stderr)?
.build_readonly()
}

View file

@ -1,118 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
env::consts::{ARCH, OS},
path::MAIN_SEPARATOR,
process::Stdio,
};
use mlua::prelude::*;
use mlua_luau_scheduler::Functions;
use lune_utils::{
path::get_current_dir,
process::{ProcessArgs, ProcessEnv},
TableBuilder,
};
mod create;
mod exec;
mod options;
use self::options::ProcessSpawnOptions;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `process` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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"
})?;
// Extract stored userdatas for args + env, the runtime struct should always provide this
let process_args = lua
.app_data_ref::<ProcessArgs>()
.ok_or_else(|| LuaError::runtime("Missing process args in Lua app data"))?
.clone();
let process_env = lua
.app_data_ref::<ProcessEnv>()
.ok_or_else(|| LuaError::runtime("Missing process env in Lua app data"))?
.clone();
// Create our process exit function, the scheduler crate provides this
let fns = Functions::new(lua.clone())?;
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", process_args)?
.with_value("cwd", cwd_str)?
.with_value("env", process_env)?
.with_value("exit", process_exit)?
.with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly()
}
async fn process_exec(
lua: Lua,
(program, args, mut options): (String, ProcessArgs, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
let stdin = options.stdio.stdin.take();
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()?;
exec::exec(lua, child, stdin, stdout, stderr).await
}
fn process_create(
lua: &Lua,
(program, args, options): (String, ProcessArgs, ProcessSpawnOptions),
) -> LuaResult<LuaValue> {
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
create::Child::new(lua, child).into_lua(lua)
}

View file

@ -1,362 +0,0 @@
export type OS = "linux" | "macos" | "windows"
export type Arch = "x86_64" | "aarch64"
export type Endianness = "big" | "little"
--[=[
@interface ExecStdioKind
@within Process
Enum determining how to treat a standard input/output stream for `process.exec`.
Can be one of the following values:
* `default` - The default behavior, writing to the final result table only
* `inherit` - Inherit the stream from the parent process, writing to both the result table and the respective stream for the parent process
* `forward` - Forward the stream to the parent process, without writing to the result table, only respective stream for the parent process
* `none` - Do not create any input/output stream
]=]
export type ExecStdioKind = "default" | "inherit" | "forward" | "none"
--[=[
@interface ExecStdioOptions
@within Process
A dictionary of stdio-specific options for `process.exec`, with the following available values:
* `stdin` - A buffer or string to write to the stdin of the process
* `stdout` - How to treat the stdout stream from the child process - see `ExecStdioKind` for more info
* `stderr` - How to treat the stderr stream from the child process - see `ExecStdioKind` for more info
]=]
export type ExecStdioOptions = {
stdin: (buffer | string)?,
stdout: ExecStdioKind?,
stderr: ExecStdioKind?,
}
--[=[
@interface ExecOptions
@within Process
A dictionary of options for `process.exec`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `StdioKind` and `StdioOptions` for more info
]=]
export type ExecOptions = {
cwd: string?,
env: { [string]: string }?,
shell: (boolean | string)?,
stdio: (ExecStdioKind | ExecStdioOptions)?,
}
--[=[
@interface CreateOptions
@within Process
A dictionary of options for `process.create`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
]=]
export type CreateOptions = {
cwd: string?,
env: { [string]: string }?,
shell: (boolean | string)?,
}
--[=[
@class ChildProcessReader
@within Process
A reader class to read data from a child process' streams in realtime.
]=]
local ChildProcessReader = {}
--[=[
@within ChildProcessReader
Reads a chunk of data up to the specified length, or a default of 1KB at a time.
Returns nil if there is no more data to read.
This function may yield until there is new data to read from reader, if all data
till present has already been read, and the process has not exited.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:read(chunkSize: number?): string?
return nil :: any
end
--[=[
@within ChildProcessReader
Reads all the data currently present in the reader as a string.
This function will yield until the process exits.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:readToEnd(): string
return nil :: any
end
--[=[
@class ChildProcessWriter
@within Process
A writer class to write data to a child process' streams in realtime.
]=]
local ChildProcessWriter = {}
--[=[
@within ChildProcessWriter
Writes a buffer or string of data to the writer.
@param data The data to write to the writer
]=]
function ChildProcessWriter:write(data: buffer | string): ()
return nil :: any
end
--[=[
@within ChildProcessWriter
Closes the underlying I/O stream for the writer.
]=]
function ChildProcessWriter:close(): ()
return nil :: any
end
--[=[
@interface ChildProcess
@within Process
Result type for child processes in `process.create`.
This is a dictionary containing the following values:
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
* `kill` - A method that kills the child process
* `status` - A method that yields and returns the exit status of the child process
]=]
export type ChildProcess = {
stdin: typeof(ChildProcessWriter),
stdout: typeof(ChildProcessReader),
stderr: typeof(ChildProcessReader),
kill: (self: ChildProcess) -> (),
status: (self: ChildProcess) -> {
ok: boolean,
code: number,
},
}
--[=[
@interface ExecResult
@within Process
Result type for child processes in `process.exec`.
This is a dictionary containing the following values:
* `ok` - If the child process exited successfully or not, meaning the exit code was zero or not set
* `code` - The exit code set by the child process, or 0 if one was not set
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
]=]
export type ExecResult = {
ok: boolean,
code: number,
stdout: string,
stderr: string,
}
--[=[
@class Process
Built-in functions for the current process & child processes
### Example usage
```lua
local process = require("@lune/process")
-- Getting the arguments passed to the Lune script
for index, arg in process.args do
print("Process argument #" .. tostring(index) .. ": " .. arg)
end
-- Getting the currently available environment variables
local PORT: string? = process.env.PORT
local HOME: string? = process.env.HOME
for name, value in process.env do
print("Environment variable " .. name .. " is set to " .. value)
end
-- Getting the current os and processor architecture
print("Running " .. process.os .. " on " .. process.arch .. "!")
-- Executing a command
local result = process.exec("program", {
"cli argument",
"other cli argument"
})
if result.ok then
print(result.stdout)
else
print(result.stderr)
end
-- Spawning a child process
local child = process.create("program", {
"cli argument",
"other cli argument"
})
-- Writing to the child process' stdin
child.stdin:write("Hello from Lune!")
-- Reading from the child process' stdout
local data = child.stdout:read()
print(data)
```
]=]
local process = {}
--[=[
@within Process
@prop os OS
@tag read_only
The current operating system being used.
Possible values:
* `"linux"`
* `"macos"`
* `"windows"`
]=]
process.os = (nil :: any) :: OS
--[=[
@within Process
@prop arch Arch
@tag read_only
The architecture of the processor currently being used.
Possible values:
* `"x86_64"`
* `"aarch64"`
]=]
process.arch = (nil :: any) :: Arch
--[=[
@within Process
@prop endianness Endianness
@tag read_only
The endianness of the processor currently being used.
Possible values:
* `"big"`
* `"little"`
]=]
process.endianness = (nil :: any) :: Endianness
--[=[
@within Process
@prop args { string }
@tag read_only
The arguments given when running the Lune script.
]=]
process.args = (nil :: any) :: { string }
--[=[
@within Process
@prop cwd string
@tag read_only
The current working directory in which the Lune script is running.
]=]
process.cwd = (nil :: any) :: string
--[=[
@within Process
@prop env { [string]: string? }
@tag read_write
Current environment variables for this process.
Setting a value on this table will set the corresponding environment variable.
]=]
process.env = (nil :: any) :: { [string]: string? }
--[=[
@within Process
Exits the currently running script as soon as possible with the given exit code.
Exit code 0 is treated as a successful exit, any other value is treated as an error.
Setting the exit code using this function will override any otherwise automatic exit code.
@param code The exit code to set
]=]
function process.exit(code: number?): never
return nil :: any
end
--[=[
@within Process
Spawns a child process in the background that runs the program `program`,
and immediately returns readers and writers to communicate with it.
In order to execute a command and wait for its output, see `process.exec`.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
@param program The program to Execute as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary with the readers and writers to communicate with the child process
]=]
function process.create(program: string, params: { string }?, options: CreateOptions?): ChildProcess
return nil :: any
end
--[=[
@within Process
Executes a child process that will execute the command `program`, waiting for it to exit.
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
In order to spawn a child process in the background, see `process.create`.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `ExecOptions` for specific option keys and their values.
@param program The program to Execute as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary representing the result of the child process
]=]
function process.exec(program: string, params: { string }?, options: ExecOptions?): ExecResult
return nil :: any
end
return process

View file

@ -1,21 +0,0 @@
[package]
name = "lune-std-regex"
version = "0.2.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.10.3", features = ["luau"] }
regex = "1.10"
self_cell = "1.0"
lune-utils = { version = "0.2.2", 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<M: LuaUserDataMethods<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<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
}
}

View file

@ -1,38 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod captures;
mod matches;
mod regex;
use self::regex::LuaRegex;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `regex` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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<F: LuaUserDataFields<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<M: LuaUserDataMethods<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<M: LuaUserDataMethods<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<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "Regex");
}
}

View file

@ -1,233 +0,0 @@
--[=[
@class RegexMatch
A match from a regular expression.
Contains the following values:
- `start` -- The start index of the match in the original string.
- `finish` -- The end index of the match in the original string.
- `text` -- The text that was matched.
- `len` -- The length of the text that was matched.
]=]
local RegexMatch = {
start = 0,
finish = 0,
text = "",
len = 0,
}
type RegexMatch = typeof(RegexMatch)
local RegexCaptures = {}
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
return nil :: any
end
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
return nil :: any
end
function RegexCaptures.format(self: RegexCaptures, format: string): string
return nil :: any
end
--[=[
@class RegexCaptures
Captures from a regular expression.
]=]
export type RegexCaptures = typeof(setmetatable(
{} :: {
--[=[
@within RegexCaptures
@tag Method
@method get
Returns the match at the given index, if one exists.
@param index -- The index of the match to get
@return RegexMatch -- The match, if one exists
]=]
get: (self: RegexCaptures, index: number) -> RegexMatch?,
--[=[
@within RegexCaptures
@tag Method
@method group
Returns the match for the given named match group, if one exists.
@param group -- The name of the group to get
@return RegexMatch -- The match, if one exists
]=]
group: (self: RegexCaptures, group: string) -> RegexMatch?,
--[=[
@within RegexCaptures
@tag Method
@method format
Formats the captures using the given format string.
### Example usage
```lua
local regex = require("@lune/regex")
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
assert(caps ~= nil, "Example pattern should match example text")
local formatted = caps:format("year=$year, month=$month, day=$day")
print(formatted) -- "year=2010, month=03, day=14"
```
@param format -- The format string to use
@return string -- The formatted string
]=]
format: (self: RegexCaptures, format: string) -> string,
},
{} :: {
__len: (self: RegexCaptures) -> number,
}
))
local Regex = {}
--[=[
@within Regex
@tag Method
Check if the given text matches the regular expression.
This method may be slightly more efficient than calling `find`
if you only need to know if the text matches the pattern.
@param text -- The text to search
@return boolean -- Whether the text matches the pattern
]=]
function Regex.isMatch(self: Regex, text: string): boolean
return nil :: any
end
--[=[
@within Regex
@tag Method
Finds the first match in the given text.
Returns `nil` if no match was found.
@param text -- The text to search
@return RegexMatch? -- The match object
]=]
function Regex.find(self: Regex, text: string): RegexMatch?
return nil :: any
end
--[=[
@within Regex
@tag Method
Finds all matches in the given text as a `RegexCaptures` object.
Returns `nil` if no matches are found.
@param text -- The text to search
@return RegexCaptures? -- The captures object
]=]
function Regex.captures(self: Regex, text: string): RegexCaptures?
return nil :: any
end
--[=[
@within Regex
@tag Method
Splits the given text using the regular expression.
@param text -- The text to split
@return { string } -- The split text
]=]
function Regex.split(self: Regex, text: string): { string }
return nil :: any
end
--[=[
@within Regex
@tag Method
Replaces the first match in the given text with the given replacer string.
@param haystack -- The text to search
@param replacer -- The string to replace matches with
@return string -- The text with the first match replaced
]=]
function Regex.replace(self: Regex, haystack: string, replacer: string): string
return nil :: any
end
--[=[
@within Regex
@tag Method
Replaces all matches in the given text with the given replacer string.
@param haystack -- The text to search
@param replacer -- The string to replace matches with
@return string -- The text with all matches replaced
]=]
function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string
return nil :: any
end
export type Regex = typeof(Regex)
--[=[
@class Regex
Built-in library for regular expressions
### Example 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
```
]=]
local regex = {}
--[=[
@within Regex
@tag Constructor
Creates a new `Regex` from a given string pattern.
### Errors
This constructor throws an error if the given pattern is invalid.
@param pattern -- The string pattern to use
@return Regex -- The new Regex object
]=]
function regex.new(pattern: string): Regex
return nil :: any
end
return regex

View file

@ -1,23 +0,0 @@
[package]
name = "lune-std-roblox"
version = "0.2.2"
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.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
rbx_cookie = { version = "0.1.4", default-features = false }
roblox_install = "1.0"
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-roblox = { version = "0.2.2", path = "../lune-roblox" }

View file

@ -1,183 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::sync::OnceLock;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use lune_roblox::{
document::{Document, DocumentError, DocumentFormat, DocumentKind},
instance::{registry::InstanceRegistry, Instance},
reflection::Database as ReflectionDatabase,
};
static REFLECTION_DATABASE: OnceLock<ReflectionDatabase> = OnceLock::new();
use lune_utils::TableBuilder;
use roblox_install::RobloxStudio;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `roblox` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
Creates the `roblox` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
let mut roblox_constants = Vec::new();
let roblox_module = lune_roblox::module(lua.clone())?;
for pair in roblox_module.pairs::<LuaValue, LuaValue>() {
roblox_constants.push(pair?);
}
TableBuilder::new(lua)?
.with_values(roblox_constants)?
.with_async_function("deserializePlace", deserialize_place)?
.with_async_function("deserializeModel", deserialize_model)?
.with_async_function("serializePlace", serialize_place)?
.with_async_function("serializeModel", serialize_model)?
.with_function("getAuthCookie", get_auth_cookie)?
.with_function("getReflectionDatabase", get_reflection_database)?
.with_function("implementProperty", implement_property)?
.with_function("implementMethod", implement_method)?
.with_function("studioApplicationPath", studio_application_path)?
.with_function("studioContentPath", studio_content_path)?
.with_function("studioPluginPath", studio_plugin_path)?
.with_function("studioBuiltinPluginPath", studio_builtin_plugin_path)?
.build_readonly()
}
async fn deserialize_place(lua: Lua, contents: LuaString) -> LuaResult<LuaValue> {
let bytes = contents.as_bytes().to_vec();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
let data_model = doc.into_data_model_instance()?;
Ok::<_, DocumentError>(data_model)
});
fut.await.into_lua_err()?.into_lua(&lua)
}
async fn deserialize_model(lua: Lua, contents: LuaString) -> LuaResult<LuaValue> {
let bytes = contents.as_bytes().to_vec();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
let instance_array = doc.into_instance_array()?;
Ok::<_, DocumentError>(instance_array)
});
fut.await.into_lua_err()?.into_lua(&lua)
}
async fn serialize_place(
lua: Lua,
(data_model, as_xml): (LuaUserDataRef<Instance>, Option<bool>),
) -> LuaResult<LuaString> {
let data_model = *data_model;
let fut = lua.spawn_blocking(move || {
let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes)
});
let bytes = fut.await.into_lua_err()?;
lua.create_string(bytes)
}
async fn serialize_model(
lua: Lua,
(instances, as_xml): (Vec<LuaUserDataRef<Instance>>, Option<bool>),
) -> LuaResult<LuaString> {
let instances = instances.iter().map(|i| **i).collect();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes)
});
let bytes = fut.await.into_lua_err()?;
lua.create_string(bytes)
}
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
if matches!(raw, Some(true)) {
Ok(rbx_cookie::get_value())
} else {
Ok(rbx_cookie::get())
}
}
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
}
fn implement_property(
lua: &Lua,
(class_name, property_name, property_getter, property_setter): (
String,
String,
LuaFunction,
Option<LuaFunction>,
),
) -> LuaResult<()> {
let property_setter = if let Some(setter) = property_setter {
setter
} else {
let property_name = property_name.clone();
lua.create_function(move |_, _: LuaMultiValue| {
Err::<(), _>(LuaError::runtime(format!(
"Property '{property_name}' is read-only"
)))
})?
};
InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter)
.into_lua_err()?;
InstanceRegistry::insert_property_setter(lua, &class_name, &property_name, property_setter)
.into_lua_err()?;
Ok(())
}
fn implement_method(
lua: &Lua,
(class_name, method_name, method): (String, String, LuaFunction),
) -> LuaResult<()> {
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
Ok(())
}
fn studio_application_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.application_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_content_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.content_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.plugins_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_builtin_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.built_in_plugins_path().display().to_string())
.map_err(LuaError::external)
}

View file

@ -1,45 +0,0 @@
[package]
name = "lune-std-serde"
version = "0.2.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.10.3", features = ["luau", "serialize", "error-send"] }
async-compression = { version = "0.4", features = [
"futures-io",
"brotli",
"deflate",
"gzip",
"zlib",
] }
blocking = "1.6"
bstr = "1.9"
futures-lite = "2.6"
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"] }
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,154 +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 FromLua for EncodeDecodeFormat {
fn from_lua(value: LuaValue, _: &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".to_string(),
message: Some(format!(
"Invalid format '{kind}', valid formats are: json, yaml, toml"
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EncodeDecodeFormat".to_string(),
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(value: LuaValue, lua: &Lua, config: EncodeDecodeConfig) -> LuaResult<LuaString> {
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".to_string(),
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 FromLua for HashAlgorithm {
fn from_lua(value: LuaValue, _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".to_string(),
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".to_string(),
message: None,
})
}
}
}
impl FromLuaMulti for HashOptions {
fn from_lua_multi(mut values: LuaMultiValue, 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: "HashOptions".to_string(),
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".to_string(),
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,79 +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;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `serde` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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,
(format, value, pretty): (EncodeDecodeFormat, LuaValue, Option<bool>),
) -> LuaResult<LuaString> {
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,196 +0,0 @@
--[=[
@within Serde
@interface EncodeDecodeFormat
A serialization/deserialization format supported by the Serde library.
Currently supported formats:
| Name | Learn More |
|:-------|:---------------------|
| `json` | https://www.json.org |
| `yaml` | https://yaml.org |
| `toml` | https://toml.io |
]=]
export type EncodeDecodeFormat = "json" | "yaml" | "toml"
--[=[
@within Serde
@interface CompressDecompressFormat
A compression/decompression format supported by the Serde library.
Currently supported formats:
| Name | Learn More |
|:---------|:----------------------------------|
| `brotli` | https://github.com/google/brotli |
| `gzip` | https://www.gnu.org/software/gzip |
| `lz4` | https://github.com/lz4/lz4 |
| `zlib` | https://www.zlib.net |
]=]
export type CompressDecompressFormat = "brotli" | "gzip" | "lz4" | "zlib"
--[=[
@within Serde
@interface HashAlgorithm
A hash algorithm supported by the Serde library.
Currently supported algorithms:
| Name | Learn More |
|:-----------|:-------------------------------------|
| `md5` | https://en.wikipedia.org/wiki/MD5 |
| `sha1` | https://en.wikipedia.org/wiki/SHA-1 |
| `sha224` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha256` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha384` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha512` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha3-224` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-256` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-384` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-512` | https://en.wikipedia.org/wiki/SHA-3 |
| `blake3` | https://en.wikipedia.org/wiki/BLAKE3 |
]=]
export type HashAlgorithm =
"md5"
| "sha1"
| "sha224"
| "sha256"
| "sha384"
| "sha512"
| "sha3-224"
| "sha3-256"
| "sha3-384"
| "sha3-512"
| "blake3"
--[=[
@class Serde
Built-in library for:
- serialization & deserialization
- encoding & decoding
- compression
### Example usage
```lua
local fs = require("@lune/fs")
local serde = require("@lune/serde")
-- Parse different file formats into lua tables
local someJson = serde.decode("json", fs.readFile("myFile.json"))
local someToml = serde.decode("toml", fs.readFile("myFile.toml"))
local someYaml = serde.decode("yaml", fs.readFile("myFile.yaml"))
-- Write lua tables to files in different formats
fs.writeFile("myFile.json", serde.encode("json", someJson))
fs.writeFile("myFile.toml", serde.encode("toml", someToml))
fs.writeFile("myFile.yaml", serde.encode("yaml", someYaml))
```
]=]
local serde = {}
--[=[
@within Serde
@tag must_use
Encodes the given value using the given format.
See [`EncodeDecodeFormat`] for a list of supported formats.
@param format The format to use
@param value The value to encode
@param pretty If the encoded string should be human-readable, including things such as newlines and spaces. Only supported for json and toml formats, and defaults to false
@return The encoded string
]=]
function serde.encode(format: EncodeDecodeFormat, value: any, pretty: boolean?): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Decodes the given string using the given format into a lua value.
See [`EncodeDecodeFormat`] for a list of supported formats.
@param format The format to use
@param encoded The string to decode
@return The decoded lua value
]=]
function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any
return nil :: any
end
--[=[
@within Serde
@tag must_use
Compresses the given string using the given format.
See [`CompressDecompressFormat`] for a list of supported formats.
@param format The format to use
@param s The string to compress
@param level The compression level to use, clamped to the format's limits. The best compression level is used by default
@return The compressed string
]=]
function serde.compress(format: CompressDecompressFormat, s: buffer | string, level: number?): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Decompresses the given string using the given format.
See [`CompressDecompressFormat`] for a list of supported formats.
@param format The format to use
@param s The string to decompress
@return The decompressed string
]=]
function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Hashes the given message using the given algorithm
and returns the hash as a hex string.
See [`HashAlgorithm`] for a list of supported algorithms.
@param algorithm The algorithm to use
@param message The message to hash
@return The hash as a hex string
]=]
function serde.hash(algorithm: HashAlgorithm, message: string | buffer): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Hashes the given message using HMAC with the given secret
and algorithm, returning the hash as a base64 string.
See [`HashAlgorithm`] for a list of supported algorithms.
@param algorithm The algorithm to use
@param message The message to hash
@return The hash as a base64 string
]=]
function serde.hmac(algorithm: HashAlgorithm, message: string | buffer, secret: string | buffer): string
return nil :: any
end
return serde

View file

@ -1,25 +0,0 @@
[package]
name = "lune-std-stdio"
version = "0.2.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]
mlua = { version = "0.10.3", features = ["luau", "error-send"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
async-io = "2.4"
async-lock = "3.4"
blocking = "1.6"
dialoguer = "0.11"
futures-lite = "2.6"
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,110 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
io::{stderr, stdin, stdout, Stdin},
sync::{Arc, LazyLock},
};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use async_lock::Mutex as AsyncMutex;
use blocking::Unblock;
use futures_lite::{io::BufReader, prelude::*};
use lune_utils::{
fmt::{pretty_format_multi_value, ValueFormatConfig},
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);
static STDIN: LazyLock<Arc<AsyncMutex<BufReader<Unblock<Stdin>>>>> = LazyLock::new(|| {
let stdin = Unblock::new(stdin());
let reader = BufReader::new(stdin);
Arc::new(AsyncMutex::new(reader))
});
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `stdio` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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("readLine", stdio_read_line)?
.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 = Unblock::new(stdout());
stdout.write_all(&s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
}
async fn stdio_ewrite(_: Lua, s: LuaString) -> LuaResult<()> {
let mut stderr = Unblock::new(stderr());
stderr.write_all(&s.as_bytes()).await?;
stderr.flush().await?;
Ok(())
}
async fn stdio_read_line(lua: Lua, (): ()) -> LuaResult<LuaString> {
let mut string = String::new();
let mut handle = STDIN.lock_arc().await;
handle.read_line(&mut string).await?;
lua.create_string(&string)
}
async fn stdio_read_to_end(lua: Lua, (): ()) -> LuaResult<LuaString> {
let mut buffer = Vec::new();
let mut handle = STDIN.lock_arc().await;
handle.read_to_end(&mut buffer).await?;
lua.create_string(&buffer)
}
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".to_string(),
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".to_string(),
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".to_string(),
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".to_string(),
message: None,
})
}
}
}

View file

@ -1,22 +0,0 @@
[package]
name = "lune-std-task"
version = "0.2.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.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
async-io = "2.4"
futures-lite = "2.6"
lune-utils = { version = "0.2.2", path = "../lune-utils" }

View file

@ -1,84 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::time::{Duration, Instant};
use async_io::Timer;
use futures_lite::future::yield_now;
use mlua::prelude::*;
use mlua_luau_scheduler::Functions;
use lune_utils::TableBuilder;
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
/**
Returns a string containing type definitions for the `task` standard library.
*/
#[must_use]
pub fn typedefs() -> String {
TYPEDEFS.to_string()
}
/**
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.clone())?;
// Create wait & delay functions
let task_wait = lua.create_async_function(wait)?;
let task_delay_env = TableBuilder::new(lua.clone())?
.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: Lua, secs: Option<f64>) -> LuaResult<f64> {
// NOTE: We must guarantee that the task.wait API always yields
// from a lua perspective, even if sleep/timer completes instantly
yield_now().await;
wait_inner(lua, secs).await
}
async fn wait_inner(_: Lua, secs: Option<f64>) -> LuaResult<f64> {
// One millisecond is a reasonable minimum sleep duration,
// anything lower than this runs the risk of completing the
// the below timer instantly, without giving control to the OS ...
let duration = Duration::from_secs_f64(secs.unwrap_or_default());
let duration = duration.max(Duration::from_millis(1));
// ... however, we should still _guarantee_ that whatever
// coroutine that calls this sleep function always yields,
// even if the timer is able to complete without doing so
yield_now().await;
// We may then sleep as normal
let before = Instant::now();
let after = Timer::after(duration).await;
Ok((after - before).as_secs_f64())
}

View file

@ -1,62 +0,0 @@
[package]
name = "lune-std"
version = "0.2.2"
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.10.3", features = ["luau"] }
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
async-channel = "2.3"
async-fs = "2.1"
async-lock = "3.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
lune-utils = { version = "0.2.2", path = "../lune-utils" }
lune-std-datetime = { optional = true, version = "0.2.2", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.2.2", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.2.2", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.2.2", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.2.2", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.2.2", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.2.2", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.2.2", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.2.2", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.2.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(&self, lua: Lua) -> LuaResult<LuaValue> {
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(|_: &Lua, 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,7 +0,0 @@
use mlua::prelude::*;
use super::context::*;
pub(super) fn require(lua: Lua, ctx: &RequireContext, name: &str) -> LuaResult<LuaMultiValue> {
ctx.load_library(lua, name)
}

View file

@ -1,121 +0,0 @@
use std::path::{Path, PathBuf};
use mlua::prelude::*;
use mlua::Error::ExternalError;
use super::context::*;
pub(super) async fn require(
lua: Lua,
ctx: &RequireContext,
source: &str,
path: &str,
resolve_as_self: bool,
) -> LuaResult<LuaMultiValue> {
let (abs_path, rel_path) = RequireContext::resolve_paths(source, path, resolve_as_self)?;
require_abs_rel(lua, ctx, abs_path, rel_path).await
}
pub(super) async fn require_abs_rel(
lua: Lua,
ctx: &RequireContext,
abs_path: PathBuf, // Absolute to filesystem
rel_path: PathBuf, // Relative to CWD (for displaying)
) -> LuaResult<LuaMultiValue> {
// 1. Try to require the exact path
match require_inner(lua.clone(), ctx, &abs_path, &rel_path).await {
Ok(res) => return Ok(res),
Err(err) => {
if !is_file_not_found_error(&err) {
return Err(err);
}
}
}
// 2. Try to require the path with an added "luau" extension
// 3. Try to require the path with an added "lua" extension
for extension in ["luau", "lua"] {
match require_inner(
lua.clone(),
ctx,
&append_extension(&abs_path, extension),
&append_extension(&rel_path, extension),
)
.await
{
Ok(res) => return Ok(res),
Err(err) => {
if !is_file_not_found_error(&err) {
return Err(err);
}
}
}
}
// We didn't find any direct file paths, look
// for directories with "init" files in them...
let abs_init = abs_path.join("init");
let rel_init = rel_path.join("init");
// 4. Try to require the init path with an added "luau" extension
// 5. Try to require the init path with an added "lua" extension
for extension in ["luau", "lua"] {
match require_inner(
lua.clone(),
ctx,
&append_extension(&abs_init, extension),
&append_extension(&rel_init, extension),
)
.await
{
Ok(res) => return Ok(res),
Err(err) => {
if !is_file_not_found_error(&err) {
return Err(err);
}
}
}
}
// Nothing left to try, throw an error
Err(LuaError::runtime(format!(
"No file exists at the path '{}'",
rel_path.display()
)))
}
async fn require_inner(
lua: Lua,
ctx: &RequireContext,
abs_path: impl AsRef<Path>,
rel_path: impl AsRef<Path>,
) -> LuaResult<LuaMultiValue> {
let abs_path = abs_path.as_ref();
let rel_path = rel_path.as_ref();
if ctx.is_cached(abs_path)? {
ctx.get_from_cache(lua, abs_path)
} else if ctx.is_pending(abs_path)? {
ctx.wait_for_cache(lua, &abs_path).await
} else {
ctx.load_with_caching(lua, &abs_path, &rel_path).await
}
}
fn append_extension(path: impl Into<PathBuf>, ext: &'static str) -> PathBuf {
let mut new = path.into();
match new.extension() {
// FUTURE: There's probably a better way to do this than converting to a lossy string
Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())),
None => new.set_extension(ext),
};
new
}
fn is_file_not_found_error(err: &LuaError) -> bool {
if let ExternalError(err) = err {
err.as_ref().downcast_ref::<std::io::Error>().is_some()
} else {
false
}
}

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(|_: &Lua, 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,30 +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.clone())?)?;
}
Ok(())
}

View file

@ -1,151 +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"),
}
}
/**
Returns type definitions for the library.
*/
#[must_use]
#[rustfmt::skip]
#[allow(unreachable_patterns)]
pub fn typedefs(&self) -> String {
match self {
#[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::typedefs(),
#[cfg(feature = "fs")] Self::Fs => lune_std_fs::typedefs(),
#[cfg(feature = "luau")] Self::Luau => lune_std_luau::typedefs(),
#[cfg(feature = "net")] Self::Net => lune_std_net::typedefs(),
#[cfg(feature = "task")] Self::Task => lune_std_task::typedefs(),
#[cfg(feature = "process")] Self::Process => lune_std_process::typedefs(),
#[cfg(feature = "regex")] Self::Regex => lune_std_regex::typedefs(),
#[cfg(feature = "serde")] Self::Serde => lune_std_serde::typedefs(),
#[cfg(feature = "stdio")] Self::Stdio => lune_std_stdio::typedefs(),
#[cfg(feature = "roblox")] Self::Roblox => lune_std_roblox::typedefs(),
_ => 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(&self, lua: Lua) -> LuaResult<LuaMultiValue> {
let mod_lua = lua.clone();
let res: LuaResult<LuaTable> = match self {
#[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::module(mod_lua),
#[cfg(feature = "fs")] Self::Fs => lune_std_fs::module(mod_lua),
#[cfg(feature = "luau")] Self::Luau => lune_std_luau::module(mod_lua),
#[cfg(feature = "net")] Self::Net => lune_std_net::module(mod_lua),
#[cfg(feature = "task")] Self::Task => lune_std_task::module(mod_lua),
#[cfg(feature = "process")] Self::Process => lune_std_process::module(mod_lua),
#[cfg(feature = "regex")] Self::Regex => lune_std_regex::module(mod_lua),
#[cfg(feature = "serde")] Self::Serde => lune_std_serde::module(mod_lua),
#[cfg(feature = "stdio")] Self::Stdio => lune_std_stdio::module(mod_lua),
#[cfg(feature = "roblox")] Self::Roblox => lune_std_roblox::module(mod_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,24 +0,0 @@
[package]
name = "lune-utils"
version = "0.2.2"
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.10.3", features = ["luau", "async"] }
console = "0.15"
dunce = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] }
path-clean = "1.0"
pathdiff = "0.2"
parking_lot = "0.12.3"
semver = "1.0"

View file

@ -1,204 +0,0 @@
use std::{
fmt,
str::FromStr,
sync::{Arc, LazyLock},
};
use console::style;
use mlua::prelude::*;
use super::StackTrace;
static STYLED_STACK_BEGIN: LazyLock<String> = LazyLock::new(|| {
format!(
"{}{}{}",
style("[").dim(),
style("Stack Begin").blue(),
style("]").dim()
)
});
static STYLED_STACK_END: LazyLock<String> = LazyLock::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
#[allow(clippy::arc_with_non_send_sync)]
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 }
}
}
impl From<Box<LuaError>> for ErrorComponents {
fn from(value: Box<LuaError>) -> Self {
Self::from(*value)
}
}

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,148 +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,101 +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::Other(_) => COLOR_MAGENTA.apply_to("<unknown>").to_string(),
LuaValue::Buffer(_) => COLOR_MAGENTA.apply_to("<buffer>").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,35 +0,0 @@
use mlua::prelude::*;
pub fn get_table_type_metavalue(tab: &LuaTable) -> Option<String> {
let meta = tab.metatable()?;
let s = meta.get::<LuaString>(LuaMetaMethod::Type.name()).ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn get_userdata_type_metavalue(usr: &LuaAnyUserData) -> Option<String> {
let meta = usr.metatable().ok()?;
let s = meta.get::<LuaString>(LuaMetaMethod::Type.name()).ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn call_table_tostring_metamethod(tab: &LuaTable) -> Option<String> {
let meta = tab.metatable()?;
let value = meta.get(LuaMetaMethod::ToString.name()).ok()?;
match value {
LuaValue::String(s) => Some(s.to_string_lossy().to_string()),
LuaValue::Function(f) => f.call(tab).ok(),
_ => None,
}
}
pub fn call_userdata_tostring_metamethod(usr: &LuaAnyUserData) -> Option<String> {
let meta = usr.metatable().ok()?;
let value = meta.get(LuaMetaMethod::ToString.name()).ok()?;
match value {
LuaValue::String(s) => Some(s.to_string_lossy().to_string()),
LuaValue::Function(f) => f.call(usr).ok(),
_ => None,
}
}

View file

@ -1,66 +0,0 @@
use std::{
collections::HashSet,
sync::{Arc, LazyLock},
};
use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
use mlua::prelude::*;
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: LazyLock<Arc<ReentrantMutex<()>>> =
LazyLock::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,10 +0,0 @@
use std::sync::LazyLock;
use console::Style;
pub static COLOR_GREEN: LazyLock<Style> = LazyLock::new(|| Style::new().green());
pub static COLOR_YELLOW: LazyLock<Style> = LazyLock::new(|| Style::new().yellow());
pub static COLOR_MAGENTA: LazyLock<Style> = LazyLock::new(|| Style::new().magenta());
pub static COLOR_CYAN: LazyLock<Style> = LazyLock::new(|| Style::new().cyan());
pub static STYLE_DIM: LazyLock<Style> = LazyLock::new(|| Style::new().dim());

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