D2R (Diablo II Resurrected) has no profile and its code could not run here: the host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a missing dm/PRODUCT, and the host files its hooks called were never in this repository. Removed with it: - rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4 rule set and private gates, the boot list, the cut scope column and the KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no params.tsv any more); dm/tools/product.sh - host: D2R save edits, add-on content, render-scale controller, time() takeover and Terror Zone clock, crash decoding at D2R offsets, host-call registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped archives, boot profiling hooks with no implementation; D2R headers, tests and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate - graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE, the eco-fps setting, the loading-screen gate; shader_map.py, nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the Breath of the Wild and Tears of the Kingdom stubs lose their mode test. Breath of the Wild's interval-controller cuts stay: the framework keeps the controller's interval while the game's VFR override holds. patch_nso.py output is byte-identical for all 15 games on this machine except the two frame-step stubs; all 18 profiles build; the host tests pass (their Makefile now builds the general tests only).
144 lines
12 KiB
C
144 lines
12 KiB
C
// Save write and save transaction unit test (takes seconds): includes hle_fs.c directly and calls its static implementation; pwrite is replaced by a scriptable fake.
|
|
// All expected values are hand-written from the semantics, not computed by calling the implementation under test:
|
|
// (1) pwrite returns 0 (can happen when the disk is full) -> WriteFile returns failure after one call (it used to loop forever here);
|
|
// (2) pwrite writes in pieces (3 bytes first, then the rest) -> success and the file content is complete;
|
|
// (3) transaction: the failure in (1) causes this transaction's Commit to be rejected and the working directory to return to the committed version; the next transaction writes in the working directory, the committed directory is still the old version before Commit and the new version after Commit;
|
|
// (4) DM_TEST_FS_FAIL=write:N:errno makes only the Nth save write fail, with writes before and after proceeding normally; that transaction's Commit is rejected, the committed version is unchanged, and the front end receives one notification;
|
|
// the next transaction commits normally, and the final save = the content of the next transaction;
|
|
// (5) remounting discards uncommitted content.
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off);
|
|
#define pwrite fake_pwrite
|
|
#include "../hle_fs.c"
|
|
#undef pwrite
|
|
ssize_t pwrite(int, const void *, size_t, off_t);
|
|
|
|
struct dm_flags g_flag; uint64_t g_t0_ns; uint64_t now_ns(void) { return 0; } // stubs for log timestamps and the read path (not used by the write path)
|
|
void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; }
|
|
extern int g_nfail, g_lasterr; // front-end notification count (the strong implementation in t_fs_stub.c replaces the weak one in hle_fs.c)
|
|
static int g_mode, g_calls; // 0 write normally; 1 always return 0; 2 write only 3 bytes the first time
|
|
static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off) {
|
|
g_calls++;
|
|
if (g_mode == 1) { if (g_calls > 1000) { printf(" ✗ pwrite retried over 1000 times after returning 0 (endless loop)\n"); exit(1); } return 0; }
|
|
if (g_mode == 2 && g_calls == 1 && n > 3) n = 3;
|
|
return pwrite(fd, buf, n, off);
|
|
}
|
|
static int fails, checks;
|
|
#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); } else printf(" ✓ "); printf(__VA_ARGS__); printf("\n"); } while (0)
|
|
static void put(const char *p, const char *s) { FILE *f = fopen(p, "w"); fputs(s, f); fclose(f); }
|
|
static int same(const char *p, const char *s) {
|
|
char b[64] = {0}; FILE *f = fopen(p, "r"); if (!f) return 0; size_t k = fread(b, 1, sizeof b - 1, f); fclose(f);
|
|
return k == strlen(s) && !memcmp(b, s, k);
|
|
}
|
|
static Result write_save(const char *path, const char *s) { // open -> set size -> write -> close
|
|
uint64_t h; Result r = h_fs_OpenFile(&h, path, MODE_WRITE); if (r) return r;
|
|
h_fs_SetFileSize(h, (int64_t)strlen(s));
|
|
r = h_fs_WriteFile(h, 0, s, strlen(s), NULL); h_fs_CloseFile(h); return r;
|
|
}
|
|
static int read_fs(const char *path, char *out, size_t n) { uint64_t h; size_t got = 0; if (h_fs_OpenFile(&h, path, MODE_READ)) return 0; Result r = h_fs_ReadFile(&got, h, 0, out, n - 1); h_fs_CloseFile(h); out[got] = 0; return !r; }
|
|
|
|
int main(void) {
|
|
char root[1024], save[1100], work[1200], p[1200];
|
|
const char *sp = getenv("TMPDIR"); snprintf(root, sizeof root, "%s/t_fs.XXXXXX", sp ? sp : "/private/tmp");
|
|
if (!mkdtemp(root)) { perror("mkdtemp"); return 1; }
|
|
char rom[1100], overlay[1100], rdata[1200], odata[1200], fn[1300], got[64];
|
|
snprintf(rom, sizeof rom, "%s/rom", root); snprintf(overlay, sizeof overlay, "%s/overlay", root); snprintf(rdata, sizeof rdata, "%s/data", rom); snprintf(odata, sizeof odata, "%s/data", overlay);
|
|
mkdir(rom, 0755); mkdir(overlay, 0755); mkdir(rdata, 0755); mkdir(odata, 0755);
|
|
snprintf(fn, sizeof fn, "%s/same", rdata); put(fn, "base"); snprintf(fn, sizeof fn, "%s/same", odata); put(fn, "overlay"); snprintf(fn, sizeof fn, "%s/base", rdata); put(fn, "only-base"); snprintf(fn, sizeof fn, "%s/over", odata); put(fn, "only-overlay");
|
|
setenv("DM_ROMFS", rom, 1); setenv("DM_ROMFS_OVERLAY", overlay, 1); CHECK(h_fs_MountRom("rom", NULL, 0) == RES_OK, "mount RomFS with two layers (base + overlay)");
|
|
CHECK(read_fs("rom:/data/same", got, sizeof got) && !strcmp(got, "overlay"), "the overlay file wins"); CHECK(read_fs("rom:/data/base", got, sizeof got) && !strcmp(got, "only-base"), "falls back to the base when the overlay lacks the file");
|
|
uint64_t rh; CHECK(h_fs_OpenFile(&rh, "rom:/data/same", MODE_WRITE) == RES_FAIL, "the two-layer mount refuses writes");
|
|
g_nmnt = 0;
|
|
snprintf(save, sizeof save, "%s/save", root); snprintf(work, sizeof work, "%s.work", save);
|
|
mkdir(save, 0755); snprintf(p, sizeof p, "%s/a", save); put(p, "old");
|
|
setenv("DM_SAVE", save, 1); setenv("DM_TEST_FS_FAIL", "write:5:28", 1); // the 5th save write fails, errno 28 = ENOSPC
|
|
CHECK(h_fs_MountSave("save", NULL) == RES_OK, "mount the save");
|
|
|
|
// Save write count: (1)=1st (2)=2nd (3)=3rd (4)=4th/5th/6th/7th (5)=8th
|
|
printf("① pwrite returns 0\n");
|
|
g_mode = 1; g_calls = 0;
|
|
CHECK(write_save("save:/a", "xyz") == RES_FAIL, "WriteFile returns failure 0x2A2");
|
|
CHECK(g_calls == 1, "pwrite called only once (actually %d times; it used to retry forever)", g_calls);
|
|
|
|
printf("② pwrite writes in pieces\n");
|
|
g_mode = 2; g_calls = 0;
|
|
CHECK(write_save("save:/a", "abcdefg") == RES_OK, "WriteFile succeeds");
|
|
CHECK(g_calls == 2, "pwrite called twice (3 bytes + 4 bytes; actually %d)", g_calls);
|
|
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "abcdefg"), "the work directory holds the complete abcdefg");
|
|
g_mode = 0;
|
|
|
|
printf("③ transaction\n");
|
|
CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == 1 && g_lasterr == ENOSPC, "the Commit of the transaction holding ①② is refused, front end notified once errno=ENOSPC (actually %d times errno=%d)", g_nfail, g_lasterr);
|
|
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "old"), "work directory discarded, back to the committed version old");
|
|
CHECK(write_save("save:/a", "new") == RES_OK, "next transaction: write new");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "old"), "before Commit the committed directory is still old");
|
|
CHECK(h_fs_Commit("save") == RES_OK, "Commit");
|
|
CHECK(same(p, "new"), "after Commit the committed directory is new");
|
|
snprintf(p, sizeof p, "%s.new", save); CHECK(access(p, F_OK) != 0, "the swapped-out old version is deleted (no .new left)");
|
|
|
|
printf("④ DM_TEST_FS_FAIL=write:5:28\n");
|
|
CHECK(write_save("save:/b", "4") == RES_PATH_NOT_FOUND, "(b does not exist: open fails, not counted as a write)");
|
|
h_fs_CreateFile("save:/b", 0);
|
|
CHECK(write_save("save:/b", "4") == RES_OK, "4th write succeeds");
|
|
errno = 0; CHECK(write_save("save:/b", "5") == RES_FAIL && errno == 28, "5th write fails errno=28 (actually errno=%d)", errno);
|
|
CHECK(write_save("save:/b", "6") == RES_OK, "6th write succeeds");
|
|
CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == 2 && g_lasterr == 28, "that transaction's Commit is refused, front end notified errno=28 (%d times in total)", g_nfail);
|
|
snprintf(p, sizeof p, "%s/b", save); CHECK(access(p, F_OK) != 0, "committed directory unchanged (no b)");
|
|
snprintf(p, sizeof p, "%s/b", work); CHECK(access(p, F_OK) != 0, "work directory discarded (no b)");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "new"), "committed directory a is still new");
|
|
h_fs_CreateFile("save:/b", 0);
|
|
CHECK(write_save("save:/b", "7") == RES_OK && h_fs_Commit("save") == RES_OK, "next transaction: write b=7, Commit succeeds");
|
|
snprintf(p, sizeof p, "%s/b", save); CHECK(same(p, "7"), "final committed b = 7 (the next transaction's content)");
|
|
CHECK(g_nfail == 2, "a successful transaction sends no notification (still %d times in total)", g_nfail);
|
|
|
|
printf("⑤ remounting discards uncommitted content\n");
|
|
CHECK(write_save("save:/a", "v9") == RES_OK, "write a=v9 without committing");
|
|
g_nmnt = 0; // simulate a process restart: clear the mount table
|
|
CHECK(h_fs_MountSave("save", NULL) == RES_OK, "remount");
|
|
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "new"), "work directory a is back to the committed version new");
|
|
snprintf(p, sizeof p, "%s/b", work); CHECK(same(p, "7"), "work directory b = the committed 7");
|
|
|
|
printf("⑥ failed writes are handled without exiting (DM_TEST_FS_FAILAT)\n");
|
|
unsetenv("DM_TEST_FS_FAIL");
|
|
// 6a Recloning after a refused commit fails: do not exit, the work directory is untouched and the committed version unchanged; the next commit is refused again and the reclone succeeds; the transaction after that commits normally
|
|
setenv("DM_TEST_FS_FAILAT", "reclone:1", 1);
|
|
CHECK(write_save("save:/a", "bad") == RES_OK, "write a=bad");
|
|
save_poison(ENOSPC); int nf0 = g_nfail; // a save write of this transaction failed
|
|
CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == nf0 + 1, "this transaction is refused (injected reclone failure), the process is still alive, front end notified once");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "new"), "committed directory a is still new");
|
|
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "bad"), "work directory not deleted (a is still bad)");
|
|
CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == nf0 + 2, "still tainted: the next commit is refused too");
|
|
snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "new"), "this time the reclone works: work directory a is back to new");
|
|
snprintf(p, sizeof p, "%s.re", work); CHECK(access(p, F_OK) != 0, "no .re left");
|
|
CHECK(write_save("save:/a", "good") == RES_OK && h_fs_Commit("save") == RES_OK, "next transaction writes a=good, commit succeeds");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "good"), "committed a = good");
|
|
// 6b Writing the guest name after a rename fails and the rollback fails too: do not exit; the file is in a transactional save, so this transaction's commit is refused and the committed version is unchanged; after recloning everything works
|
|
setenv("DM_TEST_FS_FAILAT", "guestname:1,renameback:1", 1);
|
|
h_fs_CreateFile("save:/c", 0); CHECK(write_save("save:/c", "c") == RES_OK, "write c");
|
|
CHECK(h_fs_RenameFile("save:/c", "save:/d") != RES_OK, "rename c → d returns failure (both steps injected to fail), the process is still alive");
|
|
nf0 = g_nfail; CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == nf0 + 1, "this transaction's commit is refused");
|
|
snprintf(p, sizeof p, "%s/c", save); CHECK(access(p, F_OK) != 0, "committed directory has no c");
|
|
snprintf(p, sizeof p, "%s/d", save); CHECK(access(p, F_OK) != 0, "committed directory has no d");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "good"), "committed a is still good");
|
|
CHECK(write_save("save:/a", "ok2") == RES_OK && h_fs_Commit("save") == RES_OK, "the next transaction commits as usual");
|
|
// 6c Cloning the work directory at mount fails: do not exit; mount the committed directory read-only instead. Reads work, writes fail, commits are no-ops, the committed version is unchanged, and the front end is notified once; the next normal mount works as before
|
|
setenv("DM_TEST_FS_FAILAT", "mountclone:1", 1); g_nmnt = 0; nf0 = g_nfail;
|
|
snprintf(p, sizeof p, "rm -rf '%s'", work); system(p);
|
|
CHECK(h_fs_MountSave("save", NULL) == RES_OK && g_nfail == nf0 + 1, "mount returns success, front end notified once (actually %d times)", g_nfail - nf0);
|
|
CHECK(read_fs("save:/a", got, sizeof got) && !strcmp(got, "ok2"), "the read-only mount reads the committed a=ok2 (read %s)", got);
|
|
CHECK(write_save("save:/a", "zzz") != RES_OK, "writes fail");
|
|
CHECK(h_fs_Commit("save") == RES_OK, "commit is a no-op");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "ok2"), "committed a is still ok2");
|
|
unsetenv("DM_TEST_FS_FAILAT"); g_nmnt = 0;
|
|
CHECK(h_fs_MountSave("save", NULL) == RES_OK && write_save("save:/a", "ok3") == RES_OK && h_fs_Commit("save") == RES_OK, "remount without injection: transactional as usual, writing a=ok3 and committing succeed");
|
|
snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "ok3"), "committed a = ok3");
|
|
|
|
|
|
char cmd[1200]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); system(cmd);
|
|
printf("== %d checks, %d failed ==\n", checks, fails);
|
|
return fails != 0;
|
|
}
|