Audio overrides: lossless UTF-8 name round-trip (no high-bit collision)

The per-game override store keyed and persisted image names through narrow(), which
masked each character with & 0x7F, and widen() used the full byte -- not a true
inverse. So a non-ASCII exe name was corrupted on reload, and two names differing
only in their high bits collapsed onto the same key (e.g. U+00E9 'é' masked to
'i', so "café.exe" collided with "cafi.exe").

Use real WideCharToMultiByte/MultiByteToWideChar(CP_UTF8) so the round-trip is
lossless for any Unicode name. ASCII names are byte-identical under UTF-8, so
existing override files stay compatible.

audio_overrides_test gains a high-bit-collision case (café vs cafi, built from a
code point to keep the source ASCII) plus a non-ASCII persist/reload check -- both
of which the old 7-bit mask failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 01:52:01 +02:00
parent 5e9b1cde4c
commit 6840d9df88
3 changed files with 29 additions and 9 deletions

View File

@@ -23,25 +23,31 @@ std::wstring to_lower(std::wstring s)
return s;
}
// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and can't
// collide with another name in its high bits. The old `c & 0x7F` mask was lossy and not a true
// inverse of widen; for ASCII names (the common case) UTF-8 is byte-identical, so existing override
// files stay compatible.
std::string narrow(const std::wstring& w)
{
std::string s;
s.reserve(w.size());
for (wchar_t c : w) // image names + our format tokens are ASCII
if (w.empty())
{
s.push_back(static_cast<char>(c & 0x7F));
return {};
}
const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string s(static_cast<std::size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}
std::wstring widen(const std::string& s)
{
std::wstring w;
w.reserve(s.size());
for (char c : s)
if (s.empty())
{
w.push_back(static_cast<wchar_t>(static_cast<unsigned char>(c)));
return {};
}
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
} // namespace