diff --git a/CHANGELOG.md b/CHANGELOG.md index 091b0c9..0b1d435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,64 @@ 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 + +### Changed + +- 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 the `User-Agent` header in `net.request` to be more descriptive ([#186]) +- Updated to Luau version `0.622`. + +### Fixed + +- 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 + ## `0.8.3` - April 15th, 2024 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 8e17080..469abac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -110,6 +121,15 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -193,7 +213,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -330,6 +350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", + "regex-automata 0.4.6", "serde", ] @@ -358,10 +379,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] -name = "cc" -version = "1.0.94" +name = "bzip2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cfg-if" @@ -404,6 +451,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.4" @@ -435,7 +492,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -446,13 +503,22 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -540,6 +606,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.0" @@ -571,6 +652,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "deranged" version = "0.3.11" @@ -580,6 +667,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -614,6 +712,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -807,6 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", + "libz-ng-sys", "miniz_oxide", ] @@ -867,7 +967,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1003,6 +1103,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -1112,9 +1221,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f24ce812868d86d19daa79bf3bf9175bc44ea323391147a5e3abde2a283871b" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -1140,7 +1249,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls 0.21.10", + "rustls 0.21.11", "tokio", "tokio-rustls 0.24.1", ] @@ -1152,7 +1261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.3.0", + "hyper 1.3.1", "hyper-util", "pin-project-lite", "tokio", @@ -1171,7 +1280,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.0", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -1243,6 +1352,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1264,6 +1382,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1305,6 +1432,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-ng-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "line-wrap" version = "0.2.0" @@ -1335,9 +1472,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "luau0-src" -version = "0.8.5+luau617" +version = "0.8.6+luau622" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652e36b8c35d807ec76a4931fe7c62883c62cc93311fb49bf5b76084647078ea" +checksum = "07758c1f5908f7f9dd9109efaf8c66907cc38acf312db03287e7ad2a64b5de1c" dependencies = [ "cc", ] @@ -1350,6 +1487,7 @@ dependencies = [ "async-compression", "async-trait", "blocking", + "bstr", "chrono", "chrono_lc", "clap", @@ -1362,7 +1500,7 @@ dependencies = [ "glam", "http 1.1.0", "http-body-util", - "hyper 1.3.0", + "hyper 1.3.1", "hyper-tungstenite", "hyper-util", "include_dir", @@ -1385,6 +1523,7 @@ dependencies = [ "regex", "reqwest", "rustyline", + "self_cell", "serde", "serde_json", "serde_yaml", @@ -1395,6 +1534,7 @@ dependencies = [ "tracing", "tracing-subscriber", "urlencoding", + "zip_next", ] [[package]] @@ -1426,6 +1566,16 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder 1.5.0", + "crc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1670,6 +1820,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1693,7 +1853,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1759,9 +1919,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1782,7 +1942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2039,7 +2199,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.11", "rustls-pemfile", "serde", "serde_json", @@ -2074,9 +2234,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder 1.5.0", "num-traits", @@ -2085,9 +2245,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188" dependencies = [ "byteorder 1.5.0", "rmp", @@ -2151,9 +2311,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" dependencies = [ "log", "ring", @@ -2163,9 +2323,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -2264,6 +2424,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "self_cell" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" + [[package]] name = "semver" version = "0.9.0" @@ -2287,9 +2453,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -2306,20 +2472,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap", "itoa", @@ -2411,6 +2577,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -2531,9 +2703,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -2581,22 +2753,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2721,7 +2893,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2730,7 +2902,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.11", "tokio", ] @@ -2740,7 +2912,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -2753,7 +2925,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -2799,9 +2971,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", @@ -2858,7 +3030,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2919,7 +3091,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "sha1 0.10.6", "thiserror", @@ -2937,6 +3109,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.17.0" @@ -3081,7 +3259,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3115,7 +3293,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3369,3 +3547,76 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21968e6da56f847a155a89581ba846507afa14854e041f3053edb6ddd19f807" +dependencies = [ + "aes", + "arbitrary", + "byteorder 1.5.0", + "bzip2", + "constant_time_eq 0.3.0", + "crc32fast", + "crossbeam-utils", + "deflate64", + "flate2", + "hmac", + "lzma-rs", + "pbkdf2", + "sha1 0.10.6", + "time 0.3.36", + "zopfli", + "zstd", +] + +[[package]] +name = "zip_next" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8b818e7a619a85006cbdf263036c50cddd97d907887173b7ef743b4fb5b5e" +dependencies = [ + "zip", +] + +[[package]] +name = "zopfli" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" +dependencies = [ + "crc32fast", + "log", + "simd-adler32", + "typed-arena", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 11c2ee9..437731c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ cli = [ "dep:env_logger", "dep:clap", "dep:include_dir", - "dep:regex", "dep:rustyline", + "dep:zip_next", ] roblox = [ "dep:glam", @@ -75,6 +75,9 @@ path-clean = "1.0" pathdiff = "0.2" pin-project = "1.0" urlencoding = "2.1" +bstr = "1.9" +regex = "1.10" +self_cell = "1.0" ### RUNTIME @@ -83,9 +86,8 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio = { version = "1.24", features = ["full", "tracing"] } os_str_bytes = { version = "7.0", features = ["conversions"] } - mlua-luau-scheduler = { git = "https://github.com/0x5eal/mlua-luau-scheduler-exitstatus.git" } -mlua = { version = "0.9.6", features = [ +mlua = { version = "0.9.7", features = [ "luau", "luau-jit", "async", @@ -131,11 +133,8 @@ env_logger = { optional = true, version = "0.11" } itertools = "0.12" clap = { optional = true, version = "4.1", features = ["derive"] } include_dir = { optional = true, version = "0.7", features = ["glob"] } -regex = { optional = true, version = "1.7", default-features = false, features = [ - "std", - "unicode-perl", -] } rustyline = { optional = true, version = "14.0" } +zip_next = { optional = true, version = "1.1" } ### ROBLOX diff --git a/src/cli/build.rs b/src/cli/build.rs deleted file mode 100644 index efe1738..0000000 --- a/src/cli/build.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::{ - env::consts::EXE_EXTENSION, - path::{Path, PathBuf}, - process::ExitCode, -}; - -use anyhow::{Context, Result}; -use clap::Parser; -use console::style; -use tokio::{fs, io::AsyncWriteExt as _}; - -use crate::standalone::metadata::Metadata; - -/// Build a standalone executable -#[derive(Debug, Clone, Parser)] -pub struct BuildCommand { - /// The path to the input file - pub input: PathBuf, - - /// The path to the output file - defaults to the - /// input file path with an executable extension - #[clap(short, long)] - pub output: Option, -} - -impl BuildCommand { - pub async fn run(self) -> Result { - let output_path = self - .output - .unwrap_or_else(|| self.input.with_extension(EXE_EXTENSION)); - - let input_path_displayed = self.input.display(); - let output_path_displayed = output_path.display(); - - // Try to read the input file - let source_code = fs::read(&self.input) - .await - .context("failed to read input file")?; - - // Read the contents of the lune interpreter as our starting point - println!( - "Creating standalone binary using {}", - style(input_path_displayed).green() - ); - let patched_bin = Metadata::create_env_patched_bin(source_code.clone()) - .await - .context("failed to create patched binary")?; - - // And finally write the patched binary to the output file - println!( - "Writing standalone binary to {}", - style(output_path_displayed).blue() - ); - write_executable_file_to(output_path, patched_bin).await?; - - Ok(ExitCode::SUCCESS) - } -} - -async fn write_executable_file_to(path: impl AsRef, bytes: impl AsRef<[u8]>) -> Result<()> { - let mut options = fs::OpenOptions::new(); - options.write(true).create(true).truncate(true); - - #[cfg(unix)] - { - options.mode(0o755); // Read & execute for all, write for owner - } - - let mut file = options.open(path).await?; - file.write_all(bytes.as_ref()).await?; - - Ok(()) -} diff --git a/src/cli/build/base_exe.rs b/src/cli/build/base_exe.rs new file mode 100644 index 0000000..d8507cc --- /dev/null +++ b/src/cli/build/base_exe.rs @@ -0,0 +1,86 @@ +use std::{ + io::{Cursor, Read}, + path::PathBuf, +}; + +use tokio::{fs, task}; + +use crate::standalone::metadata::CURRENT_EXE; + +use super::{ + files::write_executable_file_to, + result::{BuildError, BuildResult}, + target::{BuildTarget, CACHE_DIR}, +}; + +/** + Discovers the path to the base executable to use for cross-compilation. + + If the target is the same as the current system, the current executable is used. + + If no binary exists at the target path, it will attempt to download it from the internet. +*/ +pub async fn get_or_download_base_executable(target: BuildTarget) -> BuildResult { + if target.is_current_system() { + return Ok(CURRENT_EXE.to_path_buf()); + } + if target.cache_path().exists() { + return Ok(target.cache_path()); + } + + // The target is not cached, we must download it + println!("Requested target '{target}' does not exist in cache"); + let version = env!("CARGO_PKG_VERSION"); + let target_triple = format!("lune-{version}-{target}"); + + let release_url = format!( + "{base_url}/v{version}/{target_triple}.zip", + base_url = "https://github.com/lune-org/lune/releases/download", + ); + + // NOTE: This is not entirely accurate, but it is clearer for a user + println!("Downloading {target_triple}{}...", target.exe_suffix()); + + // Try to request to download the zip file from the target url, + // making sure transient errors are handled gracefully and + // with a different error message than "not found" + let response = reqwest::get(release_url).await?; + if !response.status().is_success() { + if response.status().as_u16() == 404 { + return Err(BuildError::ReleaseTargetNotFound(target)); + } + return Err(BuildError::Download( + response.error_for_status().unwrap_err(), + )); + } + + // Receive the full zip file + let zip_bytes = response.bytes().await?.to_vec(); + let zip_file = Cursor::new(zip_bytes); + + // Look for and extract the binary file from the zip file + // NOTE: We use spawn_blocking here since reading a zip + // archive is a somewhat slow / blocking operation + let binary_file_name = format!("lune{}", target.exe_suffix()); + let binary_file_handle = task::spawn_blocking(move || { + let mut archive = zip_next::ZipArchive::new(zip_file)?; + + let mut binary = Vec::new(); + archive + .by_name(&binary_file_name) + .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? + .read_to_end(&mut binary)?; + + Ok::<_, BuildError>(binary) + }); + let binary_file_contents = binary_file_handle.await??; + + // Finally, write the extracted binary to the cache + if !CACHE_DIR.exists() { + fs::create_dir_all(CACHE_DIR.as_path()).await?; + } + write_executable_file_to(target.cache_path(), binary_file_contents).await?; + println!("Downloaded successfully and added to cache"); + + Ok(target.cache_path()) +} diff --git a/src/cli/build/files.rs b/src/cli/build/files.rs new file mode 100644 index 0000000..cf1d003 --- /dev/null +++ b/src/cli/build/files.rs @@ -0,0 +1,42 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use tokio::{fs, io::AsyncWriteExt}; + +/** + Removes the source file extension from the given path, if it has one. + + A source file extension is an extension such as `.lua` or `.luau`. +*/ +pub fn remove_source_file_ext(path: &Path) -> PathBuf { + if path + .extension() + .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) + { + path.with_extension("") + } else { + path.to_path_buf() + } +} + +/** + Writes the given bytes to a file at the specified path, + and makes sure it has permissions to be executed. +*/ +pub async fn write_executable_file_to( + path: impl AsRef, + bytes: impl AsRef<[u8]>, +) -> Result<(), std::io::Error> { + let mut options = fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + + #[cfg(unix)] + { + options.mode(0o755); // Read & execute for all, write for owner + } + + let mut file = options.open(path).await?; + file.write_all(bytes.as_ref()).await?; + + Ok(()) +} diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs new file mode 100644 index 0000000..6546a16 --- /dev/null +++ b/src/cli/build/mod.rs @@ -0,0 +1,83 @@ +use std::{path::PathBuf, process::ExitCode}; + +use anyhow::{bail, Context, Result}; +use clap::Parser; +use console::style; +use tokio::fs; + +use crate::standalone::metadata::Metadata; + +mod base_exe; +mod files; +mod result; +mod target; + +use self::base_exe::get_or_download_base_executable; +use self::files::{remove_source_file_ext, write_executable_file_to}; +use self::target::BuildTarget; + +/// Build a standalone executable +#[derive(Debug, Clone, Parser)] +pub struct BuildCommand { + /// The path to the input file + pub input: PathBuf, + + /// The path to the output file - defaults to the + /// input file path with an executable extension + #[clap(short, long)] + pub output: Option, + + /// The target to compile for in the format `os-arch` - + /// defaults to the os and arch of the current system + #[clap(short, long)] + pub target: Option, +} + +impl BuildCommand { + pub async fn run(self) -> Result { + // Derive target spec to use, or default to the current host system + let target = self.target.unwrap_or_else(BuildTarget::current_system); + + // Derive paths to use, and make sure the output path is + // not the same as the input, so that we don't overwrite it + let output_path = self + .output + .clone() + .unwrap_or_else(|| remove_source_file_ext(&self.input)); + let output_path = output_path.with_extension(target.exe_extension()); + if output_path == self.input { + if self.output.is_some() { + bail!("output path cannot be the same as input path"); + } + bail!("output path cannot be the same as input path, please specify a different output path"); + } + + // Try to read the given input file + // FUTURE: We should try and resolve a full require file graph using the input + // path here instead, see the notes in the `standalone` module for more details + let source_code = fs::read(&self.input) + .await + .context("failed to read input file")?; + + // Derive the base executable path based on the arguments provided + let base_exe_path = get_or_download_base_executable(target).await?; + + // Read the contents of the lune interpreter as our starting point + println!( + "Compiling standalone binary from {}", + style(self.input.display()).green() + ); + let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code) + .await + .context("failed to create patched binary")?; + + // And finally write the patched binary to the output file + println!( + "Writing standalone binary to {}", + style(output_path.display()).blue() + ); + write_executable_file_to(output_path, patched_bin).await?; // Read & execute for all, write for owner + + Ok(ExitCode::SUCCESS) + } +} diff --git a/src/cli/build/result.rs b/src/cli/build/result.rs new file mode 100644 index 0000000..1131c34 --- /dev/null +++ b/src/cli/build/result.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +use super::target::BuildTarget; + +/** + Errors that may occur when building a standalone binary +*/ +#[derive(Debug, Error)] +pub enum BuildError { + #[error("failed to find lune target '{0}' in GitHub release")] + ReleaseTargetNotFound(BuildTarget), + #[error("failed to find lune binary '{0}' in downloaded zip file")] + ZippedBinaryNotFound(String), + #[error("failed to download lune binary: {0}")] + Download(#[from] reqwest::Error), + #[error("failed to unzip lune binary: {0}")] + Unzip(#[from] zip_next::result::ZipError), + #[error("panicked while unzipping lune binary: {0}")] + UnzipJoin(#[from] tokio::task::JoinError), + #[error("io error: {0}")] + IoError(#[from] std::io::Error), +} + +pub type BuildResult = std::result::Result; diff --git a/src/cli/build/target.rs b/src/cli/build/target.rs new file mode 100644 index 0000000..7ed9dd1 --- /dev/null +++ b/src/cli/build/target.rs @@ -0,0 +1,177 @@ +use std::{env::consts::ARCH, fmt, path::PathBuf, str::FromStr}; + +use directories::BaseDirs; +use once_cell::sync::Lazy; + +const HOME_DIR: Lazy = Lazy::new(|| { + BaseDirs::new() + .expect("could not find home directory") + .home_dir() + .to_path_buf() +}); + +pub const CACHE_DIR: Lazy = Lazy::new(|| HOME_DIR.join(".lune").join("target")); + +/** + A target operating system supported by Lune +*/ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuildTargetOS { + Windows, + Linux, + MacOS, +} + +impl BuildTargetOS { + fn current_system() -> Self { + match std::env::consts::OS { + "windows" => Self::Windows, + "linux" => Self::Linux, + "macos" => Self::MacOS, + _ => panic!("unsupported target OS"), + } + } + + fn exe_extension(self) -> &'static str { + // NOTE: We can't use the constants from std since + // they are only accessible for the current target + match self { + Self::Windows => "exe", + _ => "", + } + } + + fn exe_suffix(self) -> &'static str { + match self { + Self::Windows => ".exe", + _ => "", + } + } +} + +impl fmt::Display for BuildTargetOS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Windows => write!(f, "windows"), + Self::Linux => write!(f, "linux"), + Self::MacOS => write!(f, "macos"), + } + } +} + +impl FromStr for BuildTargetOS { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "win" | "windows" => Ok(Self::Windows), + "linux" => Ok(Self::Linux), + "mac" | "macos" | "darwin" => Ok(Self::MacOS), + _ => Err("invalid target OS"), + } + } +} + +/** + A target architecture supported by Lune +*/ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuildTargetArch { + X86_64, + Aarch64, +} + +impl BuildTargetArch { + fn current_system() -> Self { + match ARCH { + "x86_64" => Self::X86_64, + "aarch64" => Self::Aarch64, + _ => panic!("unsupported target architecture"), + } + } +} + +impl fmt::Display for BuildTargetArch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::X86_64 => write!(f, "x86_64"), + Self::Aarch64 => write!(f, "aarch64"), + } + } +} + +impl FromStr for BuildTargetArch { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "x86_64" | "x64" => Ok(Self::X86_64), + "aarch64" | "arm64" => Ok(Self::Aarch64), + _ => Err("invalid target architecture"), + } + } +} + +/** + A full target description that Lune supports (OS + Arch) + + This is used to determine the target to build for standalone binaries, + and to download the correct base executable for cross-compilation. + + The target may be parsed from and displayed in the form `os-arch`. + Examples of valid targets are: + + - `linux-aarch64` + - `linux-x86_64` + - `macos-aarch64` + - `macos-x86_64` + - `windows-x86_64` +*/ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuildTarget { + pub os: BuildTargetOS, + pub arch: BuildTargetArch, +} + +impl BuildTarget { + pub fn current_system() -> Self { + Self { + os: BuildTargetOS::current_system(), + arch: BuildTargetArch::current_system(), + } + } + + pub fn is_current_system(&self) -> bool { + self.os == BuildTargetOS::current_system() && self.arch == BuildTargetArch::current_system() + } + + pub fn exe_extension(&self) -> &'static str { + self.os.exe_extension() + } + + pub fn exe_suffix(&self) -> &'static str { + self.os.exe_suffix() + } + + pub fn cache_path(&self) -> PathBuf { + CACHE_DIR.join(format!("{self}{}", self.os.exe_extension())) + } +} + +impl fmt::Display for BuildTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", self.os, self.arch) + } +} + +impl FromStr for BuildTarget { + type Err = &'static str; + fn from_str(s: &str) -> Result { + let (left, right) = s + .split_once('-') + .ok_or("target must be in the form `os-arch`")?; + + let os = left.parse()?; + let arch = right.parse()?; + + Ok(Self { os, arch }) + } +} diff --git a/src/lune/builtins/fs/mod.rs b/src/lune/builtins/fs/mod.rs index 2c7b9fb..0db1f7f 100644 --- a/src/lune/builtins/fs/mod.rs +++ b/src/lune/builtins/fs/mod.rs @@ -1,6 +1,7 @@ use std::io::ErrorKind as IoErrorKind; use std::path::{PathBuf, MAIN_SEPARATOR}; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use tokio::fs; @@ -32,6 +33,7 @@ pub fn create(lua: &Lua) -> LuaResult { async fn fs_read_file(lua: &Lua, path: String) -> LuaResult { let bytes = fs::read(&path).await.into_lua_err()?; + lua.create_string(bytes) } @@ -64,8 +66,8 @@ async fn fs_read_dir(_: &Lua, path: String) -> LuaResult> { Ok(dir_strings_no_prefix) } -async fn fs_write_file(_: &Lua, (path, contents): (String, LuaString<'_>)) -> LuaResult<()> { - fs::write(&path, &contents.as_bytes()).await.into_lua_err() +async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> { + fs::write(&path, contents.as_bytes()).await.into_lua_err() } async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> { diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs index 51d0290..8006a80 100644 --- a/src/lune/builtins/mod.rs +++ b/src/lune/builtins/mod.rs @@ -7,6 +7,7 @@ mod fs; mod luau; mod net; mod process; +mod regex; mod serde; mod stdio; mod task; @@ -22,6 +23,7 @@ pub enum LuneBuiltin { Net, Task, Process, + Regex, Serde, Stdio, #[cfg(feature = "roblox")] @@ -37,6 +39,7 @@ impl LuneBuiltin { Self::Net => "net", Self::Task => "task", Self::Process => "process", + Self::Regex => "regex", Self::Serde => "serde", Self::Stdio => "stdio", #[cfg(feature = "roblox")] @@ -52,6 +55,7 @@ impl LuneBuiltin { Self::Net => net::create(lua), Self::Task => task::create(lua), Self::Process => process::create(lua), + Self::Regex => regex::create(lua), Self::Serde => serde::create(lua), Self::Stdio => stdio::create(lua), #[cfg(feature = "roblox")] @@ -77,6 +81,7 @@ impl FromStr for LuneBuiltin { "net" => Ok(Self::Net), "task" => Ok(Self::Task), "process" => Ok(Self::Process), + "regex" => Ok(Self::Regex), "serde" => Ok(Self::Serde), "stdio" => Ok(Self::Stdio), #[cfg(feature = "roblox")] diff --git a/src/lune/builtins/net/config.rs b/src/lune/builtins/net/config.rs index 5d02d2b..1abd121 100644 --- a/src/lune/builtins/net/config.rs +++ b/src/lune/builtins/net/config.rs @@ -1,12 +1,16 @@ -use std::{collections::HashMap, net::Ipv4Addr}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, +}; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use reqwest::Method; use super::util::table_to_hash_map; -const DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#" return { @@ -104,10 +108,11 @@ impl FromLua<'_> for RequestConfig { Err(_) => HashMap::new(), }; // Extract body - let body = match tab.get::<_, LuaString>("body") { + let body = match tab.get::<_, BString>("body") { Ok(config_body) => Some(config_body.as_bytes().to_owned()), Err(_) => None, }; + // Convert method string into proper enum let method = method.trim().to_ascii_uppercase(); let method = match method.as_ref() { @@ -155,7 +160,7 @@ impl FromLua<'_> for RequestConfig { #[derive(Debug)] pub struct ServeConfig<'a> { - pub address: Ipv4Addr, + pub address: IpAddr, pub handle_request: LuaFunction<'a>, pub handle_web_socket: Option>, } @@ -175,7 +180,7 @@ impl<'lua> FromLua<'lua> for ServeConfig<'lua> { let handle_request: Option = t.get("handleRequest")?; let handle_web_socket: Option = t.get("handleWebSocket")?; if handle_request.is_some() || handle_web_socket.is_some() { - let address: Ipv4Addr = match &address { + let address: IpAddr = match &address { Some(addr) => { let addr_str = addr.to_str()?; diff --git a/src/lune/builtins/net/mod.rs b/src/lune/builtins/net/mod.rs index 6253dec..9449e6b 100644 --- a/src/lune/builtins/net/mod.rs +++ b/src/lune/builtins/net/mod.rs @@ -1,5 +1,6 @@ #![allow(unused_variables)] +use bstr::BString; use mlua::prelude::*; use mlua_luau_scheduler::LuaSpawnExt; @@ -23,7 +24,7 @@ use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; pub fn create(lua: &Lua) -> LuaResult { NetClientBuilder::new() - .headers(&[("User-Agent", create_user_agent_header())])? + .headers(&[("User-Agent", create_user_agent_header(lua)?)])? .build()? .into_registry(lua); TableBuilder::new(lua)? @@ -45,7 +46,7 @@ fn net_json_encode<'lua>( .serialize_to_string(lua, val) } -fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult> { +fn net_json_decode(lua: &Lua, json: BString) -> LuaResult { EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) } diff --git a/src/lune/builtins/net/server/request.rs b/src/lune/builtins/net/server/request.rs index dde529f..bab7a5d 100644 --- a/src/lune/builtins/net/server/request.rs +++ b/src/lune/builtins/net/server/request.rs @@ -18,21 +18,30 @@ impl LuaRequest { let path = self.head.uri.path().to_string(); let body = lua.create_string(&self.body)?; - let query: HashMap = self + let query: HashMap = self .head .uri .query() .unwrap_or_default() .split('&') .filter_map(|q| q.split_once('=')) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - let headers: HashMap> = self + .map(|(k, v)| { + let k = lua.create_string(k)?; + let v = lua.create_string(v)?; + Ok((k, v)) + }) + .collect::>()?; + + let headers: HashMap = self .head .headers .iter() - .map(|(k, v)| (k.as_str().to_string(), v.as_bytes().to_vec())) - .collect(); + .map(|(k, v)| { + let k = lua.create_string(k.as_str())?; + let v = lua.create_string(v.as_bytes())?; + Ok((k, v)) + }) + .collect::>()?; TableBuilder::new(lua)? .with_value("method", method)? diff --git a/src/lune/builtins/net/server/response.rs b/src/lune/builtins/net/server/response.rs index f575990..240a7cd 100644 --- a/src/lune/builtins/net/server/response.rs +++ b/src/lune/builtins/net/server/response.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use bstr::{BString, ByteSlice}; use http_body_util::Full; use hyper::{ body::Bytes, @@ -56,7 +57,7 @@ impl FromLua<'_> for LuaResponse { LuaValue::Table(t) => { let status: Option = t.get("status")?; let headers: Option = t.get("headers")?; - let body: Option = t.get("body")?; + let body: Option = t.get("body")?; let mut headers_map = HeaderMap::new(); if let Some(headers) = headers { diff --git a/src/lune/builtins/net/util.rs b/src/lune/builtins/net/util.rs index 4603547..e18235e 100644 --- a/src/lune/builtins/net/util.rs +++ b/src/lune/builtins/net/util.rs @@ -7,12 +7,19 @@ use mlua::prelude::*; use crate::lune::util::TableBuilder; -pub fn create_user_agent_header() -> String { - let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY") - .trim_start_matches("https://github.com/") - .split_once('/') - .unwrap(); - format!("{github_owner}-{github_repo}-cli") +pub fn create_user_agent_header(lua: &Lua) -> LuaResult { + let version_global = lua + .globals() + .get::<_, LuaString>("_VERSION") + .expect("Missing _VERSION global"); + + let version_global_str = version_global + .to_str() + .context("Invalid utf8 found in _VERSION global")?; + + let (package_name, full_version) = version_global_str.split_once(' ').unwrap(); + + Ok(format!("{}/{}", package_name.to_lowercase(), full_version)) } pub fn header_map_to_table( diff --git a/src/lune/builtins/net/websocket.rs b/src/lune/builtins/net/websocket.rs index fb7233c..5dba4ec 100644 --- a/src/lune/builtins/net/websocket.rs +++ b/src/lune/builtins/net/websocket.rs @@ -3,6 +3,7 @@ use std::sync::{ Arc, }; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use futures_util::{ @@ -160,7 +161,7 @@ where methods.add_async_method( "send", - |_, this, (string, as_binary): (LuaString, Option)| async move { + |_, this, (string, as_binary): (BString, Option)| async move { this.send(if as_binary.unwrap_or_default() { WsMessage::Binary(string.as_bytes().to_vec()) } else { diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs new file mode 100644 index 0000000..5dbea74 --- /dev/null +++ b/src/lune/builtins/regex/captures.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::{Captures, Regex}; +use self_cell::self_cell; + +use super::matches::LuaMatch; + +type OptionalCaptures<'a> = Option>; + +self_cell! { + struct LuaCapturesInner { + owner: Arc, + #[covariant] + dependent: OptionalCaptures, + } +} + +/** + A wrapper over the `regex::Captures` struct that can be used from Lua. +*/ +pub struct LuaCaptures { + inner: LuaCapturesInner, +} + +impl LuaCaptures { + /** + Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text. + + Returns `Some(_)` if captures were found, `None` if no captures were found. + */ + pub fn new(pattern: &Regex, text: String) -> Option { + let inner = + LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())); + if inner.borrow_dependent().is_some() { + Some(Self { inner }) + } else { + None + } + } + + fn captures(&self) -> &Captures { + self.inner + .borrow_dependent() + .as_ref() + .expect("None captures should not be used") + } + + fn num_captures(&self) -> usize { + // NOTE: Here we exclude the match for the entire regex + // pattern, only counting the named and numbered captures + self.captures().len() - 1 + } + + fn text(&self) -> Arc { + Arc::clone(self.inner.borrow_owner()) + } +} + +impl LuaUserData for LuaCaptures { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("get", |_, this, index: usize| { + Ok(this + .captures() + .get(index) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("group", |_, this, group: String| { + Ok(this + .captures() + .name(&group) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("format", |_, this, format: String| { + let mut new = String::new(); + this.captures().expand(&format, &mut new); + Ok(new) + }); + + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexCaptures({})", this.num_captures())) + }); + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures"); + } +} diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs new file mode 100644 index 0000000..bc109f8 --- /dev/null +++ b/src/lune/builtins/regex/matches.rs @@ -0,0 +1,53 @@ +use std::{ops::Range, sync::Arc}; + +use mlua::prelude::*; +use regex::Match; + +/** + A wrapper over the `regex::Match` struct that can be used from Lua. +*/ +pub struct LuaMatch { + text: Arc, + start: usize, + end: usize, +} + +impl LuaMatch { + /** + Create a new `LuaMatch` instance from a `String` text and a `regex::Match`. + */ + pub fn new(text: Arc, matched: Match) -> Self { + Self { + text, + start: matched.start(), + end: matched.end(), + } + } + + fn range(&self) -> Range { + self.start..self.end + } + + fn slice(&self) -> &str { + &self.text[self.range()] + } +} + +impl LuaUserData for LuaMatch { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + // NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive + fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1))); + fields.add_field_method_get("finish", |_, this| Ok(this.end)); + fields.add_field_method_get("len", |_, this| Ok(this.range().len())); + fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string())); + + fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch"); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexMatch({})", this.slice())) + }); + } +} diff --git a/src/lune/builtins/regex/mod.rs b/src/lune/builtins/regex/mod.rs new file mode 100644 index 0000000..bb674c2 --- /dev/null +++ b/src/lune/builtins/regex/mod.rs @@ -0,0 +1,21 @@ +#![allow(clippy::module_inception)] + +use mlua::prelude::*; + +use crate::lune::util::TableBuilder; + +mod captures; +mod matches; +mod regex; + +use self::regex::LuaRegex; + +pub fn create(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("new", new_regex)? + .build_readonly() +} + +fn new_regex(_: &Lua, pattern: String) -> LuaResult { + LuaRegex::new(pattern) +} diff --git a/src/lune/builtins/regex/regex.rs b/src/lune/builtins/regex/regex.rs new file mode 100644 index 0000000..3325e5d --- /dev/null +++ b/src/lune/builtins/regex/regex.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::Regex; + +use super::{captures::LuaCaptures, matches::LuaMatch}; + +/** + A wrapper over the `regex::Regex` struct that can be used from Lua. +*/ +#[derive(Debug, Clone)] +pub struct LuaRegex { + inner: Regex, +} + +impl LuaRegex { + /** + Create a new `LuaRegex` instance from a `String` pattern. + */ + pub fn new(pattern: String) -> LuaResult { + Regex::new(&pattern) + .map(|inner| Self { inner }) + .map_err(LuaError::external) + } +} + +impl LuaUserData for LuaRegex { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("isMatch", |_, this, text: String| { + Ok(this.inner.is_match(&text)) + }); + + methods.add_method("find", |_, this, text: String| { + let arc = Arc::new(text); + Ok(this + .inner + .find(&arc) + .map(|m| LuaMatch::new(Arc::clone(&arc), m))) + }); + + methods.add_method("captures", |_, this, text: String| { + Ok(LuaCaptures::new(&this.inner, text)) + }); + + methods.add_method("split", |_, this, text: String| { + Ok(this + .inner + .split(&text) + .map(|s| s.to_string()) + .collect::>()) + }); + + // TODO: Determine whether it's desirable and / or feasible to support + // using a function or table for `replace` like in the lua string library + methods.add_method( + "replace", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace(&haystack, replacer).to_string()) + }, + ); + methods.add_method( + "replaceAll", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace_all(&haystack, replacer).to_string()) + }, + ); + + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("Regex({})", this.inner.as_str())) + }); + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_meta_field(LuaMetaMethod::Type, "Regex"); + } +} diff --git a/src/lune/builtins/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs index 1f83ce4..8457a25 100644 --- a/src/lune/builtins/serde/encode_decode.rs +++ b/src/lune/builtins/serde/encode_decode.rs @@ -1,3 +1,4 @@ +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use serde_json::Value as JsonValue; @@ -86,11 +87,7 @@ impl EncodeDecodeConfig { lua.create_string(bytes) } - pub fn deserialize_from_string<'lua>( - self, - lua: &'lua Lua, - string: LuaString<'lua>, - ) -> LuaResult> { + pub fn deserialize_from_string(self, lua: &Lua, string: BString) -> LuaResult { let bytes = string.as_bytes(); match self.format { EncodeDecodeFormat::Json => { diff --git a/src/lune/builtins/serde/mod.rs b/src/lune/builtins/serde/mod.rs index c149467..de351a3 100644 --- a/src/lune/builtins/serde/mod.rs +++ b/src/lune/builtins/serde/mod.rs @@ -1,3 +1,4 @@ +use bstr::BString; use mlua::prelude::*; pub(super) mod compress_decompress; @@ -25,26 +26,23 @@ fn serde_encode<'lua>( config.serialize_to_string(lua, val) } -fn serde_decode<'lua>( - lua: &'lua Lua, - (format, str): (EncodeDecodeFormat, LuaString<'lua>), -) -> LuaResult> { +fn serde_decode(lua: &Lua, (format, str): (EncodeDecodeFormat, BString)) -> LuaResult { let config = EncodeDecodeConfig::from(format); config.deserialize_from_string(lua, str) } -async fn serde_compress<'lua>( - lua: &'lua Lua, - (format, str): (CompressDecompressFormat, LuaString<'lua>), -) -> LuaResult> { +async fn serde_compress( + lua: &Lua, + (format, str): (CompressDecompressFormat, BString), +) -> LuaResult { let bytes = compress(format, str).await?; lua.create_string(bytes) } -async fn serde_decompress<'lua>( - lua: &'lua Lua, - (format, str): (CompressDecompressFormat, LuaString<'lua>), -) -> LuaResult> { +async fn serde_decompress( + lua: &Lua, + (format, str): (CompressDecompressFormat, BString), +) -> LuaResult { let bytes = decompress(format, str).await?; lua.create_string(bytes) } diff --git a/src/lune/builtins/stdio/mod.rs b/src/lune/builtins/stdio/mod.rs index 14927f2..f851bc7 100644 --- a/src/lune/builtins/stdio/mod.rs +++ b/src/lune/builtins/stdio/mod.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; use mlua_luau_scheduler::LuaSpawnExt; -use tokio::io::{self, AsyncWriteExt}; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use crate::lune::util::{ formatting::{ @@ -21,6 +21,7 @@ pub fn create(lua: &Lua) -> LuaResult> { .with_function("format", stdio_format)? .with_async_function("write", stdio_write)? .with_async_function("ewrite", stdio_ewrite)? + .with_async_function("readToEnd", stdio_read_to_end)? .with_async_function("prompt", stdio_prompt)? .build_readonly() } @@ -53,6 +54,21 @@ async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { Ok(()) } +/* + FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader. + + This is a bit tricky since we would want to be able to use **both** readLine and readToEnd + in the same script, doing something like readLine, readLine, readToEnd from lua, and + having that capture the first two lines and then read the rest of the input. +*/ + +async fn stdio_read_to_end(lua: &Lua, _: ()) -> LuaResult { + let mut input = Vec::new(); + let mut stdin = io::stdin(); + stdin.read_to_end(&mut input).await?; + lua.create_string(&input) +} + async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult { lua.spawn_blocking(move || prompt(options)) .await diff --git a/src/lune/globals/version.rs b/src/lune/globals/version.rs index 4904144..1228fd0 100644 --- a/src/lune/globals/version.rs +++ b/src/lune/globals/version.rs @@ -13,7 +13,7 @@ pub fn create(lua: &Lua) -> LuaResult> { // If this function runs more than once, we // may get an already formatted lune version. - if luau_version_str.starts_with(&lune_version) { + if luau_version_str.starts_with(lune_version.as_str()) { return Ok(luau_version_full); } diff --git a/src/main.rs b/src/main.rs index ca28c76..734436d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ clippy::match_bool, clippy::module_name_repetitions, clippy::multiple_crate_versions, - clippy::needless_pass_by_value + clippy::needless_pass_by_value, + clippy::declare_interior_mutable_const, + clippy::borrow_interior_mutable_const )] use std::process::ExitCode; diff --git a/src/standalone/metadata.rs b/src/standalone/metadata.rs index 65249a9..f2ddfd9 100644 --- a/src/standalone/metadata.rs +++ b/src/standalone/metadata.rs @@ -5,10 +5,9 @@ use mlua::Compiler as LuaCompiler; use once_cell::sync::Lazy; use tokio::fs; -const MAGIC: &[u8; 8] = b"cr3sc3nt"; - -static CURRENT_EXE: Lazy = +pub const CURRENT_EXE: Lazy = Lazy::new(|| env::current_exe().expect("failed to get current exe")); +const MAGIC: &[u8; 8] = b"cr3sc3nt"; /* TODO: Right now all we do is append the bytecode to the end @@ -49,15 +48,19 @@ impl Metadata { /** Creates a patched standalone binary from the given script contents. */ - pub async fn create_env_patched_bin(script_contents: impl Into>) -> Result> { - let mut patched_bin = fs::read(CURRENT_EXE.to_path_buf()).await?; - - // Compile luau input into bytecode - let bytecode = LuaCompiler::new() + pub async fn create_env_patched_bin( + base_exe_path: PathBuf, + script_contents: impl Into>, + ) -> Result> { + let compiler = LuaCompiler::new() .set_optimization_level(2) .set_coverage_level(0) - .set_debug_level(1) - .compile(script_contents.into()); + .set_debug_level(1); + + let mut patched_bin = fs::read(base_exe_path).await?; + + // Compile luau input into bytecode + let bytecode = compiler.compile(script_contents.into()); // Append the bytecode / metadata to the end let meta = Self { bytecode }; diff --git a/src/tests.rs b/src/tests.rs index 6fce6e5..d33a225 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -83,6 +83,10 @@ create_tests! { process_spawn_stdin: "process/spawn/stdin", process_spawn_stdio: "process/spawn/stdio", + regex_general: "regex/general", + regex_metamethods: "regex/metamethods", + regex_replace: "regex/replace", + require_aliases: "require/tests/aliases", require_async: "require/tests/async", require_async_concurrent: "require/tests/async_concurrent", diff --git a/tests/fs/copy.luau b/tests/fs/copy.luau index c7ed6dd..1b52320 100644 --- a/tests/fs/copy.luau +++ b/tests/fs/copy.luau @@ -50,15 +50,15 @@ assert(fs.isFile(TEMP_ROOT_PATH_2 .. "/foo/buzz"), "Missing copied file - root/f -- Make sure the copied files are correct assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/bar/baz" ) assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/fizz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/fizz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/fizz" ) assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/buzz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/buzz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/buzz" ) diff --git a/tests/fs/files.luau b/tests/fs/files.luau index a49412e..38b7485 100644 --- a/tests/fs/files.luau +++ b/tests/fs/files.luau @@ -11,6 +11,8 @@ fs.writeDir(TEMP_ROOT_PATH) -- Write both of our files +-- binaryBlob is of type buffer to make sure fs.writeFile +-- works with both strings and buffers fs.writeFile(TEMP_ROOT_PATH .. "/test_binary", utils.binaryBlob) fs.writeFile(TEMP_ROOT_PATH .. "/test_json.json", utils.jsonBlob) @@ -18,7 +20,7 @@ fs.writeFile(TEMP_ROOT_PATH .. "/test_json.json", utils.jsonBlob) -- wrote gets us back the original strings assert( - fs.readFile(TEMP_ROOT_PATH .. "/test_binary") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH .. "/test_binary") == buffer.tostring(utils.binaryBlob), "Binary file round-trip resulted in different strings" ) diff --git a/tests/fs/metadata.luau b/tests/fs/metadata.luau index e926a2c..8a5a795 100644 --- a/tests/fs/metadata.luau +++ b/tests/fs/metadata.luau @@ -45,7 +45,7 @@ assert(metaFile.kind == "file", "File metadata kind was invalid") local metaBefore = fs.metadata(TEMP_FILE_PATH) task.wait(1) -fs.writeFile(TEMP_FILE_PATH, utils.binaryBlob .. "\n") +fs.writeFile(TEMP_FILE_PATH, buffer.tostring(utils.binaryBlob) .. "\n") local metaAfter = fs.metadata(TEMP_FILE_PATH) assert( diff --git a/tests/fs/move.luau b/tests/fs/move.luau index 868a095..749c8cb 100644 --- a/tests/fs/move.luau +++ b/tests/fs/move.luau @@ -20,7 +20,7 @@ fs.move("bin/move_test_json.json", "bin/moved_test_json.json") -- wrote gets us back the original strings assert( - fs.readFile("bin/moved_test_binary") == utils.binaryBlob, + fs.readFile("bin/moved_test_binary") == buffer.tostring(utils.binaryBlob), "Binary file round-trip resulted in different strings" ) diff --git a/tests/fs/utils.luau b/tests/fs/utils.luau index a5a947e..89a6964 100644 --- a/tests/fs/utils.luau +++ b/tests/fs/utils.luau @@ -16,6 +16,6 @@ local jsonBlob = serde.encode("json", { -- Return testing data and utils return { - binaryBlob = binaryBlob, + binaryBlob = buffer.fromstring(binaryBlob), jsonBlob = jsonBlob, } diff --git a/tests/net/request/user_agent.luau b/tests/net/request/user_agent.luau new file mode 100644 index 0000000..04491b7 --- /dev/null +++ b/tests/net/request/user_agent.luau @@ -0,0 +1,9 @@ +local net = require("@lune/net") + +local runtime, version = table.unpack(_VERSION:split(" ")) +local expectedUserAgent = runtime:lower() .. "/" .. version + +local userAgent: string = + net.jsonDecode(net.request("https://www.whatsmyua.info/api/v1/ua").body)[1].ua.rawUa + +assert(userAgent == expectedUserAgent, "Expected user agent to be " .. expectedUserAgent) diff --git a/tests/net/socket/wss_rw.luau b/tests/net/socket/wss_rw.luau index 8ae6066..fefb244 100644 --- a/tests/net/socket/wss_rw.luau +++ b/tests/net/socket/wss_rw.luau @@ -22,7 +22,9 @@ end) task.wait(1) -socket.send('{"op":1,"d":null}') +local payload = '{"op":1,"d":null}' +socket.send(payload) +socket.send(buffer.fromstring(payload)) socket.close(1000) task.cancel(delayedThread) diff --git a/tests/regex/general.luau b/tests/regex/general.luau new file mode 100644 index 0000000..24a73b5 --- /dev/null +++ b/tests/regex/general.luau @@ -0,0 +1,39 @@ +--!nocheck + +local regex = require("@lune/regex") + +local re = regex.new("[0-9]+") + +assert(re:isMatch("look, a number: 1241425") == true) + +local mtch = re:find("1337 wow") +assert(mtch ~= nil) +assert(mtch.start == 1) +assert(mtch.finish == 4) +assert(mtch.len == 4) +assert(mtch.text == "1337") +assert(#mtch == mtch.len) + +re = regex.new([[([0-9]+) (\d+) \D+ \d+ (\d)]]) +local captures = re:captures("1337 125600 wow! 1984 0") +assert(captures ~= nil) +assert(#captures == 3) +assert(captures:get(0).text == "1337 125600 wow! 1984 0") +assert(captures:get(1).text == "1337") +assert(captures:get(2).text == "125600") +assert(captures:get(3).text == "0") +assert(captures:get(4) == nil) + +assert(captures:format("$0") == "1337 125600 wow! 1984 0") +assert(captures:format("$3 $2 $1") == "0 125600 1337") + +re = regex.new("(?P[^ ]+)[ ]+(?P[^ ]+)(?P[ ]*)") +captures = re:captures("w1 w2 w3 w4") +assert(captures:format("$last $first$space") == "w2 w1 ") + +local split = regex.new([[,]]):split("1,2,3,4") +assert(#split == 4) +assert(split[1] == "1") +assert(split[2] == "2") +assert(split[3] == "3") +assert(split[4] == "4") diff --git a/tests/regex/metamethods.luau b/tests/regex/metamethods.luau new file mode 100644 index 0000000..f14231c --- /dev/null +++ b/tests/regex/metamethods.luau @@ -0,0 +1,16 @@ +--!nolint + +local regex = require("@lune/regex") + +local re = regex.new("[0-9]+") +assert(tostring(re) == "Regex([0-9]+)") +assert(typeof(re) == "Regex") + +local mtch = re:find("1337 wow") +assert(tostring(mtch) == "RegexMatch(1337)") +assert(typeof(mtch) == "RegexMatch") + +local re2 = regex.new("([0-9]+) ([0-9]+) wow! ([0-9]+) ([0-9]+)") +local captures = re2:captures("1337 125600 wow! 1984 0") +assert(tostring(captures) == "RegexCaptures(4)") +assert(typeof(captures) == "RegexCaptures") diff --git a/tests/regex/replace.luau b/tests/regex/replace.luau new file mode 100644 index 0000000..dc6b9f4 --- /dev/null +++ b/tests/regex/replace.luau @@ -0,0 +1,53 @@ +local regex = require("@lune/regex") + +-- Tests taken from the Regex crate + +local function replace( + name: string, + pattern: string, + find: string, + replace: string, + expected: string +) + local re = regex.new(pattern) + local replaced = re:replace(find, replace) + if replaced ~= expected then + error(`test '{name}' did not return expected result (expected {expected} got {replaced})`) + end +end + +local function replaceAll( + name: string, + pattern: string, + find: string, + replace: string, + expected: string +) + local re = regex.new(pattern) + local replaced = re:replaceAll(find, replace) + if replaced ~= expected then + error(`test '{name}' did not return expected result (expected {expected} got {replaced})`) + end +end + +replace("first", "[0-9]", "age: 26", "Z", "age: Z6") +replace("plus", "[0-9]+", "age: 26", "Z", "age: Z") +replaceAll("all", "[0-9]", "age: 26", "Z", "age: ZZ") +replace("groups", "([^ ]+)[ ]+([^ ]+)", "w1 w2", "$2 $1", "w2 w1") +replace("double dollar", "([^ ]+)[ ]+([^ ]+)", "w1 w2", "$2 $$1", "w2 $1") + +replaceAll( + "named", + "(?P[^ ]+)[ ]+(?P[^ ]+)(?P[ ]*)", + "w1 w2 w3 w4", + "$last $first$space", + "w2 w1 w4 w3" +) +replaceAll("trim", "^[ \t]+|[ \t]+$", " \t trim me\t \t", "", "trim me") +replace("number hypen", "(.)(.)", "ab", "$1-$2", "a-b") +replaceAll("simple expand", "([a-z]) ([a-z])", "a b", "$2 $1", "b a") +replaceAll("literal dollar 1", "([a-z]+) ([a-z]+)", "a b", "$$1", "$1") +replaceAll("literal dollar 2", "([a-z]+) ([a-z]+)", "a b", "$2 $$c $1", "b $c a") + +replaceAll("match at start replace with empty", "foo", "foobar", "", "bar") +replace("single empty match", "^", "bar", "foo", "foobar") diff --git a/tests/roblox/rbx-test-files b/tests/roblox/rbx-test-files index 52f2c1a..655b5cc 160000 --- a/tests/roblox/rbx-test-files +++ b/tests/roblox/rbx-test-files @@ -1 +1 @@ -Subproject commit 52f2c1a686e7b67d996005eeddf63b97b170a741 +Subproject commit 655b5cc6a64024709d3662cc45ec4319c87de5a2 diff --git a/tests/serde/compression/files.luau b/tests/serde/compression/files.luau index 8d41492..6f32ed0 100644 --- a/tests/serde/compression/files.luau +++ b/tests/serde/compression/files.luau @@ -33,69 +33,60 @@ local TESTS: { Test } = { } local failed = false -for _, test in TESTS do - local source = fs.readFile(test.Source) - local target = fs.readFile(test.Target) - - local success, compressed = pcall(serde.compress, test.Format, source) +local function testOperation( + operationName: "Compress" | "Decompress", + operation: ( + format: serde.CompressDecompressFormat, + s: buffer | string + ) -> string, + format: serde.CompressDecompressFormat, + source: string | buffer, + target: string +) + local success, res = pcall(operation, format, source) if not success then stdio.ewrite( string.format( - "Compressing source using '%s' format threw an error!\n%s", - tostring(test.Format), - tostring(compressed) + "%sing source using '%s' format threw an error!\n%s", + operationName, + tostring(format), + tostring(res) ) ) failed = true - continue - elseif compressed ~= target then + elseif res ~= target then stdio.ewrite( string.format( - "Compressing source using '%s' format did not produce target!\n", - tostring(test.Format) + "%sing source using '%s' format did not produce target!\n", + operationName, + tostring(format) ) ) stdio.ewrite( string.format( - "Compressed (%d chars long):\n%s\nTarget (%d chars long):\n%s\n\n", - #compressed, - tostring(compressed), + "%sed (%d chars long):\n%s\nTarget (%d chars long):\n%s\n\n", + operationName, + #res, + tostring(res), #target, tostring(target) ) ) failed = true - continue end +end - local success2, decompressed = pcall(serde.decompress, test.Format, target) - if not success2 then - stdio.ewrite( - string.format( - "Decompressing source using '%s' format threw an error!\n%s", - tostring(test.Format), - tostring(decompressed) - ) - ) - failed = true - continue - elseif decompressed ~= source then - stdio.ewrite( - string.format( - "Decompressing target using '%s' format did not produce source!\n", - tostring(test.Format) - ) - ) - stdio.ewrite( - string.format( - "Decompressed (%d chars long):\n%s\n\n", - #decompressed, - tostring(decompressed) - ) - ) - failed = true - continue - end +for _, test in TESTS do + local source = fs.readFile(test.Source) + local target = fs.readFile(test.Target) + + -- Compression + testOperation("Compress", serde.compress, test.Format, source, target) + testOperation("Compress", serde.compress, test.Format, buffer.fromstring(source), target) + + -- Decompression + testOperation("Decompress", serde.decompress, test.Format, target, source) + testOperation("Decompress", serde.decompress, test.Format, buffer.fromstring(target), source) end if failed then diff --git a/types/fs.luau b/types/fs.luau index 1d80604..823f6f7 100644 --- a/types/fs.luau +++ b/types/fs.luau @@ -144,7 +144,7 @@ end @param path The path of the file @param contents The contents of the file ]=] -function fs.writeFile(path: string, contents: string) end +function fs.writeFile(path: string, contents: buffer | string) end --[=[ @within FS diff --git a/types/net.luau b/types/net.luau index 47c6249..e9b793e 100644 --- a/types/net.luau +++ b/types/net.luau @@ -36,7 +36,7 @@ export type FetchParamsOptions = { export type FetchParams = { url: string, method: HttpMethod?, - body: string?, + body: (string | buffer)?, query: HttpQueryMap?, headers: HttpHeaderMap?, options: FetchParamsOptions?, @@ -101,7 +101,7 @@ export type ServeRequest = { export type ServeResponse = { status: number?, headers: { [string]: string }?, - body: string?, + body: (string | buffer)?, } type ServeHttpHandler = (request: ServeRequest) -> string | ServeResponse @@ -174,7 +174,7 @@ export type ServeHandle = { export type WebSocket = { closeCode: number?, close: (code: number?) -> (), - send: (message: string, asBinaryMessage: boolean?) -> (), + send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), next: () -> string?, } diff --git a/types/regex.luau b/types/regex.luau new file mode 100644 index 0000000..e148c73 --- /dev/null +++ b/types/regex.luau @@ -0,0 +1,218 @@ +--[=[ + @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) + +--[=[ + @class RegexCaptures + + Captures from a regular expression. +]=] +local RegexCaptures = {} + +--[=[ + @within RegexCaptures + @tag Method + + 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 +]=] +function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + 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 +]=] +function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + Formats the captures using the given format string. + + ### Example usage + + ```lua + local regex = require("@lune/regex") + + local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[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 +]=] +function RegexCaptures.format(self: RegexCaptures, format: string): string + return nil :: any +end + +export type RegexCaptures = typeof(RegexCaptures) + +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 diff --git a/types/serde.luau b/types/serde.luau index 6bd63db..c4a21d8 100644 --- a/types/serde.luau +++ b/types/serde.luau @@ -70,7 +70,7 @@ end @param encoded The string to decode @return The decoded lua value ]=] -function serde.decode(format: EncodeDecodeFormat, encoded: string): any +function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any return nil :: any end @@ -93,7 +93,7 @@ end @param s The string to compress @return The compressed string ]=] -function serde.compress(format: CompressDecompressFormat, s: string): string +function serde.compress(format: CompressDecompressFormat, s: buffer | string): string return nil :: any end @@ -116,7 +116,7 @@ end @param s The string to decompress @return The decompressed string ]=] -function serde.decompress(format: CompressDecompressFormat, s: string): string +function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string return nil :: any end diff --git a/types/stdio.luau b/types/stdio.luau index 3c5198e..e6e88a4 100644 --- a/types/stdio.luau +++ b/types/stdio.luau @@ -58,6 +58,9 @@ end stdio.write("World! ") stdio.write("All on the same line") stdio.ewrite("\nAnd some error text, too") + + -- Reading the entire input from stdin + local input = stdio.readToEnd() ``` ]=] local stdio = {} @@ -143,4 +146,16 @@ function stdio.write(s: string) end ]=] function stdio.ewrite(s: string) end +--[=[ + @within Stdio + @tag must_use + + Reads the entire input from stdin. + + @return The input from stdin +]=] +function stdio.readToEnd(): string + return nil :: any +end + return stdio