Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions include/joybus/common/n64_pak_fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* @{
*/
Expand Down Expand Up @@ -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.
*
Expand Down
99 changes: 56 additions & 43 deletions src/n64_pak_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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;
}
51 changes: 51 additions & 0 deletions test/test_n64_pak_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading