mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-10-04 11:35:12 -04:00
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:
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user