From 5a7ae24bb9dbb99b0b5405b19f583ff4691569d2 Mon Sep 17 00:00:00 2001 From: Raphael Assenat Date: Sun, 25 Oct 2015 01:57:10 -0400 Subject: [PATCH] Add command-line utilities for N64 mempak manipulation --- tool/n64savetool/Makefile | 33 + tool/n64savetool/mempak.c | 178 ++++ tool/n64savetool/mempak.h | 28 + tool/n64savetool/mempak_extract_note.c | 38 + tool/n64savetool/mempak_format.c | 28 + tool/n64savetool/mempak_fs.c | 1254 ++++++++++++++++++++++++ tool/n64savetool/mempak_fs.h | 80 ++ tool/n64savetool/mempak_ls.c | 57 ++ 8 files changed, 1696 insertions(+) create mode 100644 tool/n64savetool/Makefile create mode 100644 tool/n64savetool/mempak.c create mode 100644 tool/n64savetool/mempak.h create mode 100644 tool/n64savetool/mempak_extract_note.c create mode 100644 tool/n64savetool/mempak_format.c create mode 100644 tool/n64savetool/mempak_fs.c create mode 100644 tool/n64savetool/mempak_fs.h create mode 100644 tool/n64savetool/mempak_ls.c diff --git a/tool/n64savetool/Makefile b/tool/n64savetool/Makefile new file mode 100644 index 0000000..80223d8 --- /dev/null +++ b/tool/n64savetool/Makefile @@ -0,0 +1,33 @@ +CC=gcc +LD=$(CC) + +CFLAGS=-Wall -g --std=c99 +LDFLAGS=-g + +PREFIX=/usr/local + +PROGRAMS=mempak_ls mempak_format mempak_extract_note +MEMPAKLIB_OBJS=mempak.o mempak_fs.o + +.PHONY : clean install + +all: $(PROGRAMS) + +mempak_extract_note: mempak_extract_note.o $(MEMPAKLIB_OBJS) + $(LD) $^ $(LDFLAGS) -o $@ + +mempak_ls: mempak_ls.o $(MEMPAKLIB_OBJS) + $(LD) $^ $(LDFLAGS) -o $@ + +mempak_format: mempak_format.o $(MEMPAKLIB_OBJS) + $(LD) $^ $(LDFLAGS) -o $@ + +%.o: %.c %.h + $(CC) $(CFLAGS) -c $< + +clean: + rm -f *.o $(PROGRAMS) + +install: + @echo "Install not done yet. Sorry" + diff --git a/tool/n64savetool/mempak.c b/tool/n64savetool/mempak.c new file mode 100644 index 0000000..c6cc45b --- /dev/null +++ b/tool/n64savetool/mempak.c @@ -0,0 +1,178 @@ +#include +#include +#include +#include "mempak.h" + +#define DEXDRIVE_DATA_OFFSET 0x1040 + +mempak_structure_t *mempak_new(void) +{ + mempak_structure_t *mpk; + + mpk = calloc(1, sizeof(mempak_structure_t)); + if (!mpk) { + perror("calloc"); + return NULL; + } + + format_mempak(mpk); + + return mpk; +} + +int mempak_exportNote(mempak_structure_t *mpk, int note_id, const char *dst_filename) +{ + FILE *fptr; + entry_structure_t note_header; + unsigned char databuf[0x10000]; + + if (!mpk) + return -1; + + if (0 != get_mempak_entry(mpk, note_id, ¬e_header)) { + fprintf(stderr, "Error accessing note\n"); + return -1; + } + + if (!note_header.valid) { + fprintf(stderr, "Invaid note\n"); + return -1; + } + + if (0 != read_mempak_entry_data(mpk, ¬e_header, databuf)) { + fprintf(stderr, "Error accessing note data\n"); + return -1; + } + + fptr = fopen(dst_filename, "w"); + if (!fptr) { + perror("fopen"); + return -1; + } + + /* For compatibility with bryc's javascript mempak editor[1], I set + * the inode number to 0xCAFE. + * + * [1] https://github.com/bryc/mempak + */ + note_header.raw_data[0x07] = 0xCA; + note_header.raw_data[0x08] = 0xFE; + + fwrite(note_header.raw_data, 32, 1, fptr); + fwrite(databuf, MEMPAK_BLOCK_SIZE, note_header.blocks, fptr); + fclose(fptr); + + return 0; +} + +int mempak_saveToFile(mempak_structure_t *mpk, const char *dst_filename, unsigned char format) +{ + FILE *fptr; + + if (!mpk) + return -1; + + fptr = fopen(dst_filename, "w"); + if (!fptr) { + perror("fopen"); + return -1; + } + + switch(format) + { + default: + fclose(fptr); + return -1; + + case MPK_SAVE_FORMAT_MPK: + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + break; + + case MPK_SAVE_FORMAT_MPK4: + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + break; + + case MPK_SAVE_FORMAT_N64: + // Note: This should work well for files that will + // be imported by non-official software which typically + // only look for the 123-456-STD header and then + // seek to the data. + // + // Real .N64 files contain more info. TODO: Support it + fprintf(fptr, "123-456-STD"); + fseek(fptr, DEXDRIVE_DATA_OFFSET, SEEK_SET); + fwrite(mpk->data, sizeof(mpk->data), 1, fptr); + break; + } + + return 0; +} + +mempak_structure_t *mempak_loadFromFile(const char *filename) +{ + FILE *fptr; + long file_size; + int i; + int num_images = -1; + long offset = 0; + mempak_structure_t *mpk; + + mpk = calloc(1, sizeof(mempak_structure_t)); + if (!mpk) { + perror("calloc"); + return NULL; + } + + fptr = fopen(filename, "rb"); + if (!fptr) { + perror("fopen"); + free(mpk); + return NULL; + } + + fseek(fptr, 0, SEEK_END); + file_size = ftell(fptr); + fseek(fptr, 0, SEEK_SET); + + printf("File size: %ld bytes\n", file_size); + + /* Raw binary images. Those can contain more than one card's data. For + * instance, Mupen64 seems to contain four saves. (I suppose each 32kB block is + * for the virtual mempak of one controller) */ + for (i=0; i<4; i++) { + if (file_size == 0x8000*i) { + num_images = i+1; + printf("MPK file Contains %d image(s)\n", num_images); + mpk->source = MPK_SRC_RAW_IMAGE; + } + } + + if (num_images < 0) { + char header[11]; + char *magic = "123-456-STD"; + /* If the size is not a fixed multiple, it could be a .N64 file */ + fread(header, 11, 1, fptr); + if (0 == memcmp(header, magic, sizeof(header))) { + printf(".N64 file detected\n"); + // TODO : Extract comments and other info. from the header + offset = DEXDRIVE_DATA_OFFSET; // Thanks to N-Rage`s Dinput8 Plugin sources + mpk->source = MPK_SRC_DEX_IMAGE; + } + } + + fseek(fptr, offset, SEEK_SET); + fread(mpk->data, sizeof(mpk->data), 1, fptr); + fclose(fptr); + + return mpk; +} + +void mempak_free(mempak_structure_t *mpk) +{ + if (mpk) + free(mpk); +} + diff --git a/tool/n64savetool/mempak.h b/tool/n64savetool/mempak.h new file mode 100644 index 0000000..a51ecc4 --- /dev/null +++ b/tool/n64savetool/mempak.h @@ -0,0 +1,28 @@ +#ifndef _mempak_h__ +#define _mempak_h__ + +#define MEMPAK_NUM_NOTES 16 + +#define MPK_SRC_RAW_IMAGE 0 +#define MPK_SRC_DEX_IMAGE 1 + +typedef struct mempak_structure +{ + unsigned char data[0x8000]; + unsigned char source; +} mempak_structure_t; + +mempak_structure_t *mempak_new(void); +mempak_structure_t *mempak_loadFromFile(const char *filename); + +#define MPK_SAVE_FORMAT_MPK 0 +#define MPK_SAVE_FORMAT_MPK4 1 // MPK + 3 times 32kB padding +#define MPK_SAVE_FORMAT_N64 2 +int mempak_saveToFile(mempak_structure_t *mpk, const char *dst_filename, unsigned char format); +int mempak_exportNote(mempak_structure_t *mpk, int note_id, const char *dst_filename); +void mempak_free(mempak_structure_t *mpk); + +#include "mempak_fs.h" + +#endif // _mempak_h__ + diff --git a/tool/n64savetool/mempak_extract_note.c b/tool/n64savetool/mempak_extract_note.c new file mode 100644 index 0000000..d8f2e8b --- /dev/null +++ b/tool/n64savetool/mempak_extract_note.c @@ -0,0 +1,38 @@ +#include +#include +#include "mempak.h" + +int main(int argc, char **argv) +{ + const char *infile; + const char *outfile; + int note_id; + mempak_structure_t *mpk; + + if (argc < 4) { + printf("Usage: ./mempak_extract_note in_file note_id out_file\n"); + printf("\n"); + printf("Where:\n"); + printf(" in_file The input file (full mempak image .mpk/.n64)\n"); + printf(" note_id The id of the note (as shown by mempak_ls)\n"); + printf(" out_file The output filename (eg: abc.note)\n"); + return 1; + } + + infile = argv[1]; + note_id = atoi(argv[2]); + outfile = argv[3]; + + mpk = mempak_loadFromFile(infile); + if (!mpk) { + return 1; + } + + if (mempak_exportNote(mpk, note_id, outfile)) { + fprintf(stderr, "could not export note\n"); + } + + mempak_free(mpk); + + return 0; +} diff --git a/tool/n64savetool/mempak_format.c b/tool/n64savetool/mempak_format.c new file mode 100644 index 0000000..e8958d4 --- /dev/null +++ b/tool/n64savetool/mempak_format.c @@ -0,0 +1,28 @@ +#include +#include "mempak.h" + + +int main(int argc, char **argv) +{ + mempak_structure_t *mpk; + const char *outfile; + + if (argc < 2) { + printf("Usage: ./mempak_ls file\n"); + printf("\n"); + printf("Raw files (.MPK, .BIN) and Dexdrive (.N64) formats accepted.\n"); + return 1; + } + + outfile = argv[1]; + mpk = mempak_new(); + if (!mpk) { + return 1; + } + + mempak_saveToFile(mpk, outfile, MPK_SAVE_FORMAT_N64); + + mempak_free(mpk); + + return 0; +} diff --git a/tool/n64savetool/mempak_fs.c b/tool/n64savetool/mempak_fs.c new file mode 100644 index 0000000..f6e5aec --- /dev/null +++ b/tool/n64savetool/mempak_fs.c @@ -0,0 +1,1254 @@ +/** + * @file mempak.c + * @brief Mempak Filesystem Routine + * @ingroup mempak + */ +#include +#include "mempak.h" + +/** + * @defgroup mempak Mempak Filesystem Routines + * @ingroup controller + * @brief Managed mempak interface. + * + * The mempak system is a subsystem of the @ref controller. Before attempting to + * read from or write to a mempak, be sure you have initialized the controller system + * with #controller_init and verified that you have a mempak in the correct controller + * using #identify_accessory. + * + * To read and write to the mempak in an organized way compatible with official software, + * first check that the mempak is valid using #validate_mempak. If the mempack is + * invalid, it will need to be formatted using #format_mempak. Once the mempak is + * considered valid, existing notes can be enumerated using #get_mempak_entry. To + * read the data associated with a note, use #read_mempak_entry_data. To write a + * new note to the mempak, use #write_mempak_entry_data. Note that there is no append + * functionality so if a note is being updated, ensure you have deleted the old note + * first using #delete_mempak_entry. Code should be careful to check how many blocks + * are free before writing using #get_mempak_free_space. + * + * @{ + */ + +/** + * @name Inode values + * @{ + */ + +/** @brief This block is empty */ +#define BLOCK_EMPTY 0x03 +/** @brief This is the last block in the note */ +#define BLOCK_LAST 0x01 +/** @brief First valid block that can contain user data */ +#define BLOCK_VALID_FIRST 0x05 +/** @brief Last valid block that can contain user data */ +#define BLOCK_VALID_LAST 0x7F +/** @} */ + +/** + * @brief Read a sector from a mempak + * + * This will read a sector from a mempak. Sectors on mempaks are always 256 bytes + * in size. + * + * @param[in] controller + * The controller (0-3) to read a sector from + * @param[in] sector + * The sector (0-127) to read from + * @param[out] sector_data + * Buffer to place 256 read bytes of data + * + * @retval 0 if reading was successful + * @retval -1 if the sector was out of bounds or sector_data was null + * @retval -2 if there was an error reading part of a sector + */ +int read_mempak_sector( mempak_structure_t *pak, int sector, uint8_t *sector_data ) +{ + if( sector < 0 || sector >= 128 ) { return -1; } + if( sector_data == 0 ) { return -1; } + + memcpy(sector_data, pak->data + sector * MEMPAK_BLOCK_SIZE, MEMPAK_BLOCK_SIZE); +#if 0 + /* Sectors are 256 bytes, a mempak reads 32 bytes at a time */ + for( int i = 0; i < 8; i++ ) + { + if( read_mempak_address( controller, (sector * MEMPAK_BLOCK_SIZE) + (i * 32), sector_data + (i * 32) ) ) + { + /* Failed to read a block */ + return -2; + } + } +#endif + return 0; +} + +/** + * @brief Write a sector to a mempak + * + * This will write a sector to a mempak. Sectors on mempaks are always 256 bytes + * in size. + * + * @param[in] controller + * The controller (0-3) to write a sector to + * @param[in] sector + * The sector (0-127) to write to + * @param[out] sector_data + * Buffer containing 256 bytes of data to write to sector + * + * @retval 0 if writing was successful + * @retval -1 if the sector was out of bounds or sector_data was null + * @retval -2 if there was an error writing part of a sector + */ +int write_mempak_sector( mempak_structure_t *pak, int sector, uint8_t *sector_data ) +{ + if( sector < 0 || sector >= 128 ) { return -1; } + if( sector_data == 0 ) { return -1; } + + memcpy(pak->data + sector * MEMPAK_BLOCK_SIZE, sector_data, 256); +#if 0 + /* Sectors are 256 bytes, a mempak writes 32 bytes at a time */ + for( int i = 0; i < 8; i++ ) + { + if( write_mempak_address( controller, (sector * MEMPAK_BLOCK_SIZE) + (i * 32), sector_data + (i * 32) ) ) + { + /* Failed to read a block */ + return -2; + } + } +#endif + return 0; +} + +/** + * @brief Check a mempak header for validity + * + * @param[in] sector + * A sector containing a mempak header + * + * @retval 0 if the header is valid + * @retval -1 if the header is invalid + */ +static int __validate_header( uint8_t *sector ) +{ + if( !sector ) { return -1; } + + /* Header is first sector */ + if( memcmp( §or[0x20], §or[0x60], 16 ) != 0 ) { return -1; } + if( memcmp( §or[0x80], §or[0xC0], 16 ) != 0 ) { return -1; } + if( memcmp( §or[0x20], §or[0x80], 16 ) != 0 ) { return -1; } + + return 0; +} + +/** + * @brief Calculate the checksum over a TOC sector + * + * @param[in] sector + * A sector containing a TOC + * + * @return The 8 bit checksum over the TOC + */ +static uint8_t __get_toc_checksum( uint8_t *sector ) +{ + uint32_t sum = 0; + + /* Rudimentary checksum */ + for( int i = 5; i < 128; i++ ) + { + sum += sector[(i << 1) + 1]; + } + + return sum & 0xFF; +} + +/** + * @brief Check a mempak TOC sector for validity + * + * @param[in] sector + * A sector containing a TOC + * + * @retval 0 if the TOC is valid + * @retval -1 if the TOC is invalid + */ +static int __validate_toc( uint8_t *sector ) +{ + /* True checksum is sum % 256 */ + if( __get_toc_checksum( sector ) == sector[1] ) + { + return 0; + } + else + { + return -1; + } + + return -1; +} + +/** + * @brief Convert a N64 mempak character to ASCII + * + * @param[in] c + * A character read from a mempak entry title + * + * @return ASCII equivalent of character read + */ +static char __n64_to_ascii( char c ) +{ + /* Miscelaneous chart */ + switch( c ) + { + case 0x00: + return 0; + case 0x0F: + return ' '; + case 0x34: + return '!'; + case 0x35: + return '\"'; + case 0x36: + return '#'; + case 0x37: + return '`'; + case 0x38: + return '*'; + case 0x39: + return '+'; + case 0x3A: + return ','; + case 0x3B: + return '-'; + case 0x3C: + return '.'; + case 0x3D: + return '/'; + case 0x3E: + return ':'; + case 0x3F: + return '='; + case 0x40: + return '?'; + case 0x41: + return '@'; + } + + /* Numbers */ + if( c >= 0x10 && c <= 0x19 ) + { + return '0' + (c - 0x10); + } + + /* Uppercase ASCII */ + if( c >= 0x1A && c <= 0x33 ) + { + return 'A' + (c - 0x1A); + } + + /* Default to space for unprintables */ + return ' '; +} + +/** + * @brief Convert an ASCII character to a N64 mempak character + * + * If the character passed in is one that the N64 mempak doesn't support, this + * function will default to a space. + * + * @param[in] c + * An ASCII character + * + * @return A N64 mempak character equivalent to the ASCII character passed in + */ +static char __ascii_to_n64( char c ) +{ + /* Miscelaneous chart */ + switch( c ) + { + case 0: + return 0x00; + case ' ': + return 0x0F; + case '!': + return 0x34; + case '\"': + return 0x35; + case '#': + return 0x36; + case '`': + return 0x37; + case '*': + return 0x38; + case '+': + return 0x39; + case ',': + return 0x3A; + case '-': + return 0x3B; + case '.': + return 0x3C; + case '/': + return 0x3D; + case ':': + return 0x3E; + case '=': + return 0x3F; + case '?': + return 0x40; + case '@': + return 0x41; + } + + /* Numbers */ + if( c >= '0' && c <= '9' ) + { + return 0x10 + (c - '0'); + } + + /* Uppercase ASCII */ + if( c >= 'A' && c <= 'Z' ) + { + return 0x1A + (c - 'A'); + } + + /* Default to space for unprintables */ + return 0x0F; +} + +/** + * @brief Check a region read from a mempak entry for validity + * + * @param[in] region + * A region read from a mempak entry + * + * @retval 0 if the region is valid + * @retval -1 if the region is invalid + */ +static int __validate_region( uint8_t region ) +{ + switch( region ) + { + case 0x00: + case 0x37: + case 0x41: + case 0x44: + case 0x45: + case 0x46: + case 0x49: + case 0x4A: + case 0x50: + case 0x53: + case 0x55: + case 0x58: + case 0x59: + /* Acceptible region */ + return 0; + } + + /* Invalid region */ + return -3; +} + +/** + * @brief Parse a note structure from a TOC + * + * Given a note block read from a mempak TOC, parse and return a structure + * representing the data. + * + * @param[in] tnote + * 32 bytes read from a mempak TOC + * @param[out] note + * Parsed note structure + * + * @retval 0 note was parsed properly + * @retval -1 parameters were invalid + * @retval -2 note inode out of bounds + * @retval -3 note region invalid + */ +static int __read_note( uint8_t *tnote, entry_structure_t *note ) +{ + if( !tnote || !note ) { return -1; } + + /* Easy stuff */ + note->vendor = (tnote[0] << 16) | (tnote[1] << 8) | (tnote[2]); + note->region = tnote[3]; + memcpy(note->raw_data, tnote, 32); + + /* Important stuff */ + note->game_id = (tnote[4] << 8) | (tnote[5]); + note->inode = (tnote[6] << 8) | (tnote[7]); + + /* Don't know number of blocks */ + note->blocks = 0; + note->valid = 0; + note->entry_id = 255; + + /* Translate n64 to ascii */ + memset( note->name, 0, sizeof( note->name ) ); + + for( int i = 0; i < 16; i++ ) + { + note->name[i] = __n64_to_ascii( tnote[0x10 + i] ); + } + + /* Find the last position */ + for( int i = 0; i < 17; i++ ) + { + if( note->name[i] == 0 ) + { + /* Here it is! */ + note->name[i] = '.'; + note->name[i+1] = __n64_to_ascii( tnote[0xC] ); + break; + } + } + + /* Validate entries */ + if( note->inode < BLOCK_VALID_FIRST || note->inode > BLOCK_VALID_LAST ) + { + /* Invalid inode */ + return -2; + } + + if( __validate_region( note->region ) ) + { + /* Invalid region */ + return -3; + } + + /* Checks out */ + note->valid = 1; + return 0; +} + +/** + * @brief Create a note structure for a mempak TOC + * + * Given a valid note structure, format it for writing to a mempak TOC + * + * @param[in] note + * Valid note structure to convert + * @param[out] out_note + * 32 bytes ready to be written to a mempak TOC + * + * @retval 0 if the note was converted properly + * @retval -1 if the parameters were invalid + */ +static int __write_note( entry_structure_t *note, uint8_t *out_note ) +{ + char tname[19]; + if( !out_note || !note ) { return -1; } + + /* Start with baseline */ + memset( out_note, 0, 32 ); + + /* Easy stuff */ + out_note[0] = (note->vendor >> 16) & 0xFF; + out_note[1] = (note->vendor >> 8) & 0xFF; + out_note[2] = note->vendor & 0xFF; + + out_note[3] = note->region; + + /* Important stuff */ + out_note[4] = (note->game_id >> 8) & 0xFF; + out_note[5] = note->game_id & 0xFF; + out_note[6] = (note->inode >> 8) & 0xFF; + out_note[7] = note->inode & 0xFF; + + /* Fields that generally stay the same on official saves */ + out_note[8] = 0x02; + out_note[9] = 0x03; + + /* Translate ascii to n64 */ + memcpy( tname, note->name, sizeof( note->name ) ); + for( int i = 18; i >= 0; i-- ) + { + if( tname[i] == '.' ) + { + /* Found extension */ + out_note[0xC] = __ascii_to_n64( tname[i+1] ); + + /* Erase these as they have been taken care of */ + tname[i] = 0; + tname[i+1] = 0; + } + } + + for( int i = 0; i < 16; i++ ) + { + out_note[0x10 + i] = __ascii_to_n64( tname[i] ); + } + + return 0; +} + +/** + * @brief Return number of pages a note occupies + * + * Given a starting inode and a TOC sector, walk the linked list for a note + * and return the number of pages/blocks/sectors a note occupies. + * + * @param[in] sector + * A TOC sector + * @param[in] inode + * A starting inode + * + * @retval -2 The file contained free blocks + * @retval -3 The filesystem was invalid + * @return The number of blocks in a note + */ +static int __get_num_pages( uint8_t *sector, int inode ) +{ + if( inode < BLOCK_VALID_FIRST || inode > BLOCK_VALID_LAST ) { return -1; } + + int tally = 0; + int last = inode; + int rcount = 0; + + /* If we go over this, something is wrong */ + while( rcount < 123 ) + { + switch( sector[(last << 1) + 1] ) + { + case BLOCK_LAST: + /* Last block */ + return tally + 1; + case BLOCK_EMPTY: + /* Error, can't have free blocks! */ + return -2; + default: + last = sector[(last << 1) + 1]; + tally++; + + /* Failed to point to valid next block */ + if( last < BLOCK_VALID_FIRST || last > BLOCK_VALID_LAST ) { return -3; } + break; + } + + rcount++; + } + + /* Invalid filesystem */ + return -3; +} + +/** + * @brief Get number of free blocks on a mempak + * + * @param[in] sector + * A valid TOC block to examine + * + * @return The number of free blocks + */ +static int __get_free_space( uint8_t *sector ) +{ + int space = 0; + + for( int i = BLOCK_VALID_FIRST; i <= BLOCK_VALID_LAST; i++ ) + { + if( sector[(i << 1) + 1] == BLOCK_EMPTY ) + { + space++; + } + } + + return space; +} + +/** + * @brief Get the inode of the n'th block in a note + * + * @param[in] sector + * A valid TOC sector + * @param[in] inode + * The starting inode of the note + * @param[in] block + * The block offset (starting from 0) to retrieve + * + * @retval -2 if there were free blocks in the file + * @retval -3 if the filesystem was invalid + * @return The inode of the n'th block + */ +static int __get_note_block( uint8_t *sector, int inode, int block ) +{ + if( inode < BLOCK_VALID_FIRST || inode > BLOCK_VALID_LAST ) { return -1; } + if( block < 0 || block > 123 ) { return -1; } + + int tally = block + 1; + int last = inode; + + /* Only going through until we hit the requested node */ + while( tally > 0 ) + { + /* Count down to zero, when zero is hit, this is the node we want */ + tally--; + + if( !tally ) + { + return last; + } + else + { + switch( sector[(last << 1) + 1] ) + { + case BLOCK_LAST: + /* Last block, couldn't find block number */ + return -2; + case BLOCK_EMPTY: + /* Error, can't have free blocks! */ + return -2; + default: + last = sector[(last << 1) + 1]; + + /* Failed to point to valid next block */ + if( last < 5 || last >= 128 ) { return -3; } + break; + } + } + } + + /* Invalid filesystem */ + return -3; +} + +/** + * @brief Retrieve the sector number of the first valid TOC found + * + * @param[in] controller + * The controller (0-3) to inspect for a valid TOC + * + * @retval -2 the mempak was not inserted or was bad + * @retval -3 the mempak was unformatted or the header was invalid + * @retval 1 the first sector has a valid TOC + * @retval 2 the second sector has a valid TOC + */ +static int __get_valid_toc( mempak_structure_t *mpk ) +{ + /* We will need only one sector at a time */ + uint8_t data[MEMPAK_BLOCK_SIZE]; + + /* First check to see that the header block is valid */ + if( read_mempak_sector( mpk, 0, data ) ) + { + /* Couldn't read header */ + return -2; + } + + if( __validate_header( data ) ) + { + /* Header is invalid or unformatted */ + return -3; + } + + /* Try to read the first TOC */ + if( read_mempak_sector( mpk, 1, data ) ) + { + /* Couldn't read header */ + return -2; + } + + if( __validate_toc( data ) ) + { + /* First TOC is bad. Maybe the second works? */ + if( read_mempak_sector( mpk, 2, data ) ) + { + /* Couldn't read header */ + return -2; + } + + if( __validate_toc( data ) ) + { + /* Second TOC is bad, nothing good on this memcard */ + return -3; + } + else + { + /* Found a good TOC! */ + return 2; + } + } + else + { + /* Found a good TOC! */ + return 1; + } +} + +/** + * @brief Return whether a mempak is valid + * + * This function will return whether the mempak in a particular controller + * is formatted and valid. + * + * @param[in] controller + * The controller (0-3) to validate + * + * @retval 0 if the mempak is valid and ready to be used + * @retval -2 if the mempak is not present or couldn't be read + * @retval -3 if the mempak is bad or unformatted + */ +int validate_mempak( mempak_structure_t *mpk ) +{ + int toc = __get_valid_toc( mpk ); + + if( toc == 1 || toc == 2 ) + { + /* Found a valid TOC */ + return 0; + } + else + { + /* Pass on return code */ + return toc; + } +} + +/** + * @brief Read an entry on a mempak + * + * Given an entry index (0-15), return the entry as found on the mempak. If + * the entry is blank or invalid, the valid flag is cleared. + * + * @param[in] controller + * The controller (0-3) from which the entry should be read + * @param[in] entry + * The entry index (0-15) to read + * @param[out] entry_data + * Structure containing information on the entry + * + * @retval 0 if the entry was read successfully + * @retval -1 if the entry is out of bounds or entry_data is null + * @retval -2 if the mempak is bad or not present + */ +int get_mempak_entry( mempak_structure_t *mpk, int entry, entry_structure_t *entry_data ) +{ + uint8_t data[MEMPAK_BLOCK_SIZE]; + int toc; + + if( entry < 0 || entry > 15 ) { return -1; } + if( entry_data == 0 ) { return -1; } + + /* Make sure mempak is valid */ + if( (toc = __get_valid_toc( mpk )) <= 0 ) + { + /* Bad mempak or was removed, return */ + return -2; + } + + /* Entries are spread across two sectors, but we can luckly grab just one + with a single mempak read */ + memcpy(data, mpk->data + (3 * MEMPAK_BLOCK_SIZE) + (entry * 32), 32); +#if 0 + if( read_mempak_address( mpk, (3 * MEMPAK_BLOCK_SIZE) + (entry * 32), data ) ) + { + /* Couldn't read note database */ + return -2; + } +#endif + + if( __read_note( data, entry_data ) ) + { + /* Note is most likely empty, don't bother getting length */ + return 0; + } + + /* Grab the TOC sector */ + if( read_mempak_sector( mpk, toc, data ) ) + { + /* Couldn't read TOC */ + return -2; + } + + /* Get the length of the entry */ + int blocks = __get_num_pages( data, entry_data->inode ); + + if( blocks > 0 ) + { + /* Valid entry */ + entry_data->blocks = blocks; + entry_data->entry_id = entry; + return 0; + } + else + { + /* Invalid TOC */ + entry_data->valid = 0; + return 0; + } +} + +/** + * @brief Return the number of free blocks on a mempak + * + * Note that a block is identical in size to a sector. To calculate the number of + * bytes free, multiply the return of this function by #MEMPAK_BLOCK_SIZE. + * + * @param[in] controller + * The controller (0-3) to read the free space from + * + * @return The number of blocks free on the memory card or a negative number on failure + */ +int get_mempak_free_space( mempak_structure_t *mpk ) +{ + uint8_t data[MEMPAK_BLOCK_SIZE]; + int toc; + + /* Make sure mempak is valid */ + if( (toc = __get_valid_toc( mpk )) <= 0 ) + { + /* Bad mempak or was removed, return */ + return -2; + } + + /* Grab the valid TOC to get free space */ + if( read_mempak_sector( mpk, toc, data ) ) + { + /* Couldn't read TOC */ + return -2; + } + + return __get_free_space( data ); +} + +/** + * @brief Format a mempak + * + * Formats a mempak. Should only be done to wipe a mempak or to initialize + * the filesystem in case of a blank or corrupt mempak. + * + * @param[in] controller + * The controller (0-3) to format the mempak on + * + * @retval 0 if the mempak was formatted successfully + * @retval -2 if the mempak was not present or couldn't be formatted + */ +int format_mempak( mempak_structure_t *mpk ) +{ + /* Many mempak dumps exist online for users of emulated games to get + saves that have all unlocks. Every beginning sector on all these + was the same, so this is the data I use to initialize the first + sector */ + uint8_t sector[MEMPAK_BLOCK_SIZE] = { 0x81,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f, + 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, + 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0x05,0x1a,0x5f,0x13, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0x01,0xff,0x66,0x25,0x99,0xcd, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; + + if( write_mempak_sector( mpk, 0, sector ) ) + { + /* Couldn't write initial sector */ + return -2; + } + + /* Write out entry sectors, which can safely be zero */ + memset( sector, 0x0, MEMPAK_BLOCK_SIZE ); + if( write_mempak_sector( mpk, 3, sector ) || + write_mempak_sector( mpk, 4, sector ) ) + { + /* Couldn't write entry sectors */ + return -2; + } + + /* Go through, insert 'empty sector' marking on all entries */ + for( int i = 0; i < 128; i++ ) + { + sector[(i << 1) + 1] = BLOCK_EMPTY; + } + + /* Fix the checksum */ + sector[1] = __get_toc_checksum( sector ); + + /* Write out */ + if( write_mempak_sector( mpk, 1, sector ) || + write_mempak_sector( mpk, 2, sector ) ) + { + /* Couldn't write TOC sectors */ + return -2; + } + + return 0; +} + +/** + * @brief Read the data associated with an entry on a mempak + * + * Given a valid mempak entry fetched by get_mempak_entry, retrieves the contents + * of the entry. The calling function must ensure that enough room is available in + * the passed in buffer for the entire entry. The entry structure itself contains + * the number of blocks used to store the data which can be multiplied by + * #MEMPAK_BLOCK_SIZE to calculate the size of the buffer needed. + * + * @param[in] controller + * The controller (0-3) to read the entry data from + * @param[in] entry + * The entry structure associated with the data to be read. An entry + * structure can be fetched based on index using #get_mempak_entry + * @param[out] data + * The data associated with an entry + * + * @retval 0 if the entry was successfully read + * @retval -1 if input parameters were out of bounds or the entry was corrupted somehow + * @retval -2 if the mempak was not present or bad + * @retval -3 if the data couldn't be read + */ +int read_mempak_entry_data( mempak_structure_t *mpk, entry_structure_t *entry, uint8_t *data ) +{ + int toc; + uint8_t tocdata[MEMPAK_BLOCK_SIZE]; + + /* Some serious sanity checking */ + if( entry == 0 || data == 0 ) { return -1; } + if( entry->valid == 0 ) { return -1; } + if( entry->blocks == 0 || entry->blocks > 123 ) { return -1; } + if( entry->inode < BLOCK_VALID_FIRST || entry->inode > BLOCK_VALID_LAST ) { return -1; } + + /* Grab the TOC sector so we can get to the individual blocks the data comprises of */ + if( (toc = __get_valid_toc( mpk )) <= 0 ) + { + /* Bad mempak or was removed, return */ + return -2; + } + + /* Grab the valid TOC to get free space */ + if( read_mempak_sector( mpk, toc, tocdata ) ) + { + /* Couldn't read TOC */ + return -2; + } + + /* Now loop through blocks and grab each one */ + for( int i = 0; i < entry->blocks; i++ ) + { + int block = __get_note_block( tocdata, entry->inode, i ); + + if( read_mempak_sector( mpk, block, data + (i * MEMPAK_BLOCK_SIZE) ) ) + { + /* Couldn't read a sector */ + return -3; + } + } + + /* Fetched all blocks successfully */ + return 0; +} + +/** + * @brief Write associated data to a mempak entry + * + * Given a mempak entry structure with a valid region, name and block count, writes the + * entry and associated data to the mempak. This function will not overwrite any existing + * user data. To update an existing entry, use #delete_mempak_entry followed by + * #write_mempak_entry_data with the same entry structure. + * + * @param[in] controller + * The controller (0-3) to write the entry and data to + * @param[in] entry + * The entry structure containing a region, name and block count + * @param[in] data + * The associated data to write to to the created entry + * + * @retval 0 if the entry was created and written successfully + * @retval -1 if the parameters were invalid or the note has no length + * @retval -2 if the mempak wasn't present or was bad + * @retval -3 if there was an error writing to the mempak + * @retval -4 if there wasn't enough space to store the note + * @retval -5 if there is no room in the TOC to add a new entry + */ +int write_mempak_entry_data( mempak_structure_t *mpk, entry_structure_t *entry, uint8_t *data ) +{ + uint8_t sector[MEMPAK_BLOCK_SIZE]; + uint8_t tmp_data[32]; + int toc; + + /* Sanity checking on input data */ + if( !entry || !data ) { return -1; } + if( entry->blocks < 1 ) { return -1; } + if( __validate_region( entry->region ) ) { return -1; } + if( strlen( entry->name ) == 0 ) { return -1; } + + /* Grab valid TOC */ + if( (toc = __get_valid_toc( mpk )) <= 0 ) + { + /* Bad mempak or was removed, return */ + return -2; + } + + /* Grab the valid TOC to get free space */ + if( read_mempak_sector( mpk, toc, sector ) ) + { + /* Couldn't read TOC */ + return -2; + } + + /* Verify that we have enough free space */ + if( __get_free_space( sector ) < entry->blocks ) + { + /* Not enough space for note */ + return -4; + } + + /* Find blocks in TOC to allocate */ + int tally = entry->blocks; + uint8_t last = BLOCK_LAST; + + for( int i = BLOCK_VALID_FIRST; i <= BLOCK_VALID_LAST; i++ ) + { + if( sector[(i << 1) + 1] == BLOCK_EMPTY ) + { + /* We can use this block */ + tally--; + + /* Point this towards the next block */ + sector[(i << 1) + 1] = last; + + /* This block is now the last block */ + last = i; + } + + /* If we found all blocks, we can exit out early */ + if( !tally ) { break; } + } + + if( tally > 0 ) + { + /* Even though we had free space, couldn't get all blocks? */ + return -4; + } + else + { + /* Last now contains our inode */ + entry->inode = last; + entry->valid = 0; + entry->vendor = 0; + + /* A value observed in most games */ + entry->game_id = 0x4535; + } + + /* Loop through allocated blocks and write data to sectors */ + for( int i = 0; i < entry->blocks; i++ ) + { + int block = __get_note_block( sector, entry->inode, i ); + + if( write_mempak_sector( mpk, block, data + (i * MEMPAK_BLOCK_SIZE) ) ) + { + /* Couldn't write a sector */ + return -3; + } + } + + /* Find an empty entry to store to */ + for( int i = 0; i < 16; i++ ) + { + entry_structure_t tmp_entry; + + memcpy(tmp_data, mpk->data + (3 * MEMPAK_BLOCK_SIZE) + (i * 32), 32); +#if 0 + if( read_mempak_address( mpk, (3 * MEMPAK_BLOCK_SIZE) + (i * 32), tmp_data ) ) + { + /* Couldn't read note database */ + return -2; + } +#endif + /* See if we can write to this note */ + __read_note( tmp_data, &tmp_entry ); + if( tmp_entry.valid == 0 ) + { + entry->entry_id = i; + entry->valid = 1; + break; + } + } + + if( entry->valid == 0 ) + { + /* Couldn't find an entry */ + return -5; + } + + /* Update CRC on newly updated TOC */ + sector[1] = __get_toc_checksum( sector ); + + /* Write back to alternate TOC first before erasing the known valid one */ + if( write_mempak_sector( mpk, ( toc == 1 ) ? 2 : 1, sector ) ) + { + /* Failed to write alternate TOC */ + return -2; + } + + /* Write back to good TOC now that alternate is updated */ + if( write_mempak_sector( mpk, toc, sector ) ) + { + /* Failed to write alternate TOC */ + return -2; + } + + /* Convert entry structure to proper entry data */ + __write_note( entry, tmp_data ); + + /* Store entry to empty slot on mempak */ + memcpy(mpk->data + (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), tmp_data, 32); +#if 0 + if( write_mempak_address( mpk, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), tmp_data ) ) + { + /* Couldn't update note database */ + return -2; + } +#endif + + return 0; +} + +/** + * @brief Delete a mempak entry and associated data + * + * Given a valid mempak entry fetched by #get_mempak_entry, removes the entry and frees + * all associated blocks. + * + * @param[in] controller + * The controller (0-3) to delete the note from + * @param[in] entry + * The entry structure that is to be deleted from the mempak + * + * @retval 0 if the entry was deleted successfully + * @retval -1 if the entry was invalid + * @retval -2 if the mempak was bad or not present + */ +int delete_mempak_entry( mempak_structure_t *mpk, entry_structure_t *entry ) +{ + entry_structure_t tmp_entry; + uint8_t data[MEMPAK_BLOCK_SIZE]; + int toc; + + /* Some serious sanity checking */ + if( entry == 0 ) { return -1; } + if( entry->valid == 0 ) { return -1; } + if( entry->entry_id > 15 ) { return -1; } + if( entry->inode < BLOCK_VALID_FIRST || entry->inode > BLOCK_VALID_LAST ) { return -1; } + + /* Ensure that the entry passed in matches what's on the mempak */ + memcpy(data, mpk->data + 3 * MEMPAK_BLOCK_SIZE + entry->entry_id * 32, 32); +#if 0 + if( read_mempak_address( mpk, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) + { + /* Couldn't read note database */ + return -2; + } +#endif + + if( __read_note( data, &tmp_entry ) ) + { + /* Couldn't parse entry, can't be valid */ + return -2; + } + + if( tmp_entry.inode != entry->inode ) + { + /* inodes don't match, not the same entry! */ + return -2; + } + + /* The entry matches, so blank it */ + memset( mpk->data + (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), 0, 32 ); +#if 0 + memset( data, 0, 32 ); + if( write_mempak_address( mpk, (3 * MEMPAK_BLOCK_SIZE) + (entry->entry_id * 32), data ) ) + { + /* Couldn't update note database */ + return -2; + } +#endif + /* Grab the first valid TOC entry */ + if( (toc = __get_valid_toc( mpk )) <= 0 ) + { + /* Bad mempak or was removed, return */ + return -2; + } + + /* Grab the valid TOC to erase sectors */ + if( read_mempak_sector( mpk, toc, data ) ) + { + /* Couldn't read TOC */ + return -2; + } + + /* Erase all blocks out of the TOC */ + int tally = 0; + int done = 0; + int last = entry->inode; + + /* Don't want to recurse forever if filesystem is corrupt */ + while( tally <= 123 && !done ) + { + tally++; + + switch( data[(last << 1) + 1] ) + { + case BLOCK_LAST: + /* Last block, we are done */ + data[(last << 1) + 1] = BLOCK_EMPTY; + + done = 1; + break; + case BLOCK_EMPTY: + /* Error, can't have free blocks! */ + return -2; + default: + { + int next = data[(last << 1) + 1]; + data[(last << 1) + 1] = BLOCK_EMPTY; + last = next; + + /* Failed to point to valid next block */ + if( last < 5 || last >= 128 ) { return -3; } + break; + } + } + } + + /* Update CRC on newly updated TOC */ + data[1] = __get_toc_checksum( data ); + + /* Write back to alternate TOC first before erasing the known valid one */ + if( write_mempak_sector( mpk, ( toc == 1 ) ? 2 : 1, data ) ) + { + /* Failed to write alternate TOC */ + return -2; + } + + /* Write back to good TOC now that alternate is updated */ + if( write_mempak_sector( mpk, toc, data ) ) + { + /* Failed to write alternate TOC */ + return -2; + } + + return 0; +} + +/** @} */ /* controller */ diff --git a/tool/n64savetool/mempak_fs.h b/tool/n64savetool/mempak_fs.h new file mode 100644 index 0000000..a3b0b6b --- /dev/null +++ b/tool/n64savetool/mempak_fs.h @@ -0,0 +1,80 @@ +/** + * @file mempak.h + * @brief Mempak Filesystem Routines + * @ingroup mempak + */ +#ifndef __LIBDRAGON_MEMPAK_H +#define __LIBDRAGON_MEMPAK_H + +#include "mempak.h" +#include + +/** + * @addtogroup mempak + * @{ + */ + +/** @brief Size in bytes of a Mempak block */ +#define MEMPAK_BLOCK_SIZE 256 + +/** + * @brief Structure representing a save entry in a mempak + */ +typedef struct entry_structure +{ + /** @brief Vendor ID */ + uint32_t vendor; + /** @brief Game ID */ + uint16_t game_id; + /** @brief Inode pointer */ + uint16_t inode; + /** @brief Intended region */ + uint8_t region; + /** @brief Number of blocks used by this entry. + * @see MEMPAK_BLOCK_SIZE */ + uint8_t blocks; + /** @brief Validity of this entry. */ + uint8_t valid; + /** @brief ID of this entry */ + uint8_t entry_id; + /** + * @brief Name of this entry + * + * The complete list of valid ASCII characters in a note name is: + * + *
+     * ABCDEFGHIJKLMNOPQRSTUVWXYZ!"#`*+,-./:=?\@
+     * 
+ * + * The space character is also allowed. Any other character will be + * converted to a space before writing to the mempak. + * + * @see #__n64_to_ascii and #__ascii_to_n64 + */ + char name[19]; + + /** @brief A copy of the raw note data (from the note table). */ + unsigned char raw_data[32]; +} entry_structure_t; + +#ifdef __cplusplus +extern "C" { +#endif + +int read_mempak_sector( mempak_structure_t *pak, int sector, uint8_t *sector_data ); +int write_mempak_sector( mempak_structure_t *pak, int sector, uint8_t *sector_data ); +int validate_mempak( mempak_structure_t *pak ); +int get_mempak_free_space( mempak_structure_t *pak ); +int get_mempak_entry( mempak_structure_t *pak, int entry, entry_structure_t *entry_data ); +int format_mempak( mempak_structure_t *pak ); +int read_mempak_entry_data( mempak_structure_t *pak, entry_structure_t *entry, uint8_t *data ); +int write_mempak_entry_data( mempak_structure_t *pak, entry_structure_t *entry, uint8_t *data ); +int delete_mempak_entry( mempak_structure_t *pak, entry_structure_t *entry ); + +#ifdef __cplusplus +} +#endif + +/** @} */ /* mempak */ + +#endif diff --git a/tool/n64savetool/mempak_ls.c b/tool/n64savetool/mempak_ls.c new file mode 100644 index 0000000..d28af6b --- /dev/null +++ b/tool/n64savetool/mempak_ls.c @@ -0,0 +1,57 @@ +#include +#include "mempak.h" + +int main(int argc, char **argv) +{ + const char *infile; + mempak_structure_t *mpk; + int note; + int res; + + if (argc < 2) { + printf("Usage: ./mempak_ls file\n"); + printf("\n"); + printf("Raw files (.MPK, .BIN) and Dexdrive (.N64) formats accepted.\n"); + return 1; + } + + infile = argv[1]; + mpk = mempak_loadFromFile(infile); + if (!mpk) { + return 1; + } + + printf("Mempak image loaded. Image type %d\n", mpk->source); + + if (0 != validate_mempak(mpk)) { + printf("Mempak invalid (not formatted or corrupted)\n"); + goto done; + } + + printf("Mempak content is valid\n"); + + for (note = 0; note