Files
CoopAllTheThings/tests/audio_overrides_test.cpp
BlackMark 6840d9df88 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>
2026-06-24 01:52:01 +02:00

104 lines
3.5 KiB
C++

// Unit test for the per-game audio override store (save/load round-trip, case-insensitive
// lookup by image name, and the differing-overwrite detection that drives the warning).
#include <cstdint>
#include <cstdio>
#include <string>
#include <windows.h>
#include <mmreg.h>
#include "audio/audio_overrides.hpp"
using namespace coop;
namespace
{
int g_failures = 0;
void check(bool ok, const char* what)
{
if (!ok)
{
std::printf("FAIL: %s\n", what);
++g_failures;
}
else
{
std::printf(" ok: %s\n", what);
}
}
std::wstring temp_path()
{
wchar_t dir[MAX_PATH] = {};
GetTempPathW(MAX_PATH, dir);
return std::wstring(dir) + L"coop_overrides_test_" + std::to_wstring(GetCurrentProcessId()) + L".ini";
}
} // namespace
int main()
{
const std::wstring path = temp_path();
DeleteFileW(path.c_str());
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);
store.load(); // missing file -> empty
AudioFormatOverride got;
check(!store.find(L"brotato.exe", got), "empty store: no entry");
bool differed = true;
store.set(L"C:\\games\\Brotato.exe", brotato, &differed); // full path -> basename key
check(!differed, "first set: not a differing overwrite");
store.set(L"snb.exe", snb, &differed);
check(!differed, "new game set: not a differing overwrite");
// Same value again -> not differing.
store.set(L"brotato.exe", brotato, &differed);
check(!differed, "identical re-set: not differing");
// Different value -> differing (drives the warning).
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.
{
AudioOverrideStore store(path);
store.load();
AudioFormatOverride got;
check(store.find(L"BROTATO.EXE", got), "reload: found case-insensitively");
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());
if (g_failures == 0)
{
std::printf("PASS audio_overrides_test\n");
return 0;
}
std::printf("FAILED audio_overrides_test (%d)\n", g_failures);
return 1;
}