From 4d014574175e7c34d27a315b18b00cb65d8e2784 Mon Sep 17 00:00:00 2001 From: Danshet <264011288+Danshet@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:14:00 +0200 Subject: [PATCH] Fix the log file and path settings on non-ASCII install paths Sweeps the config fix's defect class across the rest of the tree: the log file itself opened through the ANSI code page, the user_data_root/update_data_root/log_file settings, and both mod manifest loaders shared the same narrowing --- src/ac6_native_assets.cpp | 16 +++- src/ac6_texture_overrides.cpp | 14 +++- .../rexglue-sdk/include/rex/logging/types.h | 6 +- thirdparty/rexglue-sdk/src/core/logging.cpp | 75 +++++++++++++++---- .../rexglue-sdk/src/native/ui/rex_app.cpp | 15 +++- .../rexglue-sdk/thirdparty/CMakeLists.txt | 8 ++ 6 files changed, 109 insertions(+), 25 deletions(-) diff --git a/src/ac6_native_assets.cpp b/src/ac6_native_assets.cpp index e695a8e3..574caed1 100644 --- a/src/ac6_native_assets.cpp +++ b/src/ac6_native_assets.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include +#include #include REXCVAR_DEFINE_BOOL( @@ -334,10 +336,20 @@ void LoadManifestForRoot(NativeAssetRegistryState& state, RootDescriptor& root) toml::table manifest; try { - manifest = toml::parse_file(manifest_path.string()); + // Path-native stream + path_to_utf8 in the log line: parse_file(.string()) + // narrows through the ANSI code page and fails on non-ASCII install + // paths (the same construct as the config-loader bug). + std::ifstream manifest_file(manifest_path, std::ios::binary); + if (!manifest_file) { + ++state.manifest_error_count; + REXLOG_WARN("AC6 native assets: failed to open {}", rex::path_to_utf8(manifest_path)); + return; + } + manifest = toml::parse(manifest_file, rex::path_to_utf8(manifest_path)); } catch (const toml::parse_error& err) { ++state.manifest_error_count; - REXLOG_WARN("AC6 native assets: failed to parse {}: {}", manifest_path.string(), err.description()); + REXLOG_WARN("AC6 native assets: failed to parse {}: {}", rex::path_to_utf8(manifest_path), + err.description()); return; } diff --git a/src/ac6_texture_overrides.cpp b/src/ac6_texture_overrides.cpp index cf9d2785..dfd1cfc9 100644 --- a/src/ac6_texture_overrides.cpp +++ b/src/ac6_texture_overrides.cpp @@ -350,10 +350,20 @@ TextureSwapManifestCacheEntry LoadTextureSwapManifestCacheEntry(const std::files toml::table manifest; try { - manifest = toml::parse_file(manifest_path.string()); + // Path-native stream + path_to_utf8 in the log line: parse_file(.string()) + // narrows through the ANSI code page and fails on non-ASCII install + // paths (the same construct as the config-loader bug). + std::ifstream manifest_file(manifest_path, std::ios::binary); + if (!manifest_file) { + entry.parse_failed = true; + REXLOG_WARN("Texture swap manifest {}: cannot open", rex::path_to_utf8(manifest_path)); + return entry; + } + manifest = toml::parse(manifest_file, rex::path_to_utf8(manifest_path)); } catch (const toml::parse_error& err) { entry.parse_failed = true; - REXLOG_WARN("Texture swap manifest {}: parse error: {}", manifest_path.string(), err.description()); + REXLOG_WARN("Texture swap manifest {}: parse error: {}", rex::path_to_utf8(manifest_path), + err.description()); return entry; } diff --git a/thirdparty/rexglue-sdk/include/rex/logging/types.h b/thirdparty/rexglue-sdk/include/rex/logging/types.h index 984e28a4..f34d4d00 100644 --- a/thirdparty/rexglue-sdk/include/rex/logging/types.h +++ b/thirdparty/rexglue-sdk/include/rex/logging/types.h @@ -70,7 +70,9 @@ struct LogConfig { * the platform debug sink created by InitLoggingEarly(). */ bool log_to_console = false; - /** Path to a log file, or nullptr for no file logging. */ + /** Path to a log file, or nullptr for no file logging. UTF-8 (the cvar/toml + * encoding); InitLogging converts it with rex::to_path, never through the + * ANSI code page. */ const char* log_file = nullptr; /** spdlog pattern string for the stdout console sink. */ @@ -109,6 +111,8 @@ struct LogConfig { bool category_sinks_exclusive = false; std::string app_name; + /** Directory for auto-named sequential logs. UTF-8 (produce it with + * rex::path_to_utf8); InitLogging converts back with rex::to_path. */ std::string log_dir; }; diff --git a/thirdparty/rexglue-sdk/src/core/logging.cpp b/thirdparty/rexglue-sdk/src/core/logging.cpp index 85c7d82a..845d746f 100644 --- a/thirdparty/rexglue-sdk/src/core/logging.cpp +++ b/thirdparty/rexglue-sdk/src/core/logging.cpp @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include #include +#include #include #include @@ -82,16 +85,29 @@ static void RemovePreviousLog(const std::filesystem::path& path, int max_files) std::error_code ec; std::filesystem::remove(path, ec); const std::filesystem::path dir = path.parent_path(); - const std::string stem = path.stem().string(); - const std::string ext = path.extension().string(); + // path_to_utf8/to_path, not .string(): the log file name is user-chosen and + // may contain characters outside the ANSI code page (MSVC's path::string() + // throws for those). + const std::string stem = rex::path_to_utf8(path.stem()); + const std::string ext = rex::path_to_utf8(path.extension()); for (int i = 1; i <= max_files && i <= 64; ++i) { - std::filesystem::remove(dir / fmt::format("{}.{}{}", stem, i, ext), ec); + std::filesystem::remove(dir / rex::to_path(fmt::format("{}.{}{}", stem, i, ext)), ec); } } std::filesystem::path NextSequentialLogPath(const std::filesystem::path& logs_dir, std::string_view app_name) { - std::filesystem::create_directories(logs_dir); + // error_code overload: the throwing create_directories could propagate a + // filesystem_error out of InitLogging during startup (e.g. an exe dir that + // narrowed badly elsewhere). A failed log dir means "no file sink", never + // a crash before the window exists. + std::error_code create_ec; + std::filesystem::create_directories(logs_dir, create_ec); + if (create_ec) { + std::fprintf(stderr, "logging: cannot create log directory %s: %s\n", + rex::path_to_utf8(logs_dir).c_str(), create_ec.message().c_str()); + return {}; + } int max_seq = 0; std::string prefix = std::string(app_name) + "_"; @@ -99,7 +115,7 @@ std::filesystem::path NextSequentialLogPath(const std::filesystem::path& logs_di for (const auto& entry : std::filesystem::directory_iterator(logs_dir, ec)) { if (!entry.is_regular_file()) continue; - auto stem = entry.path().stem().string(); + auto stem = rex::path_to_utf8(entry.path().stem()); if (stem.starts_with(prefix)) { auto num_str = stem.substr(prefix.size()); int num = 0; @@ -235,10 +251,13 @@ void InitLogging(const LogConfig& config) { g_console_sink = sink; } - // File sink (rotating) with sequential naming fallback - std::string resolved_path; + // File sink (rotating) with sequential naming fallback. The path stays a + // std::filesystem::path end-to-end; LogConfig's log_file/log_dir strings + // are UTF-8 and are converted with rex::to_path, never the implicit + // ANSI-code-page construction that mangled non-ASCII install paths. + std::filesystem::path resolved_path; if (config.log_file) { - resolved_path = config.log_file; + resolved_path = rex::to_path(config.log_file); // Fresh single log each launch (QoL): delete the previous one first so a // fixed log_file is replaced, not appended-to, and files don't stack up. if (!resolved_path.empty() && REXCVAR_GET(log_new_file_per_launch)) { @@ -246,16 +265,32 @@ void InitLogging(const LogConfig& config) { } } else if (!config.app_name.empty()) { auto log_dir = config.log_dir.empty() ? std::filesystem::current_path() / "logs" - : std::filesystem::path(config.log_dir); - resolved_path = NextSequentialLogPath(log_dir, config.app_name).string(); + : rex::to_path(config.log_dir); + resolved_path = NextSequentialLogPath(log_dir, config.app_name); } if (!resolved_path.empty()) { - auto sink = std::make_shared( - resolved_path, static_cast(REXCVAR_GET(log_max_file_size_mb)) * 1024 * 1024, + try { +#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES) + // Wide filename: spdlog opens via _wfsopen, so any Unicode path works. + // Without SPDLOG_WCHAR_FILENAMES the narrow filename goes through the + // ANSI code page in _fsopen and a Cyrillic/CJK exe dir gets no log at + // all - the user loses their diagnostics exactly when they need them. + spdlog::filename_t sink_filename = resolved_path.wstring(); +#else + spdlog::filename_t sink_filename = resolved_path.string(); +#endif + auto sink = std::make_shared( + sink_filename, static_cast(REXCVAR_GET(log_max_file_size_mb)) * 1024 * 1024, static_cast(REXCVAR_GET(log_max_files)), false); - sink->set_level(spdlog::level::trace); - sink->set_pattern(config.file_pattern); - g_file_sink = sink; + sink->set_level(spdlog::level::trace); + sink->set_pattern(config.file_pattern); + g_file_sink = sink; + } catch (const spdlog::spdlog_ex& e) { + // A file sink that cannot open must not take startup down with it. + // (No REXLOG here: we hold g_mutex and the macros can re-enter it.) + std::fprintf(stderr, "logging: cannot open log file %s: %s\n", + rex::path_to_utf8(resolved_path).c_str(), e.what()); + } } g_extra_sinks = config.extra_sinks; @@ -561,7 +596,15 @@ std::map ParseCategoryLevelsFromConfig( return result; try { - auto config = toml::parse_file(config_path.string()); + // Path-native stream, not parse_file(path.string()): the narrow + // conversion cannot represent (and on MSVC may throw for) characters + // outside the ANSI code page - the same construct as the config + // loader had, and this one ran before logging was even up. + std::ifstream file(config_path, std::ios::binary); + if (!file) { + return result; + } + auto config = toml::parse(file, rex::path_to_utf8(config_path)); auto* log_table = config["log"].as_table(); if (!log_table) return result; diff --git a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp index e54812ca..e0308d39 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp @@ -354,7 +354,9 @@ bool ReXApp::OnInitialize() { std::filesystem::path user_dir; std::string user_data_cvar = REXCVAR_GET(user_data_root); if (!user_data_cvar.empty()) { - user_dir = user_data_cvar; + // to_path: the toml string is UTF-8, not the ANSI codepage a bare path + // construction would assume on Windows (same rule as game_iso/dlc_dir). + user_dir = rex::to_path(user_data_cvar); } else { user_dir = rex::filesystem::GetUserFolder() / GetName(); } @@ -363,7 +365,7 @@ bool ReXApp::OnInitialize() { std::filesystem::path update_dir; std::string update_data_cvar = REXCVAR_GET(update_data_root); if (!update_data_cvar.empty()) { - update_dir = update_data_cvar; + update_dir = rex::to_path(update_data_cvar); } // Allow subclass to override path defaults @@ -399,7 +401,8 @@ bool ReXApp::OnInitialize() { crash_dir = exe_dir / "logs"; } else { std::error_code crash_dir_ec; - crash_dir = std::filesystem::absolute(std::filesystem::path(log_file_cvar), crash_dir_ec) + // to_path: the cvar string is UTF-8 (see user_data_root above). + crash_dir = std::filesystem::absolute(rex::to_path(log_file_cvar), crash_dir_ec) .parent_path(); if (crash_dir.empty()) crash_dir = std::filesystem::current_path(); @@ -421,7 +424,11 @@ bool ReXApp::OnInitialize() { log_level_str, category_levels); if (log_file_cvar.empty()) { log_config.app_name = std::string(GetName()); - log_config.log_dir = (exe_dir / "logs").string(); + // path_to_utf8, not .string(): a non-ASCII exe dir narrowed through the + // ANSI code page here made the log land in a mangled directory - or + // threw during startup - exactly when a user on such a path most needs + // their diagnostics. InitLogging converts back with rex::to_path. + log_config.log_dir = rex::path_to_utf8(exe_dir / "logs"); } rex::InitLogging(log_config); rex::RegisterLogLevelCallback(); diff --git a/thirdparty/rexglue-sdk/thirdparty/CMakeLists.txt b/thirdparty/rexglue-sdk/thirdparty/CMakeLists.txt index a2dbc9ba..e9ba8515 100644 --- a/thirdparty/rexglue-sdk/thirdparty/CMakeLists.txt +++ b/thirdparty/rexglue-sdk/thirdparty/CMakeLists.txt @@ -167,6 +167,14 @@ set(SPDLOG_BUILD_BENCH OFF CACHE BOOL "" FORCE) set(SPDLOG_INSTALL ON CACHE BOOL "" FORCE) add_subdirectory(spdlog) target_compile_options(spdlog PRIVATE -w) +if(WIN32) + # Wide filenames: the log path derives from the exe directory, which is + # user-chosen and can contain any Unicode. Narrow spdlog filenames go + # through the ANSI code page in _fsopen, so a Cyrillic/CJK install path + # produced no log file at all. PUBLIC so every + # consumer's spdlog::filename_t agrees with the compiled lib. + target_compile_definitions(spdlog PUBLIC SPDLOG_WCHAR_FILENAMES) +endif() #============================================================================= # snappy - Fast compression/decompression (GPU traces)