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.
Each byte of a 'bytes' payload was parsed as two characters in base
16, and the parse stops at the first non-hex character, so "0g" was
taken as a byte of 0 and the line loaded, where the error branch for
a payload that is not hex was meant to refuse it. Both characters
are now checked.
tests/patch only fed the parser the game database's own lines, which
are all well formed. It now also loads eighteen pnach lines that are
not: spacing around '=' and the commas, a trailing comment, no '=',
a place out of range, signed or not a number, a payload not hex or
not whole bytes, an unknown CPU or operand size, too few fields, and
an address or value past its field. Each must load or be refused as
before, and what loads must carry the place, address, value and
payload written.
The logging facade in common/ -- IConsoleWriter and IDevConWriter,
Console and DevCon -- formatted every message into a heap std::string
and then handed it to the frontend's log_cb with "%s\n". One thousand
and six call sites call log_cb themselves now, with the level the
facade's method mapped to, read from its source rather than assumed:
Console.WriteLn is INFO and DevCon.WriteLn is DEBUG, but DevCon.Error
and DevCon.Warning were ERROR and WARN, and stay so. The newline the
facade appended is in each format string; the eighteen literals that
already ended in one keep the blank line they produced. The colour
overloads discarded their colour, and the argument is gone with them.
log_cb is one symbol with C linkage, declared in Pcsx2Defs.h for the
C++ translation units and in ATA.h for the C ones, which called the
facade through a C shim that appended nothing -- and whose callers,
despite its comment, put no newline in either; they have one now. It
was also called unguarded by every log line, and a frontend that offers
no log interface left it null, a crash at the first message: main.cpp
installs a stderr fallback in that case.
Ninety-four files had Pcsx2Defs.h only through Console.h and include it
directly, by the tree's rooted spelling. The patch harnesses, which
counted errors and warnings through the facade's methods, count them
through a log_cb that dispatches on level, and the settings harness
provides one.
Full default build links; all thirteen suites pass; every touched C++
file is clean for Windows, macOS and iOS shapes with
-Winfinite-recursion -- the iOS shape caught a colour variable the arm64
recompiler left behind, which no Linux build compiles -- and every
touched C file under the C89 lane's flags; the added includes sit at
depth zero; no file references anything removed.
Combine, IsAbsolute, ReplaceExtension, GetDirectory and GetFileName leave
common/. Their seventeen sites call libretro-common's file_path
directly: fill_pathname_join into a PATH_MAX_LENGTH buffer where Combine
built a std::string, path_is_absolute, fill_pathname with the dot in the
replacement where ReplaceExtension kept the path's own, fill_pathname_
basedir, and path_basename. Two things were kept exact rather than
approximated: a BIOS path with no extension is returned unchanged, as
ReplaceExtension did, where fill_pathname would append; and the
directory GetDirectory produced only ever fed a directory listing, so
fill_pathname_basedir's trailing separator changes nothing.
SplitWindowsPath had one user, the ISO9660 filesystem, and is a static
function in IsoFS.cpp now; it is that filesystem's parsing, not a path
utility. Canonicalize and SplitNativePath had none.
The EmuFolders paths are char arrays, not strings, and so is
ThreadedFileReader's filename: the first conversion put .c_str() on
them, and the build said so. PATH_MAX_LENGTH is retro_miscellaneous.h's
and file_path.h does not bring it, so each file that gained a buffer
includes it. The patch harness that links Patch.cpp gains file_path.c
and what it pulls in.
Full default build links; all twelve suites pass; every touched file is
clean for Windows, macOS and iOS shapes; every added include sits at
depth zero; and no file of any extension references anything removed.
The HostFS migration removed common/FileSystem.h; dbparse stubbed the
old namespace against that header and has not compiled since. Point
the stubs at HostFS.h, which is where the surviving FindFiles surface
lives, and stub filestream_read_file for the pnach path the test never
enters.
The "bytes" fix landed because someone reported WRC 4 as broken. Nothing
in the tree would have found it: a pnach line the parser does not
understand is rejected with a console error, and then nothing happens --
no failure, no test, and the game quietly misbehaves. Six serials carried
patches in an operand size this parser did not implement, and the only
sign was a line in a log.
So this feeds GameIndex.yaml's 854 patch lines to LoadPatchesFromString
and requires every one to be accepted. It links pcsx2/Patch.cpp rather
than reimplementing the format, so it cannot drift from what the emulator
parses, and it applies what it loaded, which is how a line that parses
into an empty payload is caught as well as one that is refused.
It would have found the original bug: deleting "bytes" from the parser
drops it to 842 of 854, exactly the twelve WRC 4 lines. Deleting
"extended" drops it to 826.
Its limit is worth stating and is in the header. Deleting "ledouble"
leaves it at 854, because nothing in the database is written in that size
-- the test tracks what the database contains rather than the whole pnach
format, so it catches a type going missing only once some entry depends
on it. That is the point at which it starts to matter, but it does mean a
green run is not a claim about the format.
Twelve suites now. Clean under ASan and UBSan.