From e964f7f8e2dca879bf8e32900679561339eea911 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 13 Sep 2026 22:14:31 +0200 Subject: [PATCH] Improve Luau require handling Now implicitly handles .lua files on top of .luau. Also handles directory/init.lua(u) when doing require("directory") cases of conflict (both .lua and .luau, or both .luau and directory) report an error For reference: https://github.com/luau-lang/rfcs/blob/79c722717ffd96e5c5b5a9493ff01fda8a5171be/docs/amended-require-resolution.md Fixes https://github.com/TwilitRealm/dusklight/issues/2406 --- mods/luau_runtime/src/runtime.cpp | 73 ++++++++++++++++++++++++---- sdk/include/mods/svc/resource.h | 5 +- src/dusk/archive.cpp | 52 +++++++++++++++----- src/dusk/archive.hpp | 4 +- src/dusk/mods/loader/bundle_disk.cpp | 8 +++ src/dusk/mods/loader/bundle_zip.cpp | 8 +++ src/dusk/mods/loader/loader.hpp | 6 +++ src/dusk/mods/svc/resource.cpp | 27 +++++++++- 8 files changed, 159 insertions(+), 24 deletions(-) diff --git a/mods/luau_runtime/src/runtime.cpp b/mods/luau_runtime/src/runtime.cpp index bb029e4645..87c103e3b3 100644 --- a/mods/luau_runtime/src/runtime.cpp +++ b/mods/luau_runtime/src/runtime.cpp @@ -154,12 +154,71 @@ std::optional normalize_module_path( } normalized.append(part); } - if (!normalized.ends_with(".luau")) { - normalized += ".luau"; - } + return normalized; } +std::optional try_load_direct_lua_files(lua_State* state, ModContext* mod, char const* modName, std::string const& fileWithoutExt) { + // Try .luau first. + auto try_luau = fileWithoutExt + ".luau"; + std::optional found_path; + if (svc_resource->file_exists(mod, try_luau.c_str())) { + found_path = try_luau; + } + + // Try .lua, if both it and .luau exist, there's a conflict. + auto try_lua = fileWithoutExt + ".lua"; + if (svc_resource->file_exists(mod, try_lua.c_str())) { + if (found_path.has_value()) { + luaL_error(state, "Cannot load module '%s' if both '%s' and '%s' exist", modName, try_luau.c_str(), try_lua.c_str()); + } + + found_path = try_lua; + } + + return found_path; +} + +std::string resolve_module_path(lua_State* state, ModContext* mod, std::string_view currentPath, std::string_view requested) { + // For reference: https://github.com/luau-lang/rfcs/blob/79c722717ffd96e5c5b5a9493ff01fda8a5171be/docs/amended-require-resolution.md + + // Normalize to remove ./ and ../ and such. Should be a path relative to res/ after this. + auto normalized = normalize_module_path(currentPath, requested); + if (!normalized.has_value()) { + luaL_error(state, "module paths must be relative and remain inside the mod's res directory"); + } + + auto has_lua_ext = normalized->ends_with(".lua") || normalized->ends_with(".luau"); + if (has_lua_ext) { + // No fancy logic in this case. + if (!svc_resource->file_exists(mod, normalized->c_str())) { + luaL_error(state, "Cannot load '%s': file does not exist", normalized->c_str()); + } + + return std::move(*normalized); + } + + auto found_path = try_load_direct_lua_files(state, mod, normalized->c_str(), *normalized); + if (found_path.has_value()) { + // If a .lua(u) file *and* a matching directory exist, it's a conflict. + if (svc_resource->directory_exists(mod, normalized->c_str())) { + luaL_error(state, "Cannot load '%s' because conflicting directory '%s' also exists", found_path->c_str(), normalized->c_str()); + } + } else { + // Check for directory/init.lua(u) + auto initPath = *normalized + "/init"; + + found_path = try_load_direct_lua_files(state, mod, normalized->c_str(), initPath); + } + + if (!found_path.has_value()) { + luaL_error(state, "Cannot load '%s': unable to locate '%s.lua(u)' or '%s/init.lua(u)'", + found_path->c_str(), normalized->c_str(), normalized->c_str()); + } + + return std::move(*found_path); +} + int module_require(lua_State* state); int print(lua_State* state); @@ -256,13 +315,7 @@ int module_require(lua_State* state) { } } else { const char* currentPath = lua_tostring(state, lua_upvalueindex(2)); - const auto normalized = - normalize_module_path(currentPath != nullptr ? currentPath : "main.luau", requested); - if (!normalized.has_value()) { - luaL_error( - state, "module paths must be relative and remain inside the mod's res directory"); - } - moduleName = *normalized; + moduleName = resolve_module_path(state, vm.subject, currentPath != nullptr ? currentPath : "main.luau", requested); } if (const auto found = vm.moduleRefs.find(moduleName); found != vm.moduleRefs.end()) { diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 516dcab20a..474c781b5b 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -14,7 +14,7 @@ #define RESOURCE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "resource" #define RESOURCE_SERVICE_MAJOR 1u -#define RESOURCE_SERVICE_MINOR 0u +#define RESOURCE_SERVICE_MINOR 1u /* * A loaded resource, allocated by the service. Return every successful load with free; @@ -44,6 +44,9 @@ typedef struct ResourceService { * already-freed buffer. */ void (*free)(ModContext* ctx, ResourceBuffer* buffer); + + bool (*file_exists)(ModContext* ctx, char const* relative_path); + bool (*directory_exists)(ModContext* ctx, char const* relative_path); } ResourceService; MOD_DECLARE_SERVICE(ResourceService, svc_resource, RESOURCE_SERVICE_ID, RESOURCE_SERVICE_MAJOR, diff --git a/src/dusk/archive.cpp b/src/dusk/archive.cpp index a749d5432b..c9ba131bc8 100644 --- a/src/dusk/archive.cpp +++ b/src/dusk/archive.cpp @@ -42,8 +42,7 @@ struct ZipArchive::Impl { } } - static size_t read_zip( - void* opaque, mz_uint64 offset, void* buffer, const size_t size) { + static size_t read_zip(void* opaque, mz_uint64 offset, void* buffer, const size_t size) { auto& archive = *static_cast(opaque); std::error_code error; return archive.file.read_at(offset, {static_cast(buffer), size}, error); @@ -127,19 +126,50 @@ std::vector ZipArchive::file_names() { return results; } -size_t ZipArchive::file_size(const std::string_view name) { - std::lock_guard lock{m_impl->mutex}; +std::optional stat_zip_entry( + mz_zip_archive* pZip, std::string_view name) { const std::string fileName{name}; - const auto index = mz_zip_reader_locate_file(&m_impl->zip, fileName.c_str(), nullptr, 0); - if (index < 0) { + uint32_t fileIdx; + if (!mz_zip_reader_locate_file_v2(pZip, fileName.c_str(), nullptr, 0, &fileIdx)) { + return std::nullopt; + } + + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(pZip, fileIdx, &stat)) { + return std::nullopt; + } + + return stat; +} + +bool ZipArchive::file_exists(const std::string& name) const { + std::lock_guard lock{m_impl->mutex}; + auto result = stat_zip_entry(&m_impl->zip, name); + if (!result.has_value()) { + return false; + } + + return !result->m_is_directory; +} + +bool ZipArchive::directory_exists(const std::string& name) const { + std::lock_guard lock{m_impl->mutex}; + auto result = stat_zip_entry(&m_impl->zip, name); + if (!result.has_value()) { + return false; + } + + return result->m_is_directory; +} + +size_t ZipArchive::file_size(const std::string& name) { + std::lock_guard lock{m_impl->mutex}; + auto stat = stat_zip_entry(&m_impl->zip, name); + if (!stat.has_value()) { throw std::runtime_error(fmt::format("Unable to locate file in ZIP: {}", name)); } - mz_zip_archive_file_stat stat{}; - if (!mz_zip_reader_file_stat(&m_impl->zip, static_cast(index), &stat)) { - throw std::runtime_error(fmt::format("Unable to inspect file in ZIP: {}", name)); - } - return static_cast(stat.m_uncomp_size); + return static_cast(stat->m_uncomp_size); } } // namespace dusk::archive diff --git a/src/dusk/archive.hpp b/src/dusk/archive.hpp index 2e1003d49c..5005620073 100644 --- a/src/dusk/archive.hpp +++ b/src/dusk/archive.hpp @@ -29,7 +29,9 @@ public: PackageFormat package_format() const noexcept; std::vector read_file(std::string_view name); std::vector file_names(); - size_t file_size(std::string_view name); + size_t file_size(const std::string& name); + bool file_exists(const std::string& name) const; + bool directory_exists(const std::string& name) const; private: struct Impl; diff --git a/src/dusk/mods/loader/bundle_disk.cpp b/src/dusk/mods/loader/bundle_disk.cpp index c9903b9b38..a94044e596 100644 --- a/src/dusk/mods/loader/bundle_disk.cpp +++ b/src/dusk/mods/loader/bundle_disk.cpp @@ -58,4 +58,12 @@ std::filesystem::path ModBundleDisk::toRealPath(const std::string& fileName) con return root_path / filePath; } +bool ModBundleDisk::file_exists(const std::string& fileName) { + return std::filesystem::is_regular_file(toRealPath(fileName)); +} + +bool ModBundleDisk::directory_exists(const std::string& fileName) { + return std::filesystem::is_directory(toRealPath(fileName)); +} + } // namespace dusk::mods diff --git a/src/dusk/mods/loader/bundle_zip.cpp b/src/dusk/mods/loader/bundle_zip.cpp index 630fa3ae97..6c3e798952 100644 --- a/src/dusk/mods/loader/bundle_zip.cpp +++ b/src/dusk/mods/loader/bundle_zip.cpp @@ -22,4 +22,12 @@ size_t ModBundleZip::getFileSize(const std::string& fileName) { return m_archive.file_size(fileName); } +bool ModBundleZip::file_exists(const std::string& fileName) { + return m_archive.file_exists(fileName); +} + +bool ModBundleZip::directory_exists(const std::string& fileName) { + return m_archive.directory_exists(fileName); +} + } // namespace dusk::mods diff --git a/src/dusk/mods/loader/loader.hpp b/src/dusk/mods/loader/loader.hpp index d5708c9be1..010abdb2d3 100644 --- a/src/dusk/mods/loader/loader.hpp +++ b/src/dusk/mods/loader/loader.hpp @@ -23,6 +23,8 @@ public: virtual std::vector readFile(const std::string& fileName) = 0; virtual std::vector getFileNames() = 0; virtual size_t getFileSize(const std::string& fileName) = 0; + virtual bool file_exists(std::string const& fileName) = 0; + virtual bool directory_exists(std::string const& fileName) = 0; }; class ModBundleZip final : public ModBundle { @@ -32,6 +34,8 @@ public: std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; size_t getFileSize(const std::string& fileName) override; + bool file_exists(const std::string& fileName) override; + bool directory_exists(const std::string& fileName) override; private: archive::ZipArchive m_archive; @@ -44,6 +48,8 @@ public: std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; size_t getFileSize(const std::string& fileName) override; + bool file_exists(const std::string& fileName) override; + bool directory_exists(const std::string& fileName) override; private: [[nodiscard]] std::filesystem::path toRealPath(const std::string& fileName) const; diff --git a/src/dusk/mods/svc/resource.cpp b/src/dusk/mods/svc/resource.cpp index d92630317e..57d3f6bdc0 100644 --- a/src/dusk/mods/svc/resource.cpp +++ b/src/dusk/mods/svc/resource.cpp @@ -35,6 +35,10 @@ void resource_remove_mod(LoadedMod& mod) { } } +std::string prefix_path(const char* relativePath) { + return fmt::format("res/{}", relativePath); +} + ModResult resource_load(ModContext* context, const char* relativePath, ResourceBuffer* outBuffer) { if (outBuffer == nullptr || outBuffer->struct_size < sizeof(ResourceBuffer)) { return MOD_INVALID_ARGUMENT; @@ -46,7 +50,7 @@ ModResult resource_load(ModContext* context, const char* relativePath, ResourceB return MOD_INVALID_ARGUMENT; } - const auto entry = fmt::format("res/{}", relativePath); + const auto entry = prefix_path(relativePath); std::vector data; try { data = mod->bundle->readFile(entry); @@ -91,10 +95,31 @@ void resource_free(ModContext* context, ResourceBuffer* buffer) { buffer->size = 0; } + +bool file_exists(ModContext* ctx, char const* relative_path) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || relative_path == nullptr || !is_safe_resource_path(relative_path)) { + return MOD_INVALID_ARGUMENT; + } + + return mod->bundle->file_exists(prefix_path(relative_path)); +} + +bool directory_exists(ModContext* ctx, char const* relative_path) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || relative_path == nullptr || !is_safe_resource_path(relative_path)) { + return MOD_INVALID_ARGUMENT; + } + + return mod->bundle->directory_exists(prefix_path(relative_path)); +} + constexpr ResourceService s_resourceService{ .header = SERVICE_HEADER(ResourceService, RESOURCE_SERVICE_MAJOR, RESOURCE_SERVICE_MINOR), .load = resource_load, .free = resource_free, + .file_exists = file_exists, + .directory_exists = directory_exists, }; } // namespace