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:
@@ -115,7 +115,6 @@ as its own commit; "verify" items are confirmed real before any change, and drop
|
||||
|
||||
Cross-process / ABI:
|
||||
- **`log_ring` torn-text window** — verify the MPSC overwrite race; fix or bound it.
|
||||
- **`narrow`/`widen` not inverse** — make the audio-override name round-trip lossless (or document).
|
||||
|
||||
Test coverage:
|
||||
- **`log_ring`** — threaded push/drain + wrap-skip generation test (currently zero coverage).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,6 +43,12 @@ int main()
|
||||
|
||||
const AudioFormatOverride brotato{44100, 2, 32, WAVE_FORMAT_IEEE_FLOAT};
|
||||
const AudioFormatOverride snb{48000, 2, 16, WAVE_FORMAT_PCM};
|
||||
const AudioFormatOverride cafe_ov{96000, 2, 32, WAVE_FORMAT_IEEE_FLOAT};
|
||||
// "cafe.exe" with the e replaced by U+00E9 ('e' with acute). Under the old 7-bit narrow,
|
||||
// 0x00E9 & 0x7F == 0x69 == 'i', so this collapsed onto "cafi.exe" -- a silent key collision.
|
||||
// Built from a code point so the source stays pure ASCII (no literal/escape encoding hazards).
|
||||
const wchar_t cafe_buf[] = {L'c', L'a', L'f', static_cast<wchar_t>(0x00E9), L'.', L'e', L'x', L'e', L'\0'};
|
||||
const wchar_t* kCafe = cafe_buf;
|
||||
|
||||
{
|
||||
AudioOverrideStore store(path);
|
||||
@@ -64,6 +70,13 @@ int main()
|
||||
const AudioFormatOverride brotato2{48000, 2, 32, WAVE_FORMAT_IEEE_FLOAT};
|
||||
store.set(L"brotato.exe", brotato2, &differed);
|
||||
check(differed, "changed value: flagged as differing overwrite");
|
||||
|
||||
// Non-ASCII names must keep their own entry (no high-bit collision with the ASCII look-alike).
|
||||
store.set(kCafe, cafe_ov, &differed);
|
||||
store.set(L"cafi.exe", snb, &differed);
|
||||
AudioFormatOverride na;
|
||||
check(store.find(kCafe, na) && na == cafe_ov, "non-ASCII name keeps its own override");
|
||||
check(store.find(L"cafi.exe", na) && na == snb, "the ASCII look-alike keeps a separate override");
|
||||
}
|
||||
|
||||
// Reload from disk in a fresh store: persistence + case-insensitive basename lookup.
|
||||
@@ -75,6 +88,8 @@ int main()
|
||||
check(got == AudioFormatOverride{48000, 2, 32, WAVE_FORMAT_IEEE_FLOAT}, "reload: latest value persisted");
|
||||
check(store.find(L"D:\\steam\\snb.exe", got) && got == snb, "reload: found by full path basename");
|
||||
check(!store.find(L"unknown.exe", got), "reload: unknown game absent");
|
||||
// The non-ASCII name must survive the UTF-8 persist/reload round-trip.
|
||||
check(store.find(kCafe, got) && got == cafe_ov, "reload: non-ASCII name round-trips losslessly");
|
||||
}
|
||||
|
||||
DeleteFileW(path.c_str());
|
||||
|
||||
Reference in New Issue
Block a user