mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Compare commits
No commits in common. "main" and "v0.8.0" have entirely different histories.
371 changed files with 7554 additions and 17461 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -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
|
||||
|
|
33
.github/workflows/ci.yaml
vendored
33
.github/workflows/ci.yaml
vendored
|
@ -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
24
.github/workflows/publish.yaml
vendored
Normal 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
|
58
.github/workflows/release.yaml
vendored
58
.github/workflows/release.yaml
vendored
|
@ -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
5
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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! 🌙")
|
||||
|
|
|
@ -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.")
|
||||
|
|
355
CHANGELOG.md
355
CHANGELOG.md
|
@ -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
3130
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
161
Cargo.toml
161
Cargo.toml
|
@ -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" }
|
||||
|
|
|
@ -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
4
aftman.toml
Normal 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"
|
|
@ -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" }
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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" }
|
|
@ -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()
|
||||
}
|
|
@ -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" }
|
|
@ -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" }
|
|
@ -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)
|
||||
}
|
|
@ -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" }
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
});
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod decode;
|
||||
mod encode;
|
||||
|
||||
pub use self::decode::decode;
|
||||
pub use self::encode::encode;
|
|
@ -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" }
|
|
@ -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;
|
||||
}
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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" }
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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" }
|
|
@ -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)
|
||||
}
|
|
@ -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" }
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()?)
|
||||
}
|
|
@ -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
|
|
@ -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" }
|
|
@ -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()
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" }
|
|
@ -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())
|
||||
}
|
|
@ -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" }
|
|
@ -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(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
pub fn create(lua: Lua) -> LuaResult<LuaValue> {
|
||||
lua.create_table()?.into_lua(&lua)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod g_table;
|
||||
pub mod print;
|
||||
pub mod require;
|
||||
pub mod version;
|
||||
pub mod warn;
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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};
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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};
|
|
@ -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}>"),
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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(" ")
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Add table
Reference in a new issue