mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
fix(config): validate overlay_capture_persist_dir independent of host path grammar
std::filesystem answers by the *host* path grammar, so this guard meant different things on different hosts. Under POSIX, "C:/outside" is a relative path whose first component is literally "C:", a backslash is an ordinary filename character, and "\\server\share" is a single relative component -- so Windows-authored absolute paths and backslash ".." traversals were accepted. Under Windows, a rooted "/outside" has no root name, so operator/ drops the project root's relative path and the result lands at the drive root; and a drive-relative "C:foo" is not absolute at all, yet replaces the whole path when the project sits on another drive. Scan the raw config bytes under both separator conventions instead, and reject an embedded NUL. TOML forbids a raw NUL byte but allows the \u0000 escape that produces one, and the runtime hands this value on as a C string (runtime/src/main.cpp:5236-5241 -> overlay_capture.c:230), so "..\u0000keep" validated as a four-byte component and then truncated to ".." at the point of use, escaping the project. Validating what the consumer actually sees closes that gap. recompiler/src/config_loader.h carries the contract next to the field and still described the old, narrower rule; docs/AOT_OVERLAY_PLAN.md carries the only prose statement of it. Both now match what is enforced. Note the doc claims identical accept/reject across hosts, not identical meaning: a value like "nested\dir" is accepted everywhere but still names different things. The registered recompiler_patch_test assertion "parser rejects absolute capture history directory" failed on POSIX before this; the suite is now 33/33 on macOS arm64.
This commit is contained in:
@@ -466,6 +466,60 @@ overlay_capture_persist_dir = "C:/outside"
|
||||
check_throws([&] { (void)PSXRecompV4::load_game_config(absolute); },
|
||||
"must be project-relative",
|
||||
"parser rejects absolute capture history directory");
|
||||
|
||||
// A game.toml travels between hosts, so each of these must be rejected on
|
||||
// every host, not only on the one whose path grammar it is written in.
|
||||
// TOML literal strings ('...') are used because a backslash is an escape
|
||||
// inside a TOML basic string. Cases marked POSIX-visible are the ones a
|
||||
// host-native std::filesystem check silently accepted under POSIX.
|
||||
struct RejectedDir {
|
||||
const char* value; // as written in game.toml
|
||||
const char* needle; // expected message fragment
|
||||
const char* name;
|
||||
};
|
||||
static constexpr RejectedDir rejected[] = {
|
||||
{"'/outside'", "must be project-relative",
|
||||
"parser rejects rooted capture history directory"},
|
||||
{R"('\outside')", "must be project-relative",
|
||||
"parser rejects backslash-rooted capture history directory"},
|
||||
{R"('C:\outside')", "must be project-relative",
|
||||
"parser rejects backslash drive-qualified capture history directory"},
|
||||
{"'C:outside'", "must be project-relative",
|
||||
"parser rejects drive-relative capture history directory"},
|
||||
{R"('\\server\share\outside')", "must be project-relative",
|
||||
"parser rejects UNC capture history directory"},
|
||||
{R"('..\outside')", "must stay inside the project",
|
||||
"parser rejects backslash-escaping capture history directory"},
|
||||
{R"('nested\..\..\outside')", "must stay inside the project",
|
||||
"parser rejects nested backslash-escaping capture history directory"},
|
||||
{"'nested/..'", "must stay inside the project",
|
||||
"parser rejects a trailing .. component"},
|
||||
// A basic string, not a literal one: TOML forbids a raw NUL byte but
|
||||
// allows the \u0000 escape that produces it. The runtime passes this
|
||||
// value on as a C string, so a NUL would truncate it after validation.
|
||||
{R"("..\u0000keep")", "must not contain a NUL byte",
|
||||
"parser rejects an embedded NUL escape"},
|
||||
};
|
||||
for (size_t i = 0; i < std::size(rejected); ++i) {
|
||||
const auto& c = rejected[i];
|
||||
const auto cfg_path = write_config(
|
||||
root, fmt::format("capture-history-rejected-{}", i),
|
||||
fmt::format("[runtime]\noverlay_capture_persist_dir = {}\n",
|
||||
c.value));
|
||||
check_throws([&] { (void)PSXRecompV4::load_game_config(cfg_path); },
|
||||
c.needle, c.name);
|
||||
}
|
||||
|
||||
// A dotfile is not a `..` component; the guard must not over-reject.
|
||||
const auto dotted = write_config(root, "capture-history-dotted", R"toml(
|
||||
[runtime]
|
||||
overlay_capture_history = true
|
||||
overlay_capture_persist_dir = ".aot_capture_history/..keep/TEST-00000"
|
||||
)toml");
|
||||
const auto dotted_cfg = PSXRecompV4::load_game_config(dotted);
|
||||
check(dotted_cfg.runtime.overlay_capture_persist_dir ==
|
||||
".aot_capture_history/..keep/TEST-00000",
|
||||
"parser accepts a capture history component that merely starts with dots");
|
||||
}
|
||||
|
||||
void codegen_tests() {
|
||||
|
||||
Reference in New Issue
Block a user