Files
libretroadmin d580522058 Patch: the loaded patches, the pnach parser and the loaders in C
The patch store was a std::vector<IniPatch> exported to Counters and
VMManager, each walking it for the places it wanted; a 'bytes'
payload was a std::vector<u8> inside each patch; the dynamic patches
were copies of the game database's vectors; the parser split lines
into std::vector<std::string_view>; the loaders took std::string and
went through std::string buffers, std::optional and FileSystem's
std::vector of results.

Now:
- The patches are a plain array in Patch.cpp, read through
  LoadedPatchCount/LoadedPatch, and applied by ApplyLoadedPatches with
  a mask of places, in load order as before. A payload is a malloc'd
  run with its length, freed by ForgetLoadedPatches, and only
  allocated once the line has passed every check.
- The dynamic patches' entries are copied into one array of their
  own, a pattern run then a replacement run per patch, so a reload of
  the game database cannot leave them dangling.
- The parser splits into five (pointer, length) fields on the stack,
  with the stripping and empty-field rules SplitString had, and the
  command table takes the same.
- LoadPatchesFromZip and LoadPatchesFromDir take C strings. The zip
  member is inflated into a malloc'd buffer. The folder is walked with
  retro_opendir directly, with FindFiles's filter (files, hidden ones
  included, "*.pnach", sizable) and its order, and each file is read
  with filestream_read_file, which terminates it. The "not found"
  message no longer has a newline in the middle of the path.

tests/patch gains a lane that links Patch.cpp against the real
libretro-common directory, file and inflate code: pnach files for the
CRC in either case and with suffixes beside another CRC's, a .txt, a
hidden file, a directory named like a pnach and an empty folder; and
archives written by Python's zipfile with the member deflated, stored
under an upper-case name, only under a subfolder, and behind a
comment, plus every truncation and every one-byte corruption of one.
It fails if the CRC match turns case-sensitive.
2026-11-03 22:29:43 +01:00

268 lines
9.7 KiB
C++

/* Every patch line in the game database, through the real parser.
*
* A pnach line the parser does not understand is rejected with a console
* error and nothing else happens: no failure, no test, and the game
* quietly misbehaves. That is how the "bytes" operand size went
* unimplemented while six WRC 4 serials carried patches written in it --
* the patches were in GameIndex.yaml, the parser refused them, and the
* only sign was a line in a log nobody reads.
*
* So this feeds bin/resources/GameIndex.yaml's patch blocks to
* LoadPatchesFromString and requires that every line is accepted. It
* links pcsx2/Patch.cpp rather than reimplementing the format, so it
* cannot drift from what the emulator actually parses.
*
* What it checks:
* - every "patch=" line loads,
* - the count loaded matches the count in the file, so a line that is
* silently dropped rather than refused is caught too,
* - author= and comment= lines are tolerated, since they share the
* blocks.
*
* What it does not check: whether a patch is correct for its game -- that
* needs the game -- and any operand size the database does not currently
* use. Deleting "ledouble" from the parser leaves this at 854 of 854,
* because nothing in GameIndex.yaml is written in it. The test tracks
* what the database contains, so it catches a type going missing only
* once some entry depends on it, which is exactly when it starts to
* matter. Deleting "bytes" costs 12 lines and "extended" costs 28.
*
* Usage: tests/patch/dbparse <path-to-GameIndex.yaml>
*/
#include <stdio.h>
#include <libretro.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <vector>
#include <string>
#include "Common.h"
#include "Patch.h"
/* Memory the patch application would reach. The parse path does not use
* it, but _ApplyPatch does, and applying is how a bytes payload is
* checked for having survived the parse. */
static u8 s_mem[0x10000];
static u32 clamp(u32 a) { return a & 0xFFFF; }
u8 vtlb_memRead8 (u32 a) { return s_mem[clamp(a)]; }
u16 vtlb_memRead16(u32 a) { u16 v; memcpy(&v, s_mem + clamp(a), 2); return v; }
u32 vtlb_memRead32(u32 a) { u32 v; memcpy(&v, s_mem + clamp(a), 4); return v; }
u64 vtlb_memRead64(u32 a) { u64 v; memcpy(&v, s_mem + clamp(a), 8); return v; }
void vtlb_memWrite8 (u32 a, u8 v) { s_mem[clamp(a)] = v; }
void vtlb_memWrite16(u32 a, u16 v) { memcpy(s_mem + clamp(a), &v, 2); }
void vtlb_memWrite32(u32 a, u32 v) { memcpy(s_mem + clamp(a), &v, 4); }
void vtlb_memWrite64(u32 a, u64 v) { memcpy(s_mem + clamp(a), &v, 8); }
void* vtlb_GetPhyPtr(u32 a) { return s_mem + clamp(a); }
RETURNS_R128 vtlb_memRead128(u32) { r128 v; memset(&v, 0, sizeof(v)); return v; }
void vtlb_memWrite128(u32, r128) { }
extern "C" void iopMemWrite8 (u32 a, u8 v) { s_mem[clamp(a)] = v; }
extern "C" void iopMemWrite16(u32, u16) { }
extern "C" void iopMemWrite32(u32, u32) { }
extern "C" u8 iopMemRead8_slow (u32) { return 0; }
extern "C" u16 iopMemRead16_slow(u32) { return 0; }
extern "C" u32 iopMemRead32_slow(u32) { return 0; }
static uptr s_rlut[0x10000];
const uptr* psxMemRLUT = s_rlut;
/* The loader's other surface; none of it is entered from a string. */
extern "C" int path_is_directory(const char*) { return 0; }
struct RDIR;
extern "C" {
struct RDIR* retro_opendir_include_hidden(const char*, bool) { return 0; }
bool retro_dirent_error(struct RDIR*) { return true; }
int retro_readdir(struct RDIR*) { return 0; }
const char* retro_dirent_get_name(struct RDIR*) { return ""; }
bool retro_dirent_is_dir(struct RDIR*, const char*) { return false; }
void retro_closedir(struct RDIR*) { }
int rl_fnmatch(const char*, const char*, int) { return 1; }
int64_t path_get_size(const char*) { return -1; }
}
extern "C" int64_t filestream_read_file(const char*, void**, int64_t*) { return 0; }
extern "C" {
void* rinflate_new(void) { return 0; }
void rinflate_free(void*) { }
void rinflate_set_in(void*, const void*, unsigned) { }
void rinflate_set_out(void*, void*, unsigned) { }
int rinflate_process(void*, unsigned*, unsigned*) { return -1; }
int rinflate(void*, const void*, unsigned, void*, unsigned, unsigned*)
{ return -1; }
}
namespace Path {
std::string_view GetFileName(const std::string_view& p) { return p; }
}
/* Errors are counted rather than printed one per line: a broken operand
* size fails hundreds at once and the first few say everything. */
static int g_errors = 0;
static int g_shown = 0;
static void test_log(enum retro_log_level level, const char* fmt, ...)
{
if (level != RETRO_LOG_ERROR)
return;
g_errors++;
if (g_shown++ < 5)
{
va_list a;
va_start(a, fmt);
printf(" ");
vprintf(fmt, a); /* the format carries its own newline now */
va_end(a);
}
}
extern "C" retro_log_printf_t log_cb = test_log;
int main(int argc, char** argv)
{
const char* path = (argc > 1) ? argv[1] : "bin/resources/GameIndex.yaml";
FILE* f = fopen(path, "r");
char buf[1024];
std::string block;
int in_content = 0, file_lines = 0, blocks = 0;
if (!f) { fprintf(stderr, "cannot open %s\n", path); return 2; }
/* The patch text sits under "content: |-" as an indented literal
* block. Any line indented further than the key belongs to it; the
* first line that is not ends the block. */
while (fgets(buf, sizeof(buf), f))
{
const char* p = buf;
int indent = 0;
while (*p == ' ') { p++; indent++; }
if (strstr(buf, "content: |-"))
{ in_content = 1; blocks++; continue; }
if (in_content)
{
if (*p == '\n' || *p == '\0') continue;
if (indent < 8) { in_content = 0; }
else
{
if (!strncmp(p, "patch=", 6)) file_lines++;
block += p;
continue;
}
}
}
fclose(f);
{
const int loaded = LoadPatchesFromString(block.c_str());
size_t i, applied = 0;
printf(" %d patch lines in %d blocks, %d loaded, %d refused\n",
file_lines, blocks, loaded, g_errors);
/* Applying them all is what proves a bytes payload survived the
* parse: a line that loaded but kept an empty payload writes
* nothing. */
for (i = 0; i < LoadedPatchCount(); i++)
{
IniPatch* ip = LoadedPatch(i);
_ApplyPatch(ip);
if (ip->type == 9 /* BYTES_T */ && (!ip->bytes || !ip->nbytes))
{
printf(" a bytes patch at %08x parsed with no payload\n",
ip->addr);
g_errors++;
}
applied++;
}
if (loaded != file_lines)
{
printf(" %d lines in the file but %d loaded: some were dropped"
" without an error\n", file_lines, loaded);
g_errors++;
}
printf("dbparse: %d/%d game database patch lines accepted\n",
loaded, file_lines);
(void)applied;
}
/* Lines the database does not have but a pnach can: spacing around
* the '=' and the commas, a trailing comment, a line with no '=', a
* place out of range, a bytes payload that is not hex or not whole
* bytes, and numbers past their field. Each says whether it loads and,
* when it does, what it loads as. */
{
static const struct
{
const char* line;
int loads;
int place;
u32 addr;
u64 data;
const char* bytes; /* hex of the payload, for a bytes line */
} cases[] = {
{"patch=1,EE,00100000,word,0000002A", 1, 1, 0x00100000u, 0x2Au, nullptr},
{" patch = 0 , EE , 2010abcd , word , DEADBEEF // note", 1, 0, 0x2010abcdu, 0xDEADBEEFu, nullptr},
{"patch=2,IOP,0001ffff,byte,ff", 1, 2, 0x0001ffffu, 0xffu, nullptr},
{"patch=1,EE,00100000,double,123456789abcdef0", 1, 1, 0x00100000u, 0x123456789abcdef0ull, nullptr},
{"patch=1,EE,00100000,bytes,00a1FF7e", 1, 1, 0x00100000u, 0, "00a1ff7e"},
{"patch 1,EE,00100000,word,1", 0, 0, 0, 0, nullptr},
{"// patch=1,EE,00100000,word,1", 0, 0, 0, 0, nullptr},
{"patch=", 0, 0, 0, 0, nullptr},
{"patch=3,EE,00100000,word,1", 0, 0, 0, 0, nullptr},
{"patch=x,EE,00100000,word,1", 0, 0, 0, 0, nullptr},
{"patch=-1,EE,00100000,word,1", 0, 0, 0, 0, nullptr},
{"patch=1,EE,00100000,bytes,0g", 0, 0, 0, 0, nullptr},
{"patch=1,EE,00100000,bytes,abc", 0, 0, 0, 0, nullptr},
{"patch=1,ARM,00100000,word,1", 0, 0, 0, 0, nullptr},
{"patch=1,EE,00100000,dword,1", 0, 0, 0, 0, nullptr},
{"patch=1,EE,00100000,word", 0, 0, 0, 0, nullptr},
/* An address past 32 bits is no address, so zero, as before. */
{"patch=1,EE,100000000,word,1", 1, 1, 0, 1, nullptr},
/* A value past 64 bits likewise. */
{"patch=1,EE,00100000,word,10000000000000000", 1, 1, 0x00100000u, 0, nullptr},
};
const int db_errors = g_errors;
int case_fails = 0;
g_shown = 5; /* the refusals below are expected; do not print them */
size_t c;
for (c = 0; c < sizeof(cases) / sizeof(cases[0]); c++)
{
int loaded;
bool ok;
ForgetLoadedPatches();
loaded = LoadPatchesFromString(cases[c].line);
ok = (loaded == cases[c].loads) && (LoadedPatchCount() == (size_t)cases[c].loads);
if (ok && cases[c].loads)
{
const IniPatch& ip = *LoadedPatch(0);
ok = ip.placetopatch == cases[c].place && ip.addr == cases[c].addr
&& ip.data == cases[c].data;
if (ok && cases[c].bytes)
{
char hex[64] = "";
size_t b;
for (b = 0; b < ip.nbytes && b * 2 + 2 < sizeof(hex); b++)
snprintf(hex + b * 2, 3, "%02x", ip.bytes[b]);
ok = strcmp(hex, cases[c].bytes) == 0;
}
}
if (!ok)
{
printf(" FAIL: \"%s\" loaded %d (want %d)", cases[c].line, loaded, cases[c].loads);
if (LoadedPatchCount())
printf(": place %d addr %08x data %llx", LoadedPatch(0)->placetopatch,
LoadedPatch(0)->addr, (unsigned long long)LoadedPatch(0)->data);
printf("\n");
case_fails++;
}
}
ForgetLoadedPatches();
g_errors = db_errors + case_fails;
printf("dbparse: %d of %d pnach edge cases as expected\n",
(int)(sizeof(cases) / sizeof(cases[0])) - case_fails,
(int)(sizeof(cases) / sizeof(cases[0])));
}
return (g_errors != 0 || file_lines == 0);
}