Add misc unit tests + harden the WAV chunk walk
Fills small coverage gaps: - shared_memory_test: SharedMemory create-or-open aliasing, move (steal + empty the source, no double-free), reset, open-missing. - wav_test: malformed input -- truncation, bad magic, missing data chunk, over-long data size (clamps), odd-sized chunk (word-align skip), and a corrupt ~4 GB chunk_size. The reader gains an advance guard so that last case can't wrap pos on a 32-bit size_t (x86) or spin the walk; it stops cleanly. - tool_paths_test: deployed_artifact_path resolution -- next-to-exe, one-dir-up, and the not-found fallback -- with real marker files. - audio_ring_test: an overrun-at-the-seam case (write head near the end: a wrapping push that fits vs. an over-capacity wrapping push dropped whole), exercising the wrap split + overrun together, not just at offset 0. The injector bitness check isn't added as a unit test: is_wow64_process is file-local and the real WOW64 path needs a 32-bit target, so it stays inspection-covered (and exercised by the x86 injection path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,8 +115,6 @@ as its own commit; "verify" items are confirmed real before any change, and drop
|
|||||||
|
|
||||||
Test coverage:
|
Test coverage:
|
||||||
- **Dedicated hook tests** — `focus_spoof`, `vk_hook` (present), `d3d9_hook`.
|
- **Dedicated hook tests** — `focus_spoof`, `vk_hook` (present), `d3d9_hook`.
|
||||||
- **Misc units** — `SharedMemory` RAII/move, `wav` malformed input, `tool_paths` resolution, injector
|
|
||||||
bitness check; strengthen the `audio_ring` overrun-at-seam case.
|
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- **Static CRT (`/MT`) for `coop_hook.dll`** (x64 + x86) so it loads in games without the VC++ redist.
|
- **Static CRT (`/MT`) for `coop_hook.dll`** (x64 + x86) so it loads in games without the VC++ redist.
|
||||||
|
|||||||
@@ -132,7 +132,15 @@ inline bool wav_read(const std::wstring& path, WavData& out)
|
|||||||
out.pcm.assign(all.begin() + body, all.begin() + body + n);
|
out.pcm.assign(all.begin() + body, all.begin() + body + n);
|
||||||
have_data = true;
|
have_data = true;
|
||||||
}
|
}
|
||||||
pos = body + chunk_size + (chunk_size & 1); // chunks are word-aligned
|
// Advance to the next chunk (word-aligned). Guard a corrupt over-long chunk_size: it would
|
||||||
|
// wrap `pos` on a 32-bit size_t (x86) and spin the loop on garbage, and there's nothing valid
|
||||||
|
// past a chunk that claims more than the file holds anyway.
|
||||||
|
const std::size_t advance = static_cast<std::size_t>(chunk_size) + (chunk_size & 1);
|
||||||
|
if (advance > all.size() - body)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos = body + advance;
|
||||||
}
|
}
|
||||||
return have_fmt && have_data;
|
return have_fmt && have_data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ target_include_directories(protocol_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
|||||||
target_link_libraries(protocol_test PRIVATE coop_common)
|
target_link_libraries(protocol_test PRIVATE coop_common)
|
||||||
add_test(NAME protocol_test COMMAND protocol_test)
|
add_test(NAME protocol_test COMMAND protocol_test)
|
||||||
|
|
||||||
|
# Unit test for the SharedMemory RAII wrapper (create-or-open aliasing, move, reset, open-missing).
|
||||||
|
add_executable(shared_memory_test shared_memory_test.cpp)
|
||||||
|
target_link_libraries(shared_memory_test PRIVATE coop_common)
|
||||||
|
add_test(NAME shared_memory_test COMMAND shared_memory_test)
|
||||||
|
|
||||||
|
# Unit test for the WAV reader's robustness on malformed input (truncation, bad magic, missing/
|
||||||
|
# over-long/odd/corrupt chunks). Header-only.
|
||||||
|
add_executable(wav_test wav_test.cpp)
|
||||||
|
target_link_libraries(wav_test PRIVATE coop_common)
|
||||||
|
add_test(NAME wav_test COMMAND wav_test)
|
||||||
|
|
||||||
|
# Unit test for deployed-artifact path resolution (next-to-exe vs one-dir-up vs not-found fallback).
|
||||||
|
add_executable(tool_paths_test tool_paths_test.cpp)
|
||||||
|
target_link_libraries(tool_paths_test PRIVATE coop_common)
|
||||||
|
add_test(NAME tool_paths_test COMMAND tool_paths_test)
|
||||||
|
|
||||||
# Unit test for the audio mixer math (decode/sum/soft-clip/encode). Header-only.
|
# Unit test for the audio mixer math (decode/sum/soft-clip/encode). Header-only.
|
||||||
add_executable(audio_mix_test audio_mix_test.cpp)
|
add_executable(audio_mix_test audio_mix_test.cpp)
|
||||||
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
||||||
@@ -336,6 +352,9 @@ coop_output_subdir(tests
|
|||||||
mkb_ring_test
|
mkb_ring_test
|
||||||
log_ring_test
|
log_ring_test
|
||||||
protocol_test
|
protocol_test
|
||||||
|
shared_memory_test
|
||||||
|
wav_test
|
||||||
|
tool_paths_test
|
||||||
mkb_map_test
|
mkb_map_test
|
||||||
audio_mix_test
|
audio_mix_test
|
||||||
tone_analysis_test
|
tone_analysis_test
|
||||||
|
|||||||
@@ -126,6 +126,47 @@ int main()
|
|||||||
check(h->overruns.load() == 1, "overruns not bumped on success");
|
check(h->overruns.load() == 1, "overruns not bumped on success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Seam interaction: with the write head near the end, a wrapping push fits while an
|
||||||
|
// over-capacity wrapping push is dropped whole (the overrun + wrap split happening together). ---
|
||||||
|
{
|
||||||
|
const std::uint32_t cap = 512;
|
||||||
|
std::vector<std::uint8_t> storage;
|
||||||
|
AudioRingHeader* h = make_ring(storage, cap);
|
||||||
|
|
||||||
|
// Advance the write head near the end so subsequent writes wrap the buffer seam.
|
||||||
|
std::vector<std::uint8_t> pre(400, 0x55);
|
||||||
|
check(audio_ring_push(*h, pre.data(), 400, 1), "seam: prime to advance the write head");
|
||||||
|
std::vector<std::uint8_t> sink(400, 0);
|
||||||
|
check(audio_ring_pop(*h, sink.data(), 400) == 400, "seam: drain it (head now near the end)");
|
||||||
|
check(audio_ring_available(*h) == 0, "seam: empty again, write head near the seam");
|
||||||
|
|
||||||
|
// A 200-byte push now splits across the seam; it fits, so the data must survive the split.
|
||||||
|
std::vector<std::uint8_t> wrap(200);
|
||||||
|
for (std::uint32_t i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
wrap[i] = static_cast<std::uint8_t>(i);
|
||||||
|
}
|
||||||
|
check(audio_ring_push(*h, wrap.data(), 200, 1), "seam: a wrapping push that fits is accepted");
|
||||||
|
check(audio_ring_available(*h) == 200, "seam: 200 bytes present after the wrapping push");
|
||||||
|
|
||||||
|
// A 400-byte push would also wrap but can't fit (free = 312) -> dropped whole, overruns++.
|
||||||
|
const std::uint64_t before = h->overruns.load();
|
||||||
|
std::vector<std::uint8_t> big(400, 0xEE);
|
||||||
|
check(!audio_ring_push(*h, big.data(), 400, 1), "seam: an over-capacity wrapping push is rejected whole");
|
||||||
|
check(h->overruns.load() == before + 1, "seam: overrun counted");
|
||||||
|
check(audio_ring_available(*h) == 200, "seam: rejected wrapping push left the ring untouched");
|
||||||
|
|
||||||
|
// Pop the wrapped payload back and verify integrity across the seam.
|
||||||
|
std::vector<std::uint8_t> out(200, 0);
|
||||||
|
check(audio_ring_pop(*h, out.data(), 200) == 200, "seam: pop the wrapped payload");
|
||||||
|
bool ok = true;
|
||||||
|
for (std::uint32_t i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
ok = ok && out[i] == static_cast<std::uint8_t>(i);
|
||||||
|
}
|
||||||
|
check(ok, "seam: wrapping push/pop preserved data across the buffer seam");
|
||||||
|
}
|
||||||
|
|
||||||
std::printf(g_failures == 0 ? "AUDIO RING TEST PASS\n" : "AUDIO RING TEST FAILED (%d)\n", g_failures);
|
std::printf(g_failures == 0 ? "AUDIO RING TEST PASS\n" : "AUDIO RING TEST FAILED (%d)\n", g_failures);
|
||||||
return g_failures == 0 ? 0 : 1;
|
return g_failures == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
80
tests/shared_memory_test.cpp
Normal file
80
tests/shared_memory_test.cpp
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Unit test for the SharedMemory RAII wrapper (common/include/coop/shared_memory.hpp): create-or-open
|
||||||
|
// aliasing, move (steal handles + leave the source empty, no double-free), reset, and open-missing.
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "coop/shared_memory.hpp"
|
||||||
|
|
||||||
|
using namespace coop;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int g_failures = 0;
|
||||||
|
void check(bool ok, const char* what)
|
||||||
|
{
|
||||||
|
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring uniq_name()
|
||||||
|
{
|
||||||
|
return L"Local\\coop_shmtest_" + std::to_wstring(GetCurrentProcessId());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const std::wstring name = uniq_name();
|
||||||
|
constexpr std::size_t kSize = 4096;
|
||||||
|
|
||||||
|
check(!SharedMemory{}.valid(), "default-constructed: not valid");
|
||||||
|
|
||||||
|
{
|
||||||
|
SharedMemory a;
|
||||||
|
check(a.create(name, kSize) && a.valid(), "create a named section");
|
||||||
|
a.as<std::uint32_t>()[0] = 0xC0FFEEu;
|
||||||
|
|
||||||
|
// open() the same name -> a second view of the SAME section (aliases a's memory).
|
||||||
|
SharedMemory b;
|
||||||
|
check(b.open(name, kSize) && b.valid(), "open the existing section");
|
||||||
|
check(b.as<std::uint32_t>()[0] == 0xC0FFEEu, "open aliases the same memory (sees the write)");
|
||||||
|
b.as<std::uint32_t>()[1] = 0x1234u;
|
||||||
|
check(a.as<std::uint32_t>()[1] == 0x1234u, "writes through one view are visible in the other");
|
||||||
|
|
||||||
|
// create() again with the same name -> opens the existing section (create-or-open).
|
||||||
|
SharedMemory c;
|
||||||
|
check(c.create(name, kSize) && c.valid() && c.as<std::uint32_t>()[0] == 0xC0FFEEu,
|
||||||
|
"create-or-open: a second create sees the existing data");
|
||||||
|
|
||||||
|
// Move: the destination owns the mapping, the source is emptied (no double-free at scope end).
|
||||||
|
const void* a_ptr = a.data();
|
||||||
|
SharedMemory moved = std::move(a);
|
||||||
|
check(moved.valid() && moved.data() == a_ptr, "move-construct steals the view");
|
||||||
|
check(!a.valid() && a.data() == nullptr, "moved-from source is emptied");
|
||||||
|
check(moved.as<std::uint32_t>()[0] == 0xC0FFEEu, "moved view still maps the section");
|
||||||
|
|
||||||
|
SharedMemory move_assigned;
|
||||||
|
move_assigned.create(uniq_name() + L"_x", kSize); // give it something to reset first
|
||||||
|
move_assigned = std::move(moved);
|
||||||
|
check(move_assigned.valid() && !moved.valid(), "move-assign steals and empties the source");
|
||||||
|
check(move_assigned.as<std::uint32_t>()[1] == 0x1234u, "move-assigned view sees prior writes");
|
||||||
|
|
||||||
|
move_assigned.reset();
|
||||||
|
check(!move_assigned.valid() && move_assigned.data() == nullptr, "reset releases the mapping");
|
||||||
|
}
|
||||||
|
|
||||||
|
// open() a name that no longer exists (all handles closed at scope end above) -> false.
|
||||||
|
{
|
||||||
|
SharedMemory gone;
|
||||||
|
check(!gone.open(name, kSize), "open a non-existent section returns false");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(g_failures == 0 ? "PASS shared_memory_test\n" : "FAILED shared_memory_test (%d)\n", g_failures);
|
||||||
|
return g_failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
84
tests/tool_paths_test.cpp
Normal file
84
tests/tool_paths_test.cpp
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Unit test for the deployed-artifact path resolution (common/include/coop/tool_paths.hpp): a probe
|
||||||
|
// staged under bin/<config>/tools/ must still find coop_hook.dll etc. at the deployable root one level
|
||||||
|
// up. Drives the real probe logic by dropping marker files next to the exe and one directory up.
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "coop/tool_paths.hpp"
|
||||||
|
|
||||||
|
using namespace coop;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int g_failures = 0;
|
||||||
|
void check(bool ok, const char* what)
|
||||||
|
{
|
||||||
|
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch(const std::wstring& path)
|
||||||
|
{
|
||||||
|
HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (h == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CloseHandle(h);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring parent_of(std::wstring dir) // dir has a trailing separator
|
||||||
|
{
|
||||||
|
if (!dir.empty())
|
||||||
|
{
|
||||||
|
dir.pop_back();
|
||||||
|
}
|
||||||
|
const std::size_t slash = dir.find_last_of(L"\\/");
|
||||||
|
return slash == std::wstring::npos ? std::wstring() : dir.substr(0, slash + 1);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const std::wstring here = exe_directory();
|
||||||
|
check(!here.empty() && (here.back() == L'\\' || here.back() == L'/'), "exe_directory ends with a separator");
|
||||||
|
|
||||||
|
const std::wstring tag = std::to_wstring(GetCurrentProcessId());
|
||||||
|
const std::wstring near_name = L"coop_tp_near_" + tag + L".marker";
|
||||||
|
const std::wstring up_name = L"coop_tp_up_" + tag + L".marker";
|
||||||
|
const std::wstring missing_name = L"coop_tp_missing_" + tag + L".marker";
|
||||||
|
|
||||||
|
const std::wstring up_dir = parent_of(here);
|
||||||
|
const bool have_up = !up_dir.empty();
|
||||||
|
|
||||||
|
// next-to-exe wins.
|
||||||
|
check(touch(here + near_name), "created a marker next to the exe");
|
||||||
|
check(deployed_artifact_path(near_name.c_str()) == here + near_name, "resolves an artifact next to the exe");
|
||||||
|
|
||||||
|
// one directory up (the deployable root) when it isn't next to the exe.
|
||||||
|
if (have_up)
|
||||||
|
{
|
||||||
|
check(touch(up_dir + up_name), "created a marker one directory up");
|
||||||
|
check(deployed_artifact_path(up_name.c_str()) == up_dir + up_name,
|
||||||
|
"falls back to the artifact one directory up");
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing -> the next-to-exe path (so the caller can report a sensible 'not found').
|
||||||
|
check(deployed_artifact_path(missing_name.c_str()) == here + missing_name,
|
||||||
|
"missing artifact -> next-to-exe fallback path");
|
||||||
|
|
||||||
|
DeleteFileW((here + near_name).c_str());
|
||||||
|
if (have_up)
|
||||||
|
{
|
||||||
|
DeleteFileW((up_dir + up_name).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(g_failures == 0 ? "PASS tool_paths_test\n" : "FAILED tool_paths_test (%d)\n", g_failures);
|
||||||
|
return g_failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
160
tests/wav_test.cpp
Normal file
160
tests/wav_test.cpp
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Unit test for the WAV reader's robustness on malformed input (common/include/coop/wav.hpp).
|
||||||
|
// tone_analysis_test already round-trips the writer's own output; this feeds hand-built byte streams:
|
||||||
|
// truncated headers, bad magic, missing chunks, an over-long `data` size (must clamp), an odd-sized
|
||||||
|
// chunk before data (word-align skip), and a corrupt huge chunk_size (must not hang/overflow).
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "coop/wav.hpp"
|
||||||
|
|
||||||
|
using namespace coop;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int g_failures = 0;
|
||||||
|
int g_counter = 0;
|
||||||
|
void check(bool ok, const char* what)
|
||||||
|
{
|
||||||
|
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring write_temp(const std::vector<std::uint8_t>& bytes)
|
||||||
|
{
|
||||||
|
wchar_t dir[MAX_PATH] = {};
|
||||||
|
GetTempPathW(MAX_PATH, dir);
|
||||||
|
std::wstring path = std::wstring(dir) + L"coop_wavtest_" + std::to_wstring(GetCurrentProcessId()) + L"_" +
|
||||||
|
std::to_wstring(g_counter++) + L".wav";
|
||||||
|
FILE* f = nullptr;
|
||||||
|
if (_wfopen_s(&f, path.c_str(), L"wb") == 0 && f != nullptr)
|
||||||
|
{
|
||||||
|
if (!bytes.empty())
|
||||||
|
{
|
||||||
|
std::fwrite(bytes.data(), 1, bytes.size(), f);
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void put4(std::vector<std::uint8_t>& b, const char* s)
|
||||||
|
{
|
||||||
|
b.insert(b.end(), s, s + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a WAV where the `data` chunk's declared size can differ from the bytes actually appended
|
||||||
|
// (declared_data_size < 0 means "use the real size"), and an optional junk chunk can be inserted
|
||||||
|
// before `data` with a chosen size field (to exercise the chunk walk).
|
||||||
|
std::vector<std::uint8_t> build_wav(std::uint16_t tag, std::uint16_t channels, std::uint32_t rate,
|
||||||
|
std::uint16_t bits, const std::vector<std::uint8_t>& data,
|
||||||
|
long long declared_data_size = -1, const char* junk_id = nullptr,
|
||||||
|
std::uint32_t junk_size = 0)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> b;
|
||||||
|
put4(b, "RIFF");
|
||||||
|
detail::wav_put_u32(b, 0); // riff size (reader ignores it)
|
||||||
|
put4(b, "WAVE");
|
||||||
|
put4(b, "fmt ");
|
||||||
|
detail::wav_put_u32(b, 16);
|
||||||
|
detail::wav_put_u16(b, tag);
|
||||||
|
detail::wav_put_u16(b, channels);
|
||||||
|
detail::wav_put_u32(b, rate);
|
||||||
|
detail::wav_put_u32(b, rate * channels * (bits / 8));
|
||||||
|
detail::wav_put_u16(b, static_cast<std::uint16_t>(channels * (bits / 8)));
|
||||||
|
detail::wav_put_u16(b, bits);
|
||||||
|
if (junk_id != nullptr)
|
||||||
|
{
|
||||||
|
put4(b, junk_id);
|
||||||
|
detail::wav_put_u32(b, junk_size);
|
||||||
|
b.insert(b.end(), junk_size, 0xAB); // junk body
|
||||||
|
if (junk_size & 1)
|
||||||
|
{
|
||||||
|
b.push_back(0); // RIFF pads odd chunks to an even boundary (what the reader's & 1 skips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put4(b, "data");
|
||||||
|
detail::wav_put_u32(b, declared_data_size < 0 ? static_cast<std::uint32_t>(data.size())
|
||||||
|
: static_cast<std::uint32_t>(declared_data_size));
|
||||||
|
b.insert(b.end(), data.begin(), data.end());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
WavData out;
|
||||||
|
|
||||||
|
// Truncated (< 44 bytes).
|
||||||
|
check(!wav_read(write_temp({'R', 'I', 'F', 'F', 0, 0, 0, 0}), out), "truncated file (<44B) rejected");
|
||||||
|
|
||||||
|
// Right size but wrong magic.
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> b(64, 0);
|
||||||
|
b[0] = 'R'; // not "RIFF"...."WAVE"
|
||||||
|
check(!wav_read(write_temp(b), out), "bad RIFF/WAVE magic rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid 16-bit PCM round-trips.
|
||||||
|
{
|
||||||
|
const std::vector<std::uint8_t> data(800, 0x42);
|
||||||
|
out = WavData{};
|
||||||
|
check(wav_read(write_temp(build_wav(1, 2, 44100, 16, data)), out), "valid PCM parses");
|
||||||
|
check(out.format_tag == 1 && out.channels == 2 && out.sample_rate == 44100 && out.bits == 16,
|
||||||
|
"valid PCM: format fields correct");
|
||||||
|
check(out.pcm.size() == 800, "valid PCM: full data recovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt present but no data chunk -> rejected (have_data == false).
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> b;
|
||||||
|
put4(b, "RIFF");
|
||||||
|
detail::wav_put_u32(b, 0);
|
||||||
|
put4(b, "WAVE");
|
||||||
|
put4(b, "fmt ");
|
||||||
|
detail::wav_put_u32(b, 16);
|
||||||
|
detail::wav_put_u16(b, 1);
|
||||||
|
detail::wav_put_u16(b, 2);
|
||||||
|
detail::wav_put_u32(b, 48000);
|
||||||
|
detail::wav_put_u32(b, 48000 * 4);
|
||||||
|
detail::wav_put_u16(b, 4);
|
||||||
|
detail::wav_put_u16(b, 16);
|
||||||
|
check(!wav_read(write_temp(b), out), "fmt-only (no data chunk) rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// data chunk declares MORE than the file holds -> clamp to what's there, still parse.
|
||||||
|
{
|
||||||
|
const std::vector<std::uint8_t> data(100, 0x7F);
|
||||||
|
out = WavData{};
|
||||||
|
check(wav_read(write_temp(build_wav(3, 1, 48000, 32, data, /*declared=*/1000000)), out),
|
||||||
|
"over-long data size still parses");
|
||||||
|
check(out.pcm.size() == 100, "over-long data size clamps to available bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// An odd-sized junk chunk before data -> the word-align skip must still find data.
|
||||||
|
{
|
||||||
|
const std::vector<std::uint8_t> data(40, 0x11);
|
||||||
|
out = WavData{};
|
||||||
|
check(wav_read(write_temp(build_wav(1, 2, 44100, 16, data, -1, "LIST", /*odd*/ 3)), out),
|
||||||
|
"odd-sized chunk before data: word-align skip finds data");
|
||||||
|
check(out.pcm.size() == 40, "data after an odd chunk recovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A corrupt, huge chunk_size before data must not hang or overflow -- the walk stops.
|
||||||
|
{
|
||||||
|
const std::vector<std::uint8_t> data(40, 0x22);
|
||||||
|
out = WavData{};
|
||||||
|
// junk chunk claims ~4 GB; the guard breaks before reaching the real data chunk -> no data found.
|
||||||
|
const bool parsed = wav_read(write_temp(build_wav(1, 2, 44100, 16, data, -1, "junk", 0xFFFFFFF0u)), out);
|
||||||
|
check(!parsed, "corrupt huge chunk_size: walk stops cleanly (no hang/overflow), data not reached");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(g_failures == 0 ? "PASS wav_test\n" : "FAILED wav_test (%d)\n", g_failures);
|
||||||
|
return g_failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user