From 30def245f00d37f9956e422a03a2b084e2d253c1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Jul 2026 20:31:51 -0600 Subject: [PATCH] Mods: Reject old layout mods & improve errors --- src/dusk/data.cpp | 46 +++++++++ src/dusk/data.hpp | 3 + src/dusk/io.cpp | 20 ++-- src/dusk/mod_loader.hpp | 20 +++- src/dusk/mods/loader/loader.cpp | 164 +++++++++++++++++++------------- src/dusk/mods/svc/host.cpp | 4 +- src/dusk/ui/settings.cpp | 54 +---------- 7 files changed, 179 insertions(+), 132 deletions(-) diff --git a/src/dusk/data.cpp b/src/dusk/data.cpp index 69ee5bebc8..48517b7308 100644 --- a/src/dusk/data.cpp +++ b/src/dusk/data.cpp @@ -1005,4 +1005,50 @@ Paths initialize_data() { }; } +std::filesystem::path user_home_path() { + const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME); + if (homePath == nullptr || homePath[0] == '\0') { + return {}; + } + return std::filesystem::path{reinterpret_cast(homePath)}; +} + +std::filesystem::path normalized_display_path(const std::filesystem::path& path) { + std::error_code ec; + auto normalized = std::filesystem::weakly_canonical(path, ec); + if (!ec) { + return normalized; + } + + normalized = std::filesystem::absolute(path, ec); + if (!ec) { + return normalized.lexically_normal(); + } + + return path.lexically_normal(); +} + +std::string abbreviated_path_string(const std::filesystem::path& path) { + const auto homePath = user_home_path(); + if (path.empty() || homePath.empty()) { + return io::fs_path_to_string(path); + } + + const auto normalizedPath = normalized_display_path(path); + const auto normalizedHome = normalized_display_path(homePath); + if (normalizedPath == normalizedHome) { + return "~"; + } + + const auto relativePath = normalizedPath.lexically_relative(normalizedHome); + if (!relativePath.empty() && !relativePath.is_absolute()) { + const auto it = relativePath.begin(); + if (it == relativePath.end() || *it != "..") { + return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath); + } + } + + return io::fs_path_to_string(path); +} + } // namespace dusk::data diff --git a/src/dusk/data.hpp b/src/dusk/data.hpp index 7e7861dbf0..a2f9313852 100644 --- a/src/dusk/data.hpp +++ b/src/dusk/data.hpp @@ -39,5 +39,8 @@ bool set_portable_data_path(); bool reset_data_path(); bool is_default_data_path(); bool is_data_path_restart_pending(); +std::filesystem::path user_home_path(); +std::filesystem::path normalized_display_path(const std::filesystem::path& path); +std::string abbreviated_path_string(const std::filesystem::path& path); } // namespace dusk::data diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 88bd04ae8f..7638194d35 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -5,8 +5,6 @@ #include "dusk/io.hpp" -using namespace dusk::io; - #if _WIN32 #define MODE_TYPE wchar_t #define MODE(val) L##val @@ -23,7 +21,10 @@ using namespace dusk::io; #define _SH_DENYWR 0 #endif -static FILE* ThrowIfNotOpen(const FileStream& file) { +namespace dusk::io { +namespace { + +FILE* ThrowIfNotOpen(const FileStream& file) { if (!file.GetFileHandle()) { throw std::runtime_error("Invalid file handle!"); } @@ -31,14 +32,14 @@ static FILE* ThrowIfNotOpen(const FileStream& file) { return static_cast(file.GetFileHandle()); } -[[noreturn]] static void ThrowForError(int code) { +[[noreturn]] void ThrowForError(int code) { if (code == 0) { throw std::system_error(std::make_error_code(std::errc::io_error)); } throw std::system_error(std::make_error_code(static_cast(code))); } -static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) { +FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) { FILE* file; int err; @@ -59,12 +60,13 @@ static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, return file; } -static FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) { +FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) { return OpenCore(reinterpret_cast(path), mode, shareFlag); } -FileStream::FileStream() noexcept : file(nullptr) { -} +} // namespace + +FileStream::FileStream() noexcept : file(nullptr) {} FileStream::FileStream(FILE* file) : file(file) { if (!file) { @@ -184,3 +186,5 @@ FILE* FileStream::ToInner() { file = nullptr; return handle; } + +} // namespace dusk::io diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index 74e01a0fdd..34e17274bc 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -139,6 +139,11 @@ enum class NativeModStatus : u8 { */ InvalidMetadata, + /** + * Mod bundle contains files in an invalid location. + */ + InvalidBundle, + /** * Unknown error loading the native mod. */ @@ -147,8 +152,10 @@ enum class NativeModStatus : u8 { struct LoadedMod { ModMetadata metadata; - std::string modPath; - std::string dir; + std::filesystem::path modPath; + std::filesystem::path dir; + // Stable UTF-8 storage for HostService::mod_dir. + std::string dirUtf8; uint32_t searchDirIndex = 0; // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. @@ -175,9 +182,11 @@ struct LoadedMod { // asset-only reloads, so it doubles as a generation for anything caching per-mod content. uint32_t cacheGeneration = 0; // Currently extracted native library, empty if none. - std::string nativePath; + std::filesystem::path nativePath; // Read-only directory containing the current platform's main module and runtime libraries. - std::string nativeDir; + std::filesystem::path nativeDir; + // Stable UTF-8 storage for HostService::native_dir. + std::string nativeDirUtf8; NativeModStatus nativeStatus = NativeModStatus::None; std::unique_ptr native; @@ -227,7 +236,7 @@ private: // the next tick, by which point every per-frame entry into the mod should have returned. struct RetiredNative { std::unique_ptr native; - std::string directory; + std::filesystem::path directory; }; std::vector> m_mods; @@ -242,6 +251,7 @@ private: void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex); void load_native(LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries); + bool load_native_if_present(LoadedMod& mod); // Resolved / if it exists on disk, empty otherwise. [[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const; void unload_native(LoadedMod& mod); diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index adf465e367..353a2ab596 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -10,10 +10,12 @@ #include #include #include +#include #include "../manifest.hpp" #include "depgraph.hpp" #include "dusk/config.hpp" +#include "dusk/data.hpp" #include "dusk/io.hpp" #include "dusk/mods/log_buffer.hpp" #include "dusk/mods/svc/config.hpp" @@ -109,17 +111,49 @@ std::unique_ptr load_bundle(const std::filesystem::path& modPath, boo } } -struct NativeLocateResult { +struct NativeRuntimeLocation { std::string entry; std::vector runtimeEntries; bool anyLibs = false; }; +struct NativeLocateFailure { + NativeModStatus status; + std::string logMessage; +}; + +using NativeLocateResult = std::variant; + +bool has_native_library_extension(std::string_view name) { + const auto endsWith = [name](std::string_view extension) { + if (name.size() < extension.size()) { + return false; + } + const auto suffix = name.substr(name.size() - extension.size()); + return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { + const auto lower = [](char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : + value; + }; + return lower(lhs) == lower(rhs); + }); + }; + return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); +} + NativeLocateResult locate_native_runtime(ModBundle& bundle) { - NativeLocateResult result; + NativeRuntimeLocation result; const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; for (const auto& name : bundle.getFileNames()) { + if (name.find('/') == std::string::npos && has_native_library_extension(name)) { + return NativeLocateFailure{ + NativeModStatus::InvalidBundle, + fmt::format( + "native library '{}' found at the root (natives go in /lib/{{platform}})", + name), + }; + } if (!name.starts_with(k_nativeLibDir)) { continue; } @@ -415,15 +449,6 @@ static bool parse_meta(NativeMod& native, LoadedMod& mod) { return true; } -static bool validate_context_symbol(ModContext** contextSymbol, LoadedMod& mod) { - if (contextSymbol == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); - mod.nativeStatus = NativeModStatus::MissingExport; - return false; - } - return true; -} - static std::string lifecycle_error_message( const char* fnName, const ModResult result, const ModError& error) { if (error.message[0] != '\0') { @@ -445,6 +470,8 @@ static std::string native_status_message(const NativeModStatus status) { return "Missing required mod API exports"; case NativeModStatus::InvalidMetadata: return "Invalid mod metadata records"; + case NativeModStatus::InvalidBundle: + return "Invalid mod bundle layout (old mod?)"; case NativeModStatus::Unknown: return "Unknown mod load failure"; case NativeModStatus::None: @@ -488,17 +515,18 @@ void ModLoader::load_native( fs::create_directories(scratchDir, ec); if (ec) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", - io::fs_path_to_string(scratchDir), ec.message()); + data::abbreviated_path_string(scratchDir), ec.message()); return; } - mod.dir = io::fs_path_to_string(fs::absolute(scratchDir)); + mod.dir = fs::absolute(scratchDir); + mod.dirUtf8 = io::fs_path_to_string(mod.dir); fs::path libPath; fs::path runtimeDir; DirectoryRollback runtimeDirRollback; if (mod.inPlace) { if (!dllEntry.empty()) { - libPath = fs::path(mod.modPath) / dllEntry; + libPath = mod.modPath / dllEntry; } else if (auto external = external_native_lib_path(mod); !external.empty()) { libPath = std::move(external); } else { @@ -525,7 +553,7 @@ void ModLoader::load_native( if (ec) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create native runtime directory {}: {}", - io::fs_path_to_string(runtimeDir), ec.message()); + data::abbreviated_path_string(runtimeDir), ec.message()); return; } @@ -580,7 +608,7 @@ void ModLoader::load_native( nativeMod->handle = std::make_unique(libPath); } catch (const std::runtime_error& e) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", - io::fs_path_to_string(libPath), e.what()); + data::abbreviated_path_string(libPath), e.what()); return; } @@ -595,7 +623,7 @@ void ModLoader::load_native( { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} missing required mod API exports; skipping", - io::fs_path_to_string(libPath.filename())); + data::abbreviated_path_string(libPath)); mod.nativeStatus = NativeModStatus::MissingExport; return; } @@ -604,18 +632,45 @@ void ModLoader::load_native( return; } - if (!validate_context_symbol(nativeMod->contextSymbol, mod)) { + if (nativeMod->contextSymbol == nullptr) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); + mod.nativeStatus = NativeModStatus::MissingExport; return; } *nativeMod->contextSymbol = mod.context.get(); - mod.nativePath = io::fs_path_to_string(fs::absolute(libPath)); - mod.nativeDir = io::fs_path_to_string(fs::absolute(runtimeDir)); + mod.nativePath = fs::absolute(libPath); + mod.nativeDir = fs::absolute(runtimeDir); + mod.nativeDirUtf8 = io::fs_path_to_string(mod.nativeDir); mod.native = std::move(nativeMod); mod.nativeStatus = NativeModStatus::Loaded; runtimeDirRollback.release(); } +bool ModLoader::load_native_if_present(LoadedMod& mod) { + const auto result = locate_native_runtime(*mod.bundle); + if (const auto* failure = std::get_if(&result)) { + mod.nativeStatus = failure->status; + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); + fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); + return false; + } + + const auto& native = std::get(result); + if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { + mod.nativeStatus = NativeModStatus::None; + return true; + } + + mod.nativeStatus = NativeModStatus::Unknown; + load_native(mod, native.entry, native.runtimeEntries); + if (mod.nativeStatus != NativeModStatus::Loaded) { + fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); + return false; + } + return true; +} + void ModLoader::unload_native(LoadedMod& mod) { if (!mod.native || mod.inPlace) { return; @@ -624,6 +679,7 @@ void ModLoader::unload_native(LoadedMod& mod) { m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)}); mod.nativePath.clear(); mod.nativeDir.clear(); + mod.nativeDirUtf8.clear(); } void ModLoader::drain_retired_natives() { @@ -714,8 +770,7 @@ void ModLoader::try_load_mod( try { bundle = load_bundle(modPath, fromDir); } catch (const std::exception& e) { - Log.error( - "Failed to open {} bundle: {}", io::fs_path_to_string(modPath.filename()), e.what()); + Log.error("Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); return; } @@ -723,26 +778,26 @@ void ModLoader::try_load_mod( try { metadata = load_metadata(modPath, *bundle); } catch (const std::exception& e) { - Log.error("bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what()); + Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what()); return; } if (const auto* existing = find_mod(metadata.id)) { if (existing->searchDirIndex < searchDirIndex) { - log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority copy {}", - io::fs_path_to_string(modPath.filename()), existing->modPath); + log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority duplicate {}", + data::abbreviated_path_string(modPath), + data::abbreviated_path_string(existing->modPath)); } else { log::write(metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", - io::fs_path_to_string(modPath.filename())); + data::abbreviated_path_string(modPath)); } return; } const auto& inserted = m_mods.emplace_back(std::make_unique()); - auto& mod = *inserted; mod.active = true; - mod.modPath = io::fs_path_to_string(fs::absolute(modPath)); + mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; mod.metadata = std::move(metadata); @@ -751,21 +806,12 @@ void ModLoader::try_load_mod( mod.context->mod = &mod; mod.cvarIsEnabled = std::make_unique>(mod_enabled_cvar_name(mod.metadata.id), true); - - const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle); - if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry, runtimeEntries); - if (mod.nativeStatus != NativeModStatus::Loaded) { - Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id); - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - } else { - mod.manifestInfo = build_manifest_info(mod.native->parsed); - } + if (load_native_if_present(mod) && mod.native) { + mod.manifestInfo = build_manifest_info(mod.native->parsed); } log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, - mod.metadata.version, mod.metadata.author, io::fs_path_to_string(modPath.filename())); + mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath)); } bool ModLoader::activate_mod(LoadedMod& mod) { @@ -890,9 +936,11 @@ void ModLoader::init() { if (!fs::is_directory(searchDir.path)) { if (dirIndex == 0) { - Log.info("mods directory '{}' not found", io::fs_path_to_string(searchDir.path)); + Log.info( + "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } else { - Log.debug("mods directory '{}' not found", io::fs_path_to_string(searchDir.path)); + Log.debug( + "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } continue; } @@ -1080,25 +1128,13 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) { if (mod.native || mod.nativeStatus == NativeModStatus::None) { return true; } - - const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle); - if (!anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::None; - return true; - } - - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry, runtimeEntries); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } - return true; + return load_native_if_present(mod); } bool ModLoader::reload_bundle(LoadedMod& mod) { namespace fs = std::filesystem; - log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", mod.modPath); + log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", + data::abbreviated_path_string(mod.modPath)); std::shared_ptr newBundle; ModMetadata newMetadata; @@ -1124,18 +1160,12 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { mod.failureReason.clear(); ModManifestInfo newInfo; - if (const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle); - anyLibs) - { - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry, runtimeEntries); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } + if (!load_native_if_present(mod)) { + return false; + } + if (mod.native) { newInfo = build_manifest_info(mod.native->parsed); } else { - mod.nativeStatus = NativeModStatus::None; ++mod.cacheGeneration; } diff --git a/src/dusk/mods/svc/host.cpp b/src/dusk/mods/svc/host.cpp index f5d409f797..5deccc7c9e 100644 --- a/src/dusk/mods/svc/host.cpp +++ b/src/dusk/mods/svc/host.cpp @@ -60,12 +60,12 @@ const char* host_mod_version(ModContext* context) { const char* host_mod_dir(ModContext* context) { const auto* mod = mod_from_context(context); - return mod != nullptr ? mod->dir.c_str() : ""; + return mod != nullptr ? mod->dirUtf8.c_str() : ""; } const char* host_native_dir(ModContext* context) { const auto* mod = mod_from_context(context); - return mod != nullptr ? mod->nativeDir.c_str() : ""; + return mod != nullptr ? mod->nativeDirUtf8.c_str() : ""; } struct LifecycleWatcher { diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index e2ee60729a..f6cfe0320f 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -219,55 +219,8 @@ AuroraBackend configured_backend() { return configuredBackend; } -std::filesystem::path normalized_display_path(const std::filesystem::path& path) { - std::error_code ec; - auto normalized = std::filesystem::weakly_canonical(path, ec); - if (!ec) { - return normalized; - } - - normalized = std::filesystem::absolute(path, ec); - if (!ec) { - return normalized.lexically_normal(); - } - - return path.lexically_normal(); -} - -std::filesystem::path user_home_path() { - const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME); - if (homePath == nullptr || homePath[0] == '\0') { - return {}; - } - return std::filesystem::path{reinterpret_cast(homePath)}; -} - -Rml::String abbreviated_data_path_string() { - const auto path = data::configured_data_path(); - const auto homePath = user_home_path(); - if (path.empty() || homePath.empty()) { - return io::fs_path_to_string(path); - } - - const auto normalizedPath = normalized_display_path(path); - const auto normalizedHome = normalized_display_path(homePath); - if (normalizedPath == normalizedHome) { - return "~"; - } - - const auto relativePath = normalizedPath.lexically_relative(normalizedHome); - if (!relativePath.empty() && !relativePath.is_absolute()) { - const auto it = relativePath.begin(); - if (it == relativePath.end() || *it != "..") { - return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath); - } - } - - return io::fs_path_to_string(path); -} - Rml::String configured_data_path_display_name() { - const auto path = abbreviated_data_path_string(); + const auto path = data::abbreviated_path_string(data::configured_data_path()); if (path.empty()) { return "(none)"; } @@ -284,8 +237,9 @@ public: explicit DataFolderPathText(Rml::Element* parent) : Component(append(parent, "div")) {} void update() override { - const Rml::String rml = "Current data folder:
" + - escape(abbreviated_data_path_string()) + "
"; + const Rml::String rml = + "Current data folder:
" + + escape(data::abbreviated_path_string(data::configured_data_path())) + "
"; if (rml != mCurrentRml) { mRoot->SetInnerRML(rml); mCurrentRml = rml;