From e76d17f6aff7a42785fd3550a5dc979e85c7ec65 Mon Sep 17 00:00:00 2001 From: James Smith Date: Sat, 26 Sep 2026 19:23:46 -0700 Subject: [PATCH] add per-page format and single ID block validation, for memory constrained systems --- include/joybus/common/n64_pak_fs.h | 33 +++++++++- src/n64_pak_fs.c | 99 +++++++++++++++++------------- test/test_n64_pak_fs.c | 51 +++++++++++++++ 3 files changed, 138 insertions(+), 45 deletions(-) diff --git a/include/joybus/common/n64_pak_fs.h b/include/joybus/common/n64_pak_fs.h index eb03398..db7bd42 100644 --- a/include/joybus/common/n64_pak_fs.h +++ b/include/joybus/common/n64_pak_fs.h @@ -7,7 +7,8 @@ * ID block in page 0, an inode table and a mirror of it per bank, then two * pages of note table. These routines write that area for a given bank count * and check whether one is present, so an emulated pak can present itself as - * formatted. They work on a bank held in memory and do not touch the bus. + * formatted. They work on a bank held in memory, or a block or page at a time + * for a pak whose banks are not, and do not touch the bus. * * @{ */ @@ -44,15 +45,43 @@ bool joybus_n64_pak_fs_is_id_block(uint16_t addr); */ uint8_t joybus_n64_pak_fs_id_banks(const uint8_t block[JOYBUS_N64_PAK_BLOCK_SIZE]); +/** + * Check whether an ID block is intact and names the given bank count. + * + * A pak holds a filesystem when any one of its four ID copies passes. + * + * @param block an ID block, ::JOYBUS_N64_PAK_BLOCK_SIZE bytes + * @param banks how many banks the pak presents + * @return true if both checksums match, the block marks a Controller Pak, and it names that many banks + */ +bool joybus_n64_pak_fs_id_valid(const uint8_t block[JOYBUS_N64_PAK_BLOCK_SIZE], uint8_t banks); + /** * Check whether a bank holds a filesystem for the given bank count. * - * @param bank0 the first bank of the pak, ::JOYBUS_N64_PAK_BANK_SIZE bytes + * Only the ID copies in page 0 are read. + * + * @param bank0 the first bank of the pak, at least ::JOYBUS_N64_PAK_FS_PAGE_SIZE bytes * @param banks how many banks the pak presents * @return true if an intact ID names that many banks, false otherwise */ bool joybus_n64_pak_fs_valid(const uint8_t *bank0, uint8_t banks); +/** + * Fill one page of a fresh system area for the given bank count. + * + * Every page from 0 to JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks) - 1, each written + * at its index in the first bank, together format the pak. + * + * @param page the page to fill, ::JOYBUS_N64_PAK_FS_PAGE_SIZE bytes + * @param index the page's index in the first bank, below JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks) + * @param banks how many banks the pak presents, 1 to ::JOYBUS_N64_PAK_FS_MAX_BANKS + * @param random value stored in the ID to tell this pak apart from others + * @return 0 on success, -JOYBUS_ERR_INVALID_ARG for a bank count or index out of range + */ +int joybus_n64_pak_fs_format_page(uint8_t page[JOYBUS_N64_PAK_FS_PAGE_SIZE], uint8_t index, uint8_t banks, + uint32_t random); + /** * Write a fresh filesystem for the given bank count into a bank. * diff --git a/src/n64_pak_fs.c b/src/n64_pak_fs.c index 86df8ed..90fa104 100644 --- a/src/n64_pak_fs.c +++ b/src/n64_pak_fs.c @@ -61,21 +61,23 @@ static void id_checksums(const uint8_t *id, uint16_t *sum, uint16_t *inverted) } } -// Check whether one ID copy is intact and describes the given bank count -static bool id_valid(const uint8_t *id, uint8_t banks) +// Build the ID block as a console writes one +static void build_id(uint8_t id[ID_SIZE], uint8_t banks, uint32_t random) { - // Both checksums must match - uint16_t sum, inverted; - id_checksums(id, &sum, &inverted); - if (read16(&id[ID_CHECKSUM]) != sum || read16(&id[ID_INV_CHECKSUM]) != inverted) - return false; + memset(id, 0, ID_SIZE); + write32(&id[ID_REPAIRED], 0xFFFFFFFF); - // The low bit of the device ID marks a Controller Pak - if ((read16(&id[ID_DEVICE]) & 1) == 0) - return false; + // A console compares the whole ID on every access, so the random word is what tells paks apart + write32(&id[ID_RANDOM], random); + write16(&id[ID_DEVICE], ID_DEVICE_PAK); + id[ID_BANKS] = banks; + id[ID_VERSION] = 0; - // The ID must name the bank count the pak presents - return id[ID_BANKS] == banks; + // Checksum the ID + uint16_t sum, inverted; + id_checksums(id, &sum, &inverted); + write16(&id[ID_CHECKSUM], sum); + write16(&id[ID_INV_CHECKSUM], inverted); } // Build an inode page with every data page free @@ -96,11 +98,27 @@ static void inode_page(uint8_t *page, uint8_t bank, uint8_t banks) page[1] = sum; } +bool joybus_n64_pak_fs_id_valid(const uint8_t block[JOYBUS_N64_PAK_BLOCK_SIZE], uint8_t banks) +{ + // Both checksums must match + uint16_t sum, inverted; + id_checksums(block, &sum, &inverted); + if (read16(&block[ID_CHECKSUM]) != sum || read16(&block[ID_INV_CHECKSUM]) != inverted) + return false; + + // The low bit of the device ID marks a Controller Pak + if ((read16(&block[ID_DEVICE]) & 1) == 0) + return false; + + // The ID must name the bank count the pak presents + return block[ID_BANKS] == banks; +} + bool joybus_n64_pak_fs_valid(const uint8_t *bank0, uint8_t banks) { // Any one intact copy is enough, a console repairs the rest from it for (int i = 0; i < ID_COPIES; i++) { - if (id_valid(&bank0[id_offsets[i]], banks)) + if (joybus_n64_pak_fs_id_valid(&bank0[id_offsets[i]], banks)) return true; } @@ -124,44 +142,39 @@ uint8_t joybus_n64_pak_fs_id_banks(const uint8_t block[JOYBUS_N64_PAK_BLOCK_SIZE return block[ID_BANKS]; } -int joybus_n64_pak_fs_format(uint8_t *bank0, uint8_t banks, uint32_t random) +int joybus_n64_pak_fs_format_page(uint8_t page[JOYBUS_N64_PAK_FS_PAGE_SIZE], uint8_t index, uint8_t banks, + uint32_t random) { - if (banks == 0 || banks > JOYBUS_N64_PAK_FS_MAX_BANKS) + if (banks == 0 || banks > JOYBUS_N64_PAK_FS_MAX_BANKS || index >= JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks)) return -JOYBUS_ERR_INVALID_ARG; - int system = JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks) * JOYBUS_N64_PAK_FS_PAGE_SIZE; - - // Clear the system area, which leaves the note table empty - memset(bank0, 0, system); + // One inode page per bank from page 1, then a mirror of each + if (index >= 1 && index <= 2 * banks) { + inode_page(page, (index - 1) % banks, banks); + return 0; + } - // Build the ID block as a console writes one - uint8_t id[ID_SIZE] = {0}; - write32(&id[ID_REPAIRED], 0xFFFFFFFF); + // Page 0 and the note table are empty, apart from the ID copies in page 0 + memset(page, 0, JOYBUS_N64_PAK_FS_PAGE_SIZE); - // A console compares the whole ID on every access, so the random word is what tells paks apart - write32(&id[ID_RANDOM], random); - write16(&id[ID_DEVICE], ID_DEVICE_PAK); - id[ID_BANKS] = banks; - id[ID_VERSION] = 0; + if (index == 0) { + uint8_t id[ID_SIZE]; + build_id(id, banks, random); - // Checksum the ID - uint16_t sum, inverted; - id_checksums(id, &sum, &inverted); - write16(&id[ID_CHECKSUM], sum); - write16(&id[ID_INV_CHECKSUM], inverted); + for (int i = 0; i < ID_COPIES; i++) + memcpy(&page[id_offsets[i]], id, ID_SIZE); + } - // Write all four copies of the ID - for (int i = 0; i < ID_COPIES; i++) - memcpy(&bank0[id_offsets[i]], id, ID_SIZE); + return 0; +} - // Write one inode page per bank from page 1, then a mirror of each - for (uint8_t bank = 0; bank < banks; bank++) { - uint8_t *table = &bank0[(1 + bank) * JOYBUS_N64_PAK_FS_PAGE_SIZE]; - uint8_t *mirror = &bank0[(1 + banks + bank) * JOYBUS_N64_PAK_FS_PAGE_SIZE]; +int joybus_n64_pak_fs_format(uint8_t *bank0, uint8_t banks, uint32_t random) +{ + if (banks == 0 || banks > JOYBUS_N64_PAK_FS_MAX_BANKS) + return -JOYBUS_ERR_INVALID_ARG; - inode_page(table, bank, banks); - memcpy(mirror, table, JOYBUS_N64_PAK_FS_PAGE_SIZE); - } + for (uint8_t index = 0; index < JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks); index++) + joybus_n64_pak_fs_format_page(&bank0[index * JOYBUS_N64_PAK_FS_PAGE_SIZE], index, banks, random); - return system; + return JOYBUS_N64_PAK_FS_SYSTEM_PAGES(banks) * JOYBUS_N64_PAK_FS_PAGE_SIZE; } diff --git a/test/test_n64_pak_fs.c b/test/test_n64_pak_fs.c index 94e0df0..c7d1921 100644 --- a/test/test_n64_pak_fs.c +++ b/test/test_n64_pak_fs.c @@ -160,10 +160,61 @@ static void test_format_inodes_one_bank() TEST_ASSERT_TRUE(joybus_n64_pak_fs_valid(bank0, 1)); } +// Test that a single ID block is checked for its checksums and bank count +static void test_id_valid() +{ + uint8_t block[32]; + memcpy(block, console_id, sizeof(block)); + + TEST_ASSERT_TRUE(joybus_n64_pak_fs_id_valid(block, 16)); + TEST_ASSERT_FALSE(joybus_n64_pak_fs_id_valid(block, 1)); + + block[0x1E] ^= 0x01; + TEST_ASSERT_FALSE(joybus_n64_pak_fs_id_valid(block, 16)); +} + +// Test that a page index or bank count outside the system area is rejected without writing +static void test_format_page_rejects_out_of_range() +{ + uint8_t page[256]; + memset(page, 0xFF, sizeof(page)); + + TEST_ASSERT_EQUAL(-JOYBUS_ERR_INVALID_ARG, joybus_n64_pak_fs_format_page(page, 3 + 2 * 16, 16, 0)); + TEST_ASSERT_EQUAL(-JOYBUS_ERR_INVALID_ARG, joybus_n64_pak_fs_format_page(page, 0, 0, 0)); + TEST_ASSERT_EQUAL(-JOYBUS_ERR_INVALID_ARG, + joybus_n64_pak_fs_format_page(page, 0, JOYBUS_N64_PAK_FS_MAX_BANKS + 1, 0)); + TEST_ASSERT_EACH_EQUAL_HEX8(0xFF, page, sizeof(page)); + + TEST_ASSERT_EQUAL(0, joybus_n64_pak_fs_format_page(page, 2 + 2 * 16, 16, 0)); +} + +// Test that page 0 built alone holds ID blocks that pass one at a time, as a +// pak with no bank in memory checks them +static void test_format_page_ids_valid_by_block() +{ + uint8_t page[256]; + TEST_ASSERT_EQUAL(0, joybus_n64_pak_fs_format_page(page, 0, 16, 0x12345678)); + + int copies = 0; + for (uint16_t addr = 0; addr < sizeof(page); addr += JOYBUS_N64_PAK_BLOCK_SIZE) { + if (!joybus_n64_pak_fs_is_id_block(addr)) + continue; + + TEST_ASSERT_TRUE(joybus_n64_pak_fs_id_valid(&page[addr], 16)); + copies++; + } + + TEST_ASSERT_EQUAL(4, copies); +} + int main(int argc, char **argv) { UNITY_BEGIN(); + RUN_TEST(test_id_valid); + RUN_TEST(test_format_page_rejects_out_of_range); + RUN_TEST(test_format_page_ids_valid_by_block); + RUN_TEST(test_valid_rejects_erased); RUN_TEST(test_valid_accepts_console_id); RUN_TEST(test_valid_accepts_any_copy);