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
This commit is contained in:
Pieter-Jan Briers
2026-09-13 22:14:31 +02:00
parent 07b2f5accf
commit e964f7f8e2
8 changed files with 159 additions and 24 deletions
+63 -10
View File
@@ -154,12 +154,71 @@ std::optional<std::string> normalize_module_path(
}
normalized.append(part);
}
if (!normalized.ends_with(".luau")) {
normalized += ".luau";
}
return normalized;
}
std::optional<std::string> 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<std::string> 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()) {
+4 -1
View File
@@ -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,
+41 -11
View File
@@ -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<Impl*>(opaque);
std::error_code error;
return archive.file.read_at(offset, {static_cast<std::byte*>(buffer), size}, error);
@@ -127,19 +126,50 @@ std::vector<std::string> ZipArchive::file_names() {
return results;
}
size_t ZipArchive::file_size(const std::string_view name) {
std::lock_guard lock{m_impl->mutex};
std::optional<mz_zip_archive_file_stat> 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<mz_uint>(index), &stat)) {
throw std::runtime_error(fmt::format("Unable to inspect file in ZIP: {}", name));
}
return static_cast<size_t>(stat.m_uncomp_size);
return static_cast<size_t>(stat->m_uncomp_size);
}
} // namespace dusk::archive
+3 -1
View File
@@ -29,7 +29,9 @@ public:
PackageFormat package_format() const noexcept;
std::vector<uint8_t> read_file(std::string_view name);
std::vector<std::string> 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;
+8
View File
@@ -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
+8
View File
@@ -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
+6
View File
@@ -23,6 +23,8 @@ public:
virtual std::vector<u8> readFile(const std::string& fileName) = 0;
virtual std::vector<std::string> 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<u8> readFile(const std::string& fileName) override;
std::vector<std::string> 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<u8> readFile(const std::string& fileName) override;
std::vector<std::string> 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;
+26 -1
View File
@@ -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<u8> 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