diff --git a/README.md b/README.md index 5ede1bc..00091be 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/host/src/audio/audio_overrides.cpp b/host/src/audio/audio_overrides.cpp index 0735409..ba4e240 100644 --- a/host/src/audio/audio_overrides.cpp +++ b/host/src/audio/audio_overrides.cpp @@ -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(c & 0x7F)); + return {}; } + const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); + std::string s(static_cast(n), '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(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(static_cast(c))); + return {}; } + const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); + std::wstring w(static_cast(n), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), w.data(), n); return w; } } // namespace diff --git a/tests/audio_overrides_test.cpp b/tests/audio_overrides_test.cpp index 152e0a0..f35d9b9 100644 --- a/tests/audio_overrides_test.cpp +++ b/tests/audio_overrides_test.cpp @@ -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(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());