Skip to content
bryc edited this page Apr 26, 2021 · 4 revisions

Page 0

Notes about researching the data found in Page 0. But the graphic is quite useful to detail its overall structure. To see the inner format of the 32-byte ID area block, see "Checksummed ID Area".

image

The above graphic shows the general structure of page 0, the "ID Area". It describes what data is typically found in each section based on analyzing hundreds of example files. Some areas are absolutely critical, some are backups. Some areas sometimes contain meaningless garbage data, at varying frequencies. The rationale for this was to determine if any of these areas could be re-purposed for custom data. The result is "Probably, but it might get overwritten in some edge cases or data failures".

There are 64 bytes that are 98% reliable for extension in this area. If you count the third area, then there are 96 bytes that are about 95% reliable for extension. A full area of 128 bytes is possible, but the label area is not reliable, especially the first byte. 224 bytes could potentially be possible, by overwriting back-up slots. However if a repair operation occurs, it will overwrite the backups.

The label area should be initialized as 0. Factory-sealed Paks seem to leave this at 0. The filled value is only due to pfsSetLabel being used in rare cases in some games. All other unused areas should be left to 0 as well, unless I can find a way to use it for additional storage of some stuff.

"Checksummed ID Area" (__OSPackId)

typedef struct {
    /* 0x0 */ u32 repaired;
    /* 0x4 */ u32 random;
    /* 0x8 */ u64 serial_mid;
    /* 0x10 */ u64 serial_low;
    /* 0x18 */ u16 deviceid;
    /* 0x1A */ u8 banks;
    /* 0x1B */ u8 version;
    /* 0x1C */ u16 checksum;
    /* 0x1E */ u16 inverted_checksum;
} __OSPackId;

Note: The possibility of repaired and random being serial_high, is plausible. Some DexDrive files show this.

image

  • banks has to be exactly 0x01. It is an integer that defines the capacity of the MPK. A value higher than 0x01 implies larger than 32K of space - which was never commercially available. It seems games will not behave if this value is higher (but this could be investigated).
  • deviceid has to be at least 00 01. It acts as a bitfield. If bit 1 is not set ((id->deviceid & 0x01) == 0), an error occurs and osRepairPackId is executed. libultra itself does not use it for anything, it simply checks for this bit.
  • version is not used or validated, aside to being copied during osPfsInitPak (pfs->version = 0, struct's initialized value) or osPfsRepairId (newid->version = badid->version).
  • serial_low/serial_mid are not used by libultra (aside from being copied during osPfsRepairId). however the SDK mentions each Controller Pak has a unique serial number here (assigned by Nintendo), and that it is used for insertion/removal. But the "Controller Pak Library" doesn't seem to check it in any way. Their initial value is 0 in the struct, which may or may not be responsible for some values turning into 0.
  • repaired. This is set to -1 (FF FF FF FF) in __osRepairPackId, indicating a repair was done on the pak.
  • random is not actually random, it just represents the current value of the CP0 Count Register, as obtained by osGetCount. It is an unsigned 32-bit int.
  • checksum/inverted_checksum are checksums which sum the preceding 14 words (in 16-bit chunks).
// for reference: native implementation of osidchecksum.
s32 __osIdCheckSum(u16 *ptr, u16 *csum, u16 *icsum) {
    u16 data;
    u32 j;
    data = 0;
    *icsum = 0;
    *csum = *icsum;
    for (j = 0; j < 28; j += 2) {
        data = *(u16 *)((u8 *)ptr + j);
        *csum += data;
        *icsum += ~data;
    }
    return 0;
}

Note Table / Note Directory (__OSDir)

typedef struct {
    /* 0x0 */ u32 game_code;
    /* 0x4 */ u16 company_code;
    /* 0x6 */ __OSInodeUnit start_page;
    /* 0x8 */ u8 status;
    /* 0x9 */ s8 reserved;
    /* 0xA */ u16 data_sum;
    /* 0xC */ u8 ext_name[PFS_FILE_EXT_LEN];
    /* 0x10 */ u8 game_name[PFS_FILE_NAME_LEN];
} __OSDir;
  • The OSDir status byte (dir.status) should not be confused with the OSPfs status byte (pfs.status).
  • dir.status only has one flag, known as "DIR_STATUS_OCCUPIED" or "DIR_WRITTEN", which has a value of 0x02. This bit is checked and written in osPfsReadWriteFile. This byte gets set to 0 ("DIR_STATUS_EMPTY") if the note is deleted or the file system becomes corrupted.
  • Other bits are often set in dir.status and dir.reserved. They serve no purpose and are likely just garbage data leftover from function calls or uninitialized variables. However I haven't traced the origins to be sure. dir.reserved is never used.
  • dir.data_sum is initialized to 0 in osPfsAllocateFile, which means it is written to 0 during each write? A checksum of save data is never calculated and this value is not checked.
  • game_code and company_code cannot have the value 0, as this is used to indicate a note does not exist (as per condition (company_code != 0) && (game_code != 0))

Various snippets regarding Status bits:

if (flag == PFS_WRITE && !(dir.status & PFS_WRITTEN)) 
    dir.status |= PFS_WRITTEN;
////////////////////////////////////////////////////////
    if ((flag == PFS_READ) && ((dir.status & PFS_WRITTEN) == 0))
        return(PFS_ERR_BAD_DATA);
////////////////////////////////////////////////////////

Inode Table stuff

// __OSInodeUnit
typedef union {
    /* 0x0 */ struct
    {
        /* 0x0 */ u8 bank;
        /* 0x1 */ u8 page;
    } inode_t;
    /* 0x0 */ u16 ipage;
} __OSInodeUnit;


// contpfs.c
// 8-bit inode checksum gets written this way
inode->inode_page[0].inode_t.page = __osSumcalc((u8*)&inode->inode_page[offset], (-offset) * 2 + 256);

// 8-bit inode checksum gets checked this way
sum = __osSumcalc((u8*)&inode->inode_page[offset], (-offset) * 2 + 256);
if (sum != inode->inode_page[0].inode_t.page) { }


// inode and minode written this way
ret = __osContRamWrite(pfs->queue, pfs->channel, pfs->inode_table + bank * 8 + j, addr, FALSE);
ret = __osContRamWrite(pfs->queue, pfs->channel, pfs->minode_table + bank * 8 + j, addr, FALSE);