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
This commit is contained in:
Danshet
2026-08-17 21:14:00 +02:00
parent ee5036e213
commit 4d01457417
6 changed files with 109 additions and 25 deletions
+14 -2
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <cctype>
#include <fstream>
#include <mutex>
#include <string>
#include <string_view>
@@ -14,6 +15,7 @@
#include <rex/cvar.h>
#include <rex/crypto/sha256.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
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;
}
+12 -2
View File
@@ -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;
}
+5 -1
View File
@@ -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;
};
+59 -16
View File
@@ -12,7 +12,9 @@
#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <unordered_map>
#include <vector>
@@ -23,6 +25,7 @@
#include <toml++/toml.hpp>
#include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <rex/platform.h>
@@ -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<spdlog::sinks::rotating_file_sink_mt>(
resolved_path, static_cast<size_t>(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<spdlog::sinks::rotating_file_sink_mt>(
sink_filename, static_cast<size_t>(REXCVAR_GET(log_max_file_size_mb)) * 1024 * 1024,
static_cast<size_t>(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<std::string, std::string> 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;
+11 -4
View File
@@ -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();
+8
View File
@@ -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)