Files
psxrecomp/runtime/tests/test_memcard_extensions.c
Matt Stanley c0ffd6d4c5 Support guarded mod address adapters and atomic extended memory cards
Extract the audited adapters and card transport onto master without the held selector/menu stack. Keep adapters and codecs tied to mod activation, use overlay ABI 33, retain rejected-load protection on codec registration, and refuse extended-card codecs in netplay before frame zero. Preserve default atomic save publication and guest-visible write failures.
2026-10-19 16:00:11 -07:00

215 lines
7.6 KiB
C

#include "memcard.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#endif
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
#define PATH0 "card1.mcd"
#define PATH1 "card2.mcd"
#define SECTOR 400
static uint8_t base[MEMCARD_SIZE], saved[MEMCARD_SIZE];
static int loads, save_reject, load_reject, have_tail[2];
static uint32_t large_size;
static int large_load(int card, const uint8_t* raw, const uint8_t* tail, uint32_t size) {
CHECK(card == 0 || card == 1);
CHECK(raw[0] == 'M' && raw[1] == 'C');
CHECK(size == (card == 0 ? large_size : 0));
for (uint32_t i = 0; i < size; ++i) CHECK(tail[i] == 'X');
return 0;
}
static int large_save(int card, const uint8_t* raw, uint8_t* tail, uint32_t capacity) {
CHECK(raw[0] == 'M' && raw[1] == 'C');
CHECK(capacity == MEMCARD_EXTENSION_MAX);
if (card != 0) return 0;
CHECK(large_size <= capacity);
memset(tail, 'X', large_size);
return (int)large_size;
}
static long file_size(const char* path) {
FILE* f = fopen(path, "rb");
CHECK(f && fseek(f, 0, SEEK_END) == 0);
long result = ftell(f);
CHECK(fclose(f) == 0);
return result;
}
static void append_tail(uint32_t size) {
FILE* f = fopen(PATH0, "ab");
CHECK(f);
for (uint32_t i = 0; i < size; ++i) CHECK(fputc('X', f) != EOF);
CHECK(fclose(f) == 0);
}
static void read_base(uint8_t* out) {
FILE* f = fopen(PATH0, "rb");
CHECK(f && fread(out, 1, MEMCARD_SIZE, f) == MEMCARD_SIZE);
CHECK(fclose(f) == 0);
}
static int codec_load(int card, const uint8_t* raw, const uint8_t* tail,
uint32_t size) {
CHECK(card == 0 || card == 1);
CHECK(raw[0] == 'M' && raw[1] == 'C');
CHECK(size == 0 || (size == 5 && tail));
++loads;
have_tail[card] = size != 0;
return load_reject ? -1 : 0;
}
static int codec_save(int card, const uint8_t* raw, uint8_t* tail,
uint32_t capacity) {
if (save_reject) return save_reject == 1 ? -1 : (int)capacity + 1;
if (!have_tail[card]) return 0;
CHECK(capacity >= 5);
memcpy(tail, "TAIL", 4);
tail[4] = raw[SECTOR * MEMCARD_SECTOR_SIZE];
return 5;
}
static void write_sector(uint8_t value) {
uint8_t sector[MEMCARD_SECTOR_SIZE];
memset(sector, value, sizeof sector);
CHECK(memcard_write_sector(0, SECTOR, sector) == 0);
}
static int dirty(void) {
int value;
CHECK(memcard_debug_info(0, NULL, NULL, NULL, &value) == 0);
return value;
}
int main(void) {
const MemcardSlotConfig slots[2] = {{PATH0, 1}, {PATH1, 1}};
MemcardSummary summary;
CHECK(memcard_format_file(PATH0) == 0);
CHECK(memcard_format_file(PATH1) == 0);
append_tail(5);
/* Inactive mods cannot silently truncate metadata, and the device/raw
* export must still expose exactly the ordinary card bytes. */
memcard_set_extension_codec(NULL, NULL);
CHECK(!memcard_extension_codec_active());
memcard_init_slots(NULL, slots);
CHECK(memcard_export_raw(0, base) == 0);
CHECK(base[0] == 'M' && base[1] == 'C');
write_sector(17);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == 0 && !dirty());
CHECK(file_size(PATH0) == MEMCARD_SIZE + 5);
FILE* f = fopen(PATH0, "rb");
CHECK(f && fseek(f, MEMCARD_SIZE, SEEK_SET) == 0);
for (int i = 0; i < 5; ++i) CHECK(fgetc(f) == 'X');
CHECK(fclose(f) == 0);
CHECK(memcard_summary_path(PATH0, &summary) == 0);
CHECK(summary.valid && summary.size_bytes == MEMCARD_SIZE + 5);
/* A selected codec sees both slots and binds the extension to the base
* in the SAME publication, after the actual native writes. */
memcard_set_extension_codec(codec_load, codec_save);
CHECK(memcard_extension_codec_active());
memcard_init_slots(NULL, slots);
CHECK(loads == 2 && have_tail[0] && !have_tail[1]);
write_sector(39);
memcard_flush_all();
CHECK(memcard_last_flush_result(0) == 0 && !dirty());
CHECK(file_size(PATH0) == MEMCARD_SIZE + 5);
f = fopen(PATH0, "rb");
CHECK(f && fseek(f, MEMCARD_SIZE, SEEK_SET) == 0);
char tail[5];
CHECK(fread(tail, 1, 5, f) == 5);
CHECK(memcmp(tail, "TAIL", 4) == 0 && tail[4] == 39);
CHECK(fclose(f) == 0);
read_base(saved);
/* Rejection or an impossible returned length must leave the original
* file intact and the buffered native write dirty for a later retry. */
write_sector(54);
for (save_reject = 1; save_reject <= 2; ++save_reject) {
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == -2 && dirty());
read_base(base);
CHECK(memcmp(base, saved, sizeof base) == 0);
CHECK(file_size(PATH0) == MEMCARD_SIZE + 5);
}
save_reject = 0;
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == 0 && !dirty());
read_base(saved);
load_reject = 1;
CHECK(memcard_reload_bound() == 0);
write_sector(71);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == -2 && dirty());
read_base(base);
CHECK(memcmp(base, saved, sizeof base) == 0);
/* Re-registering the same codec cannot bypass its rejected load. */
memcard_set_extension_codec(codec_load, codec_save);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == -2 && dirty());
read_base(base);
CHECK(memcmp(base, saved, sizeof base) == 0);
load_reject = 0;
CHECK(memcard_reload_bound() == 0);
/* The former cap, its first new byte, and the exact new cap all survive
* both an absent codec and a codec refresh/reload, byte for byte. */
const uint32_t sizes[] = {65536u, 65537u, MEMCARD_EXTENSION_MAX};
for (unsigned i = 0; i < sizeof sizes / sizeof sizes[0]; ++i) {
large_size = sizes[i];
memcard_set_extension_codec(NULL, NULL);
CHECK(memcard_format_file(PATH0) == 0);
append_tail(large_size);
CHECK(memcard_reload_bound() == 0);
write_sector(83);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == 0 && !dirty());
CHECK(file_size(PATH0) == (long)(MEMCARD_SIZE + large_size));
memcard_set_extension_codec(large_load, large_save);
CHECK(memcard_reload_bound() == 0);
write_sector(84);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == 0 && !dirty());
CHECK(file_size(PATH0) == (long)(MEMCARD_SIZE + large_size));
CHECK(memcard_reload_bound() == 0);
}
/* Oversized tails cannot be accidentally replaced with a shorter image. */
memcard_set_extension_codec(NULL, NULL);
CHECK(memcard_format_file(PATH0) == 0);
append_tail(MEMCARD_EXTENSION_MAX + 1u);
CHECK(memcard_reload_bound() == 0);
write_sector(91);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == -2 && dirty());
CHECK(file_size(PATH0) == MEMCARD_SIZE + MEMCARD_EXTENSION_MAX + 1u);
/* Raw import is explicit replacement, so previous extension ownership
* does not leak into a different standard card (e.g. netplay sandbox). */
CHECK(memcard_import_raw(0, saved) == 0);
CHECK(file_size(PATH0) == MEMCARD_SIZE);
/* Publication failure leaves dirty data and does not touch the old card. */
#ifdef _WIN32
(void)_mkdir("blocked_target");
#else
(void)mkdir("blocked_target", 0700);
#endif
CHECK(memcard_rebind_path(0, "blocked_target") == 0);
write_sector(105);
memcard_flush(0);
CHECK(memcard_last_flush_result(0) == -1 && dirty());
read_base(base);
CHECK(memcmp(base, saved, sizeof base) == 0);
memcard_set_extension_codec(NULL, NULL);
puts("memcard extensions: preservation, refresh, rejection, raw import and atomic failure passed");
return 0;
}