Line 1: |
Line 1: |
| This page describes the format and encryption of savegames contained in gamecards, SD and NAND. You can find savegames from various 3DS games on the [[Games]] page. | | This page describes the format and encryption of savegames contained in gamecards, SD and NAND. You can find savegames from various 3DS games on the [[Games]] page. |
− |
| |
− | This page does not describe [[DISA and DIFF|DISA container format]], which all savegames use as wrappers.
| |
− |
| |
− | All data in this page is little-endian unless otherwise specified. All "unused / padding" fields can contain uninitialized data unless otherwise specified.
| |
| | | |
| == Overview == | | == Overview == |
− | Savegames are stored in [[DISA and DIFF|DISA container format]] (follow this link for the container format description). It forms a file system inside the inner content of the container. In this page only the inner file system format of the content is described. | + | Savegames are stored in [[DISA and DIFF|DISA container format]]. Inside the DISA container, it forms a [[Inner FAT|FAT filesystem]]. '''Please refer to these pages for how to fully extract save files'''. This page only describes additional encryption wear leveling on top of the DISA container. These layers only apply to gamecard save games. SD savegames and NAND savegames are DISA containers in plaintext after decrypting the common SD/NAND encryption layer. |
− | | |
− | Unlike SD and NAND savegames, gamecard savegames has additional encryption + wear leveling layer. They are described in the following sections.
| |
| | | |
| == Gamecard savegame Encryption == | | == Gamecard savegame Encryption == |
Line 104: |
Line 98: |
| * each byte is the checksum of an encrypted 0x200 bytes large block | | * each byte is the checksum of an encrypted 0x200 bytes large block |
| * to calculate the checksum, a CRC16 of the block (with starting value 0xFFFF) is calculated, and the two bytes of the CRC16 are XORed together to produce the 8bit checksum | | * to calculate the checksum, a CRC16 of the block (with starting value 0xFFFF) is calculated, and the two bytes of the CRC16 are XORed together to produce the 8bit checksum |
− |
| |
− | == Components and partitions ==
| |
− |
| |
− | A savegame, after unwrapping the DISA container, consists of the following components:
| |
− |
| |
− | * SAVE header
| |
− | * directory hash table
| |
− | * file hash table
| |
− | * file allocation table
| |
− | * directory entry table
| |
− | * file entry table
| |
− | * data region
| |
− |
| |
− | A DISA container can have one or two partitions, and correspondingly a savegame has two possible layouts. The layout is determined by the parameter <code>duplicate data</code> passed in [[FS:FormatSaveData]] or [[FS:CreateSystemSaveData]].
| |
− |
| |
− | === Layout for <code>duplicate data = true</code> ===
| |
− |
| |
− | The DISA container only has one partition which is always configured as external IVFC level 4 disabled (see [[DISA and DIFF|DISA format for details]]). All components are stored in this partition as
| |
− |
| |
− | * SAVE header at the beginning
| |
− | * directory hash table
| |
− | * file hash table
| |
− | * file allocation table
| |
− | * data region
| |
− | ** directory entry table is allocated inside data region
| |
− | ** file entry table as well
| |
− | ** all file data is also allocated here
| |
− |
| |
− | In this layout, all data is duplicated by DISA's DPFS tree, which is what the parameter <code>duplicate data</code> implies.
| |
− |
| |
− | === Layout for <code>duplicate data = false</code> ===
| |
− |
| |
− | The DISA container has two partitions. Partition A is always configured as external IVFC level 4 disabled, and partition B is configured as it enabled. Components are stored among the two partitions as
| |
− |
| |
− | * Partition A
| |
− | ** SAVE header at the beginning.
| |
− | ** directory hash table
| |
− | ** file hash table
| |
− | ** file allocation table
| |
− | ** directory entry table
| |
− | ** file entry table
| |
− | * Partition B
| |
− | ** used as data region entirely, and only has file data allocated.
| |
− |
| |
− | In this layout, all file system metadata is duplicated by partition A DPFS tree, but file data is not as partition B has external IVFC level 4.
| |
− |
| |
− | === SAVE Header ===
| |
− |
| |
− | The SAVE header defines the rest components of the savegame. All "offsets" in the table below are relative to the beginning of partition A (inner content), while all "starting block index" are relative to the beginning of data region.
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | Magic "SAVE"
| |
− | |-
| |
− | | 0x04
| |
− | | 4
| |
− | | Magic 0x40000
| |
− | |-
| |
− | | 0x08
| |
− | | 8
| |
− | | File system Information offset (0x20)
| |
− | |-
| |
− | | 0x10
| |
− | | 8
| |
− | | Image size in blocks
| |
− | |-
| |
− | | 0x18
| |
− | | 4
| |
− | | Image block size
| |
− | |-
| |
− | | 0x1C
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | |
| |
− |
| |
− | |
| |
− |
| |
− | | Below is File system Information
| |
− | |-
| |
− | | 0x20
| |
− | | 4
| |
− | | Unknown
| |
− | |-
| |
− | | 0x24
| |
− | | 4
| |
− | | Data region block size
| |
− | |-
| |
− | | 0x28
| |
− | | 8
| |
− | | Directory hash table offset
| |
− | |-
| |
− | | 0x30
| |
− | | 4
| |
− | | Directory hash table bucket count
| |
− | |-
| |
− | | 0x34
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x38
| |
− | | 8
| |
− | | File hash table offset
| |
− | |-
| |
− | | 0x40
| |
− | | 4
| |
− | | File hash table bucket count
| |
− | |-
| |
− | | 0x44
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x48
| |
− | | 8
| |
− | | File allocation table offset
| |
− | |-
| |
− | | 0x50
| |
− | | 4
| |
− | | File allocation table entry count
| |
− | |-
| |
− | | 0x54
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x58
| |
− | | 8
| |
− | | Data region offset (if no partition B)
| |
− | |-
| |
− | | 0x60
| |
− | | 4
| |
− | | Data region block count (= File allocation table entry count)
| |
− | |-
| |
− | | 0x64
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x68
| |
− | | 8
| |
− | | If partition B exists: directory entry table offset;
| |
− | |-
| |
− | |
| |
− |
| |
− | |
| |
− |
| |
− | | otherwise: u32 directory entry table starting block index + u32 directory entry table block count
| |
− | |-
| |
− | | 0x70
| |
− | | 4
| |
− | | Maximum directory count
| |
− | |-
| |
− | | 0x74
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x78
| |
− | | 8
| |
− | | If partition B exists: file entry table offset;
| |
− | |-
| |
− | |
| |
− |
| |
− | |
| |
− |
| |
− | | otherwise: u32 file entry table starting block index + u32 file entry table block count
| |
− | |-
| |
− | | 0x80
| |
− | | 4
| |
− | | Maximum file count
| |
− | |-
| |
− | | 0x84
| |
− | | 4
| |
− | | Padding
| |
− | |}
| |
− |
| |
− | * The file/directory bucket count & maximum count are specified by the parameters of [[FS:FormatSaveData]] or [[FS:CreateSystemSaveData]].
| |
− | * When partition B doesn't exist, directory & file entry tables are allocated in the data region, and while be marked allocated in file allocation table as if they are two normal files. However, only continuous allocation has been observed, so directly reading <code>block_count * block_size</code> bytes from <code>data_region + starting_block_index * block_size</code> should be safe. See the section [[#File Allocation Table]] below for more information.
| |
− |
| |
− | === Directory Entry Table ===
| |
− |
| |
− | The directory entry table is an array of the entry type shown below. It describes the directory hierarchy of the file system.
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | Parent directory index. 0 for root
| |
− | |-
| |
− | | 0x04
| |
− | | 16
| |
− | | ASCII directory name in. All zero for root
| |
− | |-
| |
− | | 0x14
| |
− | | 4
| |
− | | Next sibling directory index. 0 if this is the last one
| |
− | |-
| |
− | | 0x18
| |
− | | 4
| |
− | | First subdirectory index. 0 if not exists
| |
− | |-
| |
− | | 0x1C
| |
− | | 4
| |
− | | First file index in file entry table. 0 for empty directory
| |
− | |-
| |
− | | 0x20
| |
− | | 4
| |
− | | Padding / zero?
| |
− | |-
| |
− | | 0x24
| |
− | | 4
| |
− | | Index of the next directory in the same hash table bucket. 0 if this is the last one
| |
− | |}
| |
− |
| |
− | There are also some dummy entries in the array:
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | Current Total entry count
| |
− | |-
| |
− | | 0x04
| |
− | | 4
| |
− | | Maximum entry count = maximum directory count + 2
| |
− | |-
| |
− | | 0x08
| |
− | | 28
| |
− | | Padding / All zero
| |
− | |-
| |
− | | 0x24
| |
− | | 4
| |
− | | Index of the next dummy entry. 0 if this is the last one
| |
− | |}
| |
− |
| |
− | The 0-th entry of the array is always a dummy entry, which functions as the head of the dummy entry linked list. The 1-st entry of the array is always the root. Therefore maximum entry count is two more than maximum directory count. Dummy entries are left there when deleting directories, and reserved for future use.
| |
− |
| |
− | === File Entry Table ===
| |
− |
| |
− | The file entry table is an array of the entry type shown below. It contains information for each file.
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | Parent directory index in directory entry table
| |
− | |-
| |
− | | 0x04
| |
− | | 16
| |
− | | ASCII file name
| |
− | |-
| |
− | | 0x14
| |
− | | 4
| |
− | | Next sibling file index. 0 if this is the last one
| |
− | |-
| |
− | | 0x18
| |
− | | 4
| |
− | | Padding
| |
− | |-
| |
− | | 0x1C
| |
− | | 4
| |
− | | First block index in data region. 0x80000000 if the file is just created and has no data.
| |
− | |-
| |
− | | 0x20
| |
− | | 8
| |
− | | File Size
| |
− | |-
| |
− | | 0x28
| |
− | | 4
| |
− | | Padding?
| |
− | |-
| |
− | | 0x2C
| |
− | | 4
| |
− | | Index of the next file in the same hash table bucket. 0 if this is the last one
| |
− | |}
| |
− |
| |
− | Like directory entry table, file entry table also has some dummy entries:
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | Current total entry count
| |
− | |-
| |
− | | 0x04
| |
− | | 4
| |
− | | Maximum entry count = maximum file count + 1
| |
− | |-
| |
− | | 0x08
| |
− | | 36
| |
− | | Padding / All zero
| |
− | |-
| |
− | | 0x2C
| |
− | | 4
| |
− | | Index of the next dummy entry. 0 if this is the last one
| |
− | |}
| |
− |
| |
− | The 0-th entry of the array is always a dummy entry, which functions as the head of the dummy entry linked list. Therefore maximum entry count is one more than maximum file count. Dummy entries are left there when deleting files, and reserved for future use.
| |
− |
| |
− | === Directory Hash Table & File Hash Table ===
| |
− |
| |
− | This is a u32 array of size = bucket count, each of which is an index to the directory / file entry table. The directory / file name is hashed and its entry index is put to the corresponding bucket. If there is already a directory/file entry in the bucket, then it appends to the linked list formed by <code>Index of the next directory/file in the same hash table bucket</code> field in the directory/file entry table. i.e. this is a hash table using separate chaining with linked lists
| |
− |
| |
− | The hash function takes the parent index and the name as key. The function is equivalent to
| |
− |
| |
− | <pre>uint32_t GetBucket(
| |
− | char name[16], // takes all 16 bytes including trailing zeros
| |
− | uint32_t parent_dir_index,
| |
− | uint32_t bucket_count
| |
− | ) {
| |
− | uint32_t hash = parent_dir_index ^ 0x091A2B3C;
| |
− | for (int i = 0; i < 4; ++i) {
| |
− | hash = (hash >> 1) | (hash << 31);
| |
− | hash ^= (uint32_t)name[i * 4]
| |
− | hash ^= (uint32_t)name[i * 4 + 1] << 8
| |
− | hash ^= (uint32_t)name[i * 4 + 2] << 16
| |
− | hash ^= (uint32_t)name[i * 4 + 3] << 24
| |
− | }
| |
− | return hash % bucket_count;
| |
− | }
| |
− | </pre>
| |
− | === File Allocation Table ===
| |
− |
| |
− | The file allocation table is an array of a 8-byte entry shown below. The array size is actually ''one larger than'' the size recorded in the SAVE header. Each entry corresponds to a block in the data region (the block size is defined in SAVE header). However, the 0th entry corresponds to nothing, so the corresponding block index is off by one. e.g. entry 31 in this table corresponds to block 30 in the data region.
| |
− |
| |
− | {| class="wikitable" border="1"
| |
− | ! Offset
| |
− | ! Length
| |
− | ! Description
| |
− | |-
| |
− | | 0x00
| |
− | | 4
| |
− | | bit[0:30]: Index U; bit[31]: Flag U
| |
− | |-
| |
− | | 0x04
| |
− | | 4
| |
− | | bit[0:30]: Index V; bit[31]: Flag V
| |
− | |}
| |
− |
| |
− | Entries in this table forms several chains, representing how blocks in the data region should be linked together. However, unlike normal FAT systems, which uses chains of entries, 3DS savegames use chain of ''nodes''. Each node spans one or multiple entries.
| |
− |
| |
− | One node spanning <code>n</code> entries starting from <code>FAT[k]</code> is in the following format:
| |
− |
| |
− | <pre>FAT[k + 0]:
| |
− | Index_U = index of the first entry of the previous node. 0 if this is the first node.
| |
− | Index_V = index of the first entry of the next node. 0 if this is the last node.
| |
− | Flag_U set if this is the first node.
| |
− | Flag_V set if this node has multiple entries.
| |
− |
| |
− | FAT[k + 1]:
| |
− | Index_U = k (the first entry index of this node)
| |
− | Index_V = k + n - 1 (the last entry index of this node)
| |
− | Flag_U always set
| |
− | Flag_V always clear
| |
− |
| |
− | FAT[k + 2] ~ FAT[k + n - 2]:
| |
− | All these entries are uninitialized
| |
− |
| |
− | FAT[k + n - 1]:
| |
− | Index_U = k
| |
− | Index_V = k + n - 1
| |
− | Flag_U always set
| |
− | Flag_V always clear
| |
− | (Same values as FAT[k + 1])
| |
− | </pre>
| |
− | * Note: all indices above are entry indices (block index + 1)
| |
− |
| |
− | All free blocks that are not allocated to any files also form a node chain in the allocation table. The head index of this "free chain" is recorded in <code>FAT[0].Index_V</code>. Other fields of <code>FAT[0]</code> are all zero
| |
− |
| |
− | Here is an example: [https://raw.githubusercontent.com/wwylele/3ds-save-tool/master/disa-fat.png]
| |
| | | |
| == Initialization == | | == Initialization == |