Difference between revisions of "Savegames"

From Nintendo Switch Brew
Jump to navigation Jump to search
(Remap Storage)
(→‎Extra data: prior save struct link fix pointed to wrong struct)
 
(17 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
This page describes the format of save files contained in NAND. These files are stored as completely unencrypted, plaintext data. Save files are not cleared upon creation, resulting in possible garbage data in unused portions of the container.
 
This page describes the format of save files contained in NAND. These files are stored as completely unencrypted, plaintext data. Save files are not cleared upon creation, resulting in possible garbage data in unused portions of the container.
  
== Main header ==
+
= Main header =
  
 
The header is 0x4000 bytes long.
 
The header is 0x4000 bytes long.
  
 
There are 2 headers stored at 0x0 and 0x4000, presumably for commit and rollback purposes.
 
There are 2 headers stored at 0x0 and 0x4000, presumably for commit and rollback purposes.
 +
 +
Decimal versions are separated as Major, Minor, Micro, and Bugfix with each using one byte. e.g. version 3.4.5.6 would be 0x03040506.
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 64: Line 66:
 
The additional storage at the end of the header is used to store any extra header data. This data's structure is determined by offsets stored in the main part of the header.
 
The additional storage at the end of the header is used to store any extra header data. This data's structure is determined by offsets stored in the main part of the header.
  
=== AES CMAC header ===
+
== AES CMAC header ==
 +
 
 +
This is internally referred to as MasterHeaderMac.
 +
 
 
{| class="wikitable"
 
{| class="wikitable"
 
|-
 
|-
Line 82: Line 87:
 
The final CMAC key used for this is generated using GenerateAesKek with a kek source and the device key, along with and LoadAesKey and a set key seed.
 
The final CMAC key used for this is generated using GenerateAesKek with a kek source and the device key, along with and LoadAesKey and a set key seed.
  
=== DISF ===
+
== DISF ==
  
 
This section contains information about the structure of the save file.
 
This section contains information about the structure of the save file.
Line 98: Line 103:
 
| 0x004
 
| 0x004
 
| 4
 
| 4
| Version? (Upper 2 bytes must be 0x0004)
+
| Version (Major version must be 4 or 5. Only system version 5.0.0+ can read version 5 save files)
 
|-
 
|-
 
| 0x008
 
| 0x008
Line 260: Line 265:
 
| Index of the active duplex master bitmap
 
| Index of the active duplex master bitmap
 
|-
 
|-
| 0x200
+
| 0x160
|
 
| End
 
|}
 
 
 
=== Duplex header ===
 
 
 
* Block sizes are stored as powers of 2
 
 
 
{| class="wikitable"
 
|-
 
! Start
 
! Length
 
! Description
 
|-
 
| 0x00
 
| 4
 
| Magic ("DPFS")
 
|-
 
| 0x04
 
| 4
 
| Version? (Upper 2 bytes must be 0x0001)
 
|-
 
| 0x08
 
 
| 8
 
| 8
| Master bitmap offset
+
| [5.0.0+] File allocation table IVFC master hash offset A
 
|-
 
|-
| 0x10
+
| 0x168
 
| 8
 
| 8
| Master bitmap size
+
| [5.0.0+] File allocation table IVFC master hash offset B
|-
 
| 0x18
 
| 4
 
| Master bitmap block size power
 
 
|-
 
|-
| 0x1C
+
| 0x170
 
| 8
 
| 8
| Level 1 offset
+
| [5.0.0+] File allocation table IVFC level 1 virtual offset
 
|-
 
|-
| 0x24
+
| 0x178
 
| 8
 
| 8
| Level 1 size
+
| [5.0.0+] File allocation table IVFC level 1 size
|-
 
| 0x2C
 
| 4
 
| Level 1 block size power
 
 
|-
 
|-
| 0x30
+
| 0x180
 
| 8
 
| 8
| Level 2 offset
+
| [5.0.0+] File allocation table IVFC level 2 virtual offset
 
|-
 
|-
| 0x38
+
| 0x188
 
| 8
 
| 8
| Level 2 size
+
| [5.0.0+] File allocation table IVFC level 2 size
|-
 
| 0x40
 
| 4
 
| Level 2 block size power
 
 
|-
 
|-
 +
| 0x200
 +
|
 +
| End
 
|}
 
|}
  
=== Integrity verification header ===
+
== Integrity verification header ==
  
 
* Offsets for levels 1-3 come from the metadata remap storage
 
* Offsets for levels 1-3 come from the metadata remap storage
Line 339: Line 312:
 
| 0x04
 
| 0x04
 
| 4
 
| 4
| Version? (Upper 2 bytes must be 0x0002)
+
| Version (0.2.x.x)
 
|-
 
|-
 
| 0x08
 
| 0x08
Line 359: Line 332:
 
|}
 
|}
  
==== Level information ====
+
=== Level information ===
  
 
* 0x18 bytes long
 
* 0x18 bytes long
Line 388: Line 361:
 
|}
 
|}
  
=== Journal header ===
+
== Journal header ==
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 402: Line 375:
 
| 0x04
 
| 0x04
 
| 4
 
| 4
| Version? (Must be 0x10000 or less)
+
| Version (Must be 0.0.x.x or 0.1.0.0)
 
|-
 
|-
 
| 0x08
 
| 0x08
Line 415: Line 388:
 
| 8
 
| 8
 
| Block size
 
| Block size
|-
 
|
 
|
 
| The below fields are treated as a separate subheader
 
 
|-
 
|-
 
| 0x20
 
| 0x20
| 4
+
| 16
| Version? (Must be 0 or 1)
+
| Journal map header
|-
 
| 0x24
 
| 4
 
| Main data block count
 
|-
 
| 0x28
 
| 8
 
| Journal block count
 
 
|-
 
|-
 
| 0x200
 
| 0x200
Line 438: Line 399:
 
|}
 
|}
  
=== Save FS header ===
+
=== Journal map header ===
 
 
* Structure is different than 3DS.
 
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 450: Line 409:
 
| 0x00
 
| 0x00
 
| 4
 
| 4
| Magic ("SAVE")
+
| Version (Stored as a normal 32-bit integer. Must be 0 or 1)
 
|-
 
|-
 
| 0x04
 
| 0x04
 
| 4
 
| 4
| Version? (Upper 2 bytes must be 0x0006)
+
| Main data block count
 
|-
 
|-
 
| 0x08
 
| 0x08
| 8
+
| 4
| Number of blocks. Does not change if save file is resized.
+
| Journal block count
 +
|-
 +
| 0x0C
 +
| 4
 +
| Padding
 +
|-
 +
|}
 +
 
 +
== Extra data ==
 +
 
 +
{| class="wikitable"
 
|-
 
|-
| 0x10
+
! Start
| 8
+
! Length
| Block Size
+
! Description
 
|-
 
|-
|  
+
| 0x00
|  
+
| 0x40
| The below fields are treated as a separate subheader
+
| [[Filesystem_services#SaveDataAttribute|SaveDataAttribute]]
 
|-
 
|-
| 0x18
+
| 0x40
 
| 8
 
| 8
| Block size
+
| Save owner ID
 
|-
 
|-
| 0x20
+
| 0x48
 
| 8
 
| 8
| FAT offset
+
| Timestamp
 
|-
 
|-
| 0x28
+
| 0x50
 
| 4
 
| 4
| FAT entry count
+
| Flags?
 
|-
 
|-
| 0x2C
+
| 0x54
 
| 4
 
| 4
| Padding
+
| Unused?
 
|-
 
|-
| 0x30
+
| 0x58
 
| 8
 
| 8
| Data offset
+
| Size of usable save data
 
|-
 
|-
| 0x38
+
| 0x60
| 4
 
| Data block count
 
|-
 
| 0x3C
 
| 4
 
| Padding
 
|-
 
| 0x40
 
 
| 8
 
| 8
| Directory table block index
+
| Journal size
 
|-
 
|-
| 0x48
+
| 0x68
 
| 8
 
| 8
| File table block index
+
| Commit ID
 
|-
 
|-
 +
| 0x200
 +
|
 +
| End
 
|}
 
|}
  
=== Remap storage header ===
+
= Remap Storage =
 +
 
 +
Remap Storage is used to remap segments of data from virtual offsets to physical offsets. This allows extending the save file without having to relocate existing data.
 +
 
 +
Each Remap Storage has three components: [[#Remap storage header|a header]], a remapping table, and the main data storage.
 +
 
 +
A remap storage can contain a varying number of segments, each representing a chunk of contiguous virtual storage. A segment can be composed of one or more entries. Each of these entries are mapped from their virtual locations to their physical locations by entries in the remapping table. A physical offset corresponds to that offset in the main data storage.
 +
 
 +
When a segment is extended a new remapping entry is appended to the physical storage, allowing expansion without relocating the existing entries.
 +
 
 +
Each virtual offset has two parts, a segment index and an offset. The size of these sections is controlled by the remap header.
 +
 
 +
Example: 0x3000000000000100<br />
 +
If 4 bits were reserved for the segment index, the offset would be split like this, representing offset 0x100 of segment 3.<br />
 +
Segment index: 0x3 Offset: 0x000000000000100
 +
 
 +
== Remap storage header ==
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 520: Line 500:
 
| 0x04
 
| 0x04
 
| 4
 
| 4
| Version? (Must be 0x10000 or less)
+
| Version (Must be 0.0.x.x or 0.1.x.x)
 
|-
 
|-
 
| 0x08
 
| 0x08
Line 539: Line 519:
 
|}
 
|}
  
=== Extra data ===
+
== Remapping Entry ==
 +
 
 +
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 +
|-
 +
| 0x00
 +
| 8
 +
| Virtual offset
 +
|-
 +
| 0x08
 +
| 8
 +
| Physical offset
 +
|-
 +
| 0x10
 +
| 8
 +
| Size
 +
|-
 +
| 0x18
 +
| 4
 +
| Alignment
 +
|-
 +
| 0x1c
 +
| 4
 +
| Padding?
 +
|-
 +
|}
 +
 
 +
= Duplex Storage =
 +
 
 +
A Duplex Storage contains four separate elements: [[#Duplex header|a header]], a bitmap, and two identically-sized chunks of data.
 +
 
 +
As hinted by the name, a Duplex Storage contains two main chunks of data. To store X bytes, two chunks of data each with size X are required.
 +
 
 +
== Bitmap ==
 +
 
 +
This main data storage is split into blocks of the size indicated in the duplex header. The bitmap contains as many bits as the main data has blocks. If the main data is 0x40000 bytes long with a block size of 0x4000 bytes, the bitmap would contain 0x10 bits.
 +
 
 +
The bitmap controls which data chunk is active for each block. e.g. If bit 3 of the bitmap is a 0 then block 3 of data chunk 0 is active and block 3 of data chunk 1 is inactive. This means that when data from block 3 is read, the data from chunk 0 will be returned and the data from chunk 1 will be completely ignored.
 +
 
 +
== Hierarchical Duplex Storage ==
 +
 
 +
Multiple Duplex Storages can be chained together to gain various benefits. With a Hierarchical Duplex Storage, the bitmap for the main data is stored inside another Duplex Storage.
 +
 
 +
The bitmap for this second Duplex Storage is stored in a special Duplex Storage. The data of this top level contains a master bitmap that is typically 0x40 bytes long. A bit in the save file header controls which master bitmap is active.
 +
 
 +
This allows for atomic operations on the Hierarchical Duplex Storage. When writing to the storage, data will be written to the inactive blocks and inactive bitmaps. When the data is committed the bit in the save file header is flipped, changing which master bitmap is active.
 +
 
 +
== Duplex header ==
 +
 
 +
* Block sizes are stored as powers of 2
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 548: Line 580:
 
|-
 
|-
 
| 0x00
 
| 0x00
| 0x40
+
| 4
| [[Filesystem_services#Save_Struct|Save Struct]]
+
| Magic ("DPFS")
 +
|-
 +
| 0x04
 +
| 4
 +
| Version (0.1.x.x)
 
|-
 
|-
| 0x40
+
| 0x08
 
| 8
 
| 8
| Save owner ID
+
| Master bitmap offset
 
|-
 
|-
| 0x48
+
| 0x10
 
| 8
 
| 8
| Timestamp
+
| Master bitmap size
 
|-
 
|-
| 0x50
+
| 0x18
 
| 4
 
| 4
| Unknown
+
| Master bitmap block size power
 +
|-
 +
| 0x1C
 +
| 8
 +
| Level 1 offset
 +
|-
 +
| 0x24
 +
| 8
 +
| Level 1 size
 
|-
 
|-
| 0x54
+
| 0x2C
 
| 4
 
| 4
| Unused?
+
| Level 1 block size power
 
|-
 
|-
| 0x58
+
| 0x30
 
| 8
 
| 8
| Size of usable save data
+
| Level 2 offset
 
|-
 
|-
| 0x60
+
| 0x38
 
| 8
 
| 8
| Journal size
+
| Level 2 size
 +
|-
 +
| 0x40
 +
| 4
 +
| Level 2 block size power
 
|-
 
|-
| 0x200
 
|
 
| End
 
 
|}
 
|}
  
== Remap Storage ==
+
= Save FS =
  
Remap Storage is used to remap segments of data from virtual offsets to physical offsets. This allows extending the save file without having to relocate existing data.
+
== Save FS header ==
 +
 
 +
* Structure is different than 3DS.
  
Each Remap Storage has three components: [[#Remap storage header|a header]], a remapping table, and the main data storage.
+
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 +
|-
 +
| 0x00
 +
| 4
 +
| Magic ("SAVE")
 +
|-
 +
| 0x04
 +
| 4
 +
| Version (0.6.x.x)
 +
|-
 +
| 0x08
 +
| 8
 +
| Number of blocks. Does not change if save file is resized.
 +
|-
 +
| 0x10
 +
| 8
 +
| Block Size
 +
|-
 +
| 0x18
 +
| 0x30
 +
| FAT header
 +
|-
 +
|}
  
A remap storage can contain a varying number of segments, each representing a chunk of contiguous virtual storage. A segment can be composed of one or more entries. Each of these entries are mapped from their virtual locations to their physical locations by entries in the remapping table. A physical offset corresponds to that offset in the main data storage.
+
== File allocation table ==
  
When a segment is extended a new remapping entry is appended to the end of the physical storage, allowing expansion without relocating the existing entries.
+
The savedata FS uses an allocation table to keep track of block allocation. This FAT contains doubly-linked lists of the blocks allocated to each file. Each entry in the FAT is 8 bytes in size.
  
Each virtual offset has two parts, a segment index and an offset. The size of these sections is controlled by the remap header.
+
FAT entry 0 is reserved for the list of free blocks. Because of this, the FAT entry for block n is found at FAT index n+1. The indexes stored in FAT entries refer the index of the next/previous FAT entry in the chain, not the index of the next/previous block.
  
Example: 0x3000000000000100<br />
+
The FAT header is internally called AllocationTableControlArea. The FAT itself is called AllocationTableMeta. The actual save FS data is called AllocationTableData.
If 4 bits were reserved for the segment index, the offset would be split like this, representing offset 0x100 of segment 3.<br />
 
Segment index: 0x3 Offset: 0x000000000000100
 
  
=== Remapping Entry ===
+
=== File allocation table header ===
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 606: Line 677:
 
| 0x00
 
| 0x00
 
| 8
 
| 8
| Virtual offset
+
| Block size
 
|-
 
|-
 
| 0x08
 
| 0x08
 
| 8
 
| 8
| Physical offset
+
| FAT offset
 
|-
 
|-
 
| 0x10
 
| 0x10
 +
| 4
 +
| FAT entry count
 +
|-
 +
| 0x14
 +
| 4
 +
| Padding
 +
|-
 +
| 0x18
 
| 8
 
| 8
| Size
+
| Data offset
 +
|-
 +
| 0x20
 +
| 4
 +
| Data block count
 +
|-
 +
| 0x24
 +
| 4
 +
| Padding
 +
|-
 +
| 0x28
 +
| 4
 +
| Directory table block index
 +
|-
 +
| 0x2C
 +
| 4
 +
| File table block index
 +
|-
 +
|}
 +
 
 +
=== File allocation table entry ===
 +
 
 +
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 +
|-
 +
| 0
 +
| 4 (High bit)
 +
| Set if entry is the first entry in the list.
 +
|-
 +
| 0
 +
| 4 (Lower 31 bits)
 +
| Previous entry index. First entry in list if 0.
 +
|-
 +
| 4
 +
| 4 (High bit)
 +
| Set if the allocation segment has multiple blocks.
 +
|-
 +
| 4
 +
| 4 (Lower 31 bits)
 +
| Next entry index. Last entry in list if 0.
 +
|-
 +
|}
 +
 
 +
If the allocation segment has multiple blocks, the first entry will be followed by a range descriptor entry. The last entry in the segment will contain a duplicate of this entry.
 +
 
 +
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 +
|-
 +
| 0
 +
| 4 (High bit)
 +
| Always set.
 +
|-
 +
| 0
 +
| 4 (Lower 31 bits)
 +
| First entry in this segment.
 
|-
 
|-
| 0x18
 
 
| 4
 
| 4
| Alignment
+
| 4 (High bit)
 +
| Never set.
 
|-
 
|-
| 0x1c
 
 
| 4
 
| 4
| Padding?
+
| 4 (Lower 31 bits)
 +
| Last entry in this segment.
 
|-
 
|-
 
|}
 
|}
  
== Files ==
+
== Save File Table ==
  
=== Directory Table Entry ===
+
The save file table is similar to the RomFS file table, except the save file table uses linked lists instead of dictionaries.
  
* Index 0 is the start of a linked list that contains all invalid/inactive directories.
+
The table contains a list of directory entries and a list of file entries. Their respective types are:<br />
* Index 1 is the start of a linked list that contains all valid/active directories.
+
'''SaveFsList<SaveFileTableEntry<SaveDirectoryInfo>>'''<br />
* Index 2 is the root directory.
+
'''SaveFsList<SaveFileTableEntry<SaveFileInfo>>'''
 +
 
 +
=== Save File Table Entry ===
 +
 
 +
SaveFileTableEntry<class T>
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 642: Line 785:
 
| 0x00
 
| 0x00
 
| 4
 
| 4
| Parent directory node index
+
| Next entry index. A value of 0 indicates the end of the list.
 
|-
 
|-
 
| 0x04
 
| 0x04
| 64
+
| sizeof(T)
| Filename
+
| Value of type T.
 +
|-
 +
|}
 +
 
 +
=== Save File Info ===
 +
 
 +
Holds the information of a single file.
 +
 
 +
{| class="wikitable"
 
|-
 
|-
| 0x44
+
! Start
| 4
+
! Length
| Next sibling directory node index
+
! Description
 
|-
 
|-
| 0x48
+
| 0x00
 
| 4
 
| 4
| First child directory node index
+
| Starting block index.
 
|-
 
|-
| 0x4c
+
| 0x04
 
| 8
 
| 8
| First child file node index
+
| File length in bytes.
 
|-
 
|-
| 0x54
+
| 0x0C
 
| 8
 
| 8
| Unused?
+
| Reserved.
 +
|-
 +
|}
 +
 
 +
=== Save Directory Info ===
 +
 
 +
Holds the information of a single directory.
 +
 
 +
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 +
|-
 +
| 0x00
 +
| 4
 +
| First child directory index. 0 if none.
 
|-
 
|-
| 0x5c
+
| 0x04
 
| 4
 
| 4
| Next directory node index in the chain of invalid or valid directories. If this is the first block in a list of length 0, this value will contain the total number of directory nodes.
+
| First child file index. 0 if none.
 +
|-
 +
| 0x08
 +
| 0xC
 +
| Reserved.
 
|-
 
|-
 
|}
 
|}
  
=== File Table Entry ===
+
== Save FS List ==
 +
 
 +
SaveFsList<class T>
 +
 
 +
This is a linked list that is used internally by '''Save File Table''' as a key-value store. Integer/string pairs are used as keys. The list is represented as a single array so that it can be easily stored and read from a file. Entry indexes 0 and 1 are reserved.
 +
 
 +
Index 0 is the start of a list containing all free entries. When an item in the list is removed, the entry it was using is added to this list for future reuse.
 +
 
 +
Index 1 is the start of a list containing all currently used entries.
  
* Index 0 is the start of a linked list that contains all invalid/inactive files.
+
The first 8 bytes of the list are used as follows. Indexes 0 and 1 are included in these counts.
* Index 1 is the start of a linked list that contains all valid/active files.
 
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 683: Line 861:
 
| 0x00
 
| 0x00
 
| 4
 
| 4
| Parent directory node index
+
| The size of the list. Freed entries that have not been reused are included in the count.
 
|-
 
|-
 
| 0x04
 
| 0x04
| 64
+
| 4
| Filename
+
| The current capacity of the list based on the number of bytes allocated.
 +
|-
 +
|}
 +
 
 +
=== Save FS List Key ===
 +
 
 +
{| class="wikitable"
 +
|-
 +
! Start
 +
! Length
 +
! Description
 
|-
 
|-
| 0x44
+
| 0x00
 
| 4
 
| 4
| Next sibling file node index
+
| 32-bit integer.
 +
|-
 +
| 0x04
 +
| 0x40
 +
| 0x40-byte string.
 +
|-
 +
|}
 +
 
 +
=== Save FS List Entry ===
 +
 
 +
{| class="wikitable"
 
|-
 
|-
| 0x48
+
! Start
| 4
+
! Length
| Index of the block that the file starts at
+
! Description
 
|-
 
|-
| 0x4c
+
| 0x00
| 8
+
| 0x44
| File size in bytes
+
| Key.
 
|-
 
|-
| 0x54
+
| 0x44
| 8
+
| sizeof(T)
| Unused?
+
| Value.
 
|-
 
|-
| 0x5c
+
| 0x44 + sizeof(T)
 
| 4
 
| 4
| Next file node index in the chain of invalid or valid files. If this is the first block in a list of length 0, this value will contain the total number of file nodes.
+
| Next entry node index. A value of 0 indicates the end of the list.
 
|-
 
|-
 
|}
 
|}

Latest revision as of 21:03, 28 April 2020

This page describes the format of save files contained in NAND. These files are stored as completely unencrypted, plaintext data. Save files are not cleared upon creation, resulting in possible garbage data in unused portions of the container.

Main header

The header is 0x4000 bytes long.

There are 2 headers stored at 0x0 and 0x4000, presumably for commit and rollback purposes.

Decimal versions are separated as Major, Minor, Micro, and Bugfix with each using one byte. e.g. version 3.4.5.6 would be 0x03040506.

Image offset Length Description
0x000 0x100 AES-CMAC header
0x100 0x200 DISF header
0x300 0x44 Duplex header
0x344 0xC4 Integrity verification header
0x408 0x200 Journal header
0x608 0x48 Save header
0x650 0x40 Main data remap header
0x690 0x40 Meta data remap header
0x6D0 0x8 Unknown
0x6D8 0x200 Extra data A
0x8D8 0x200 Extra data B
0xAD8 0x3528 Additional storage

The additional storage at the end of the header is used to store any extra header data. This data's structure is determined by offsets stored in the main part of the header.

AES CMAC header

This is internally referred to as MasterHeaderMac.

Image offset Length Description
0x00 0x10 AES-CMAC over DISF header (size 0x200)
0x10 0xF0 Zero padding

The final CMAC key used for this is generated using GenerateAesKek with a kek source and the device key, along with and LoadAesKey and a set key seed.

DISF

This section contains information about the structure of the save file.

Start Length Description
0x000 4 Magic ("DISF")
0x004 4 Version (Major version must be 4 or 5. Only system version 5.0.0+ can read version 5 save files)
0x008 32 Hash of start of DPFS to end of 0x4000 block (0x300-0x3FFF)
0x028 8 Main data remap mapping table offset
0x030 8 Main data remap mapping table size
0x038 8 Meta data remap mapping table offset
0x040 8 Meta data remap mapping table size
0x048 8 Main data remap offset
0x050 8 Main data remap size
0x058 8 Duplex level 1 virtual offset A
0x060 8 Duplex level 1 virtual offset B
0x068 8 Duplex level 1 size
0x070 8 Duplex level 2 virtual offset A
0x078 8 Duplex level 2 virtual offset B
0x080 8 Duplex level 2 size
0x088 8 Journal storage virtual offset
0x090 8 Journal storage data size A
0x098 8 Journal storage data size B
0x0A0 8 Journal storage journal size
0x0A8 8 Duplex master bitmap offset A
0x0B0 8 Duplex master bitmap offset B
0x0B8 8 Duplex master bitmap size
0x0C0 8 IVFC master hash offset A
0x0C8 8 IVFC master hash offset B
0x0D0 8 IVFC master hash size
0x0D8 8 Journal block table virtual offset
0x0E0 8 Journal block table size
0x0E8 8 Virtual offset of bitmap of modified physical journal blocks
0x0F0 8 Size of bitmap of modified physical journal blocks
0x0F8 8 Virtual offset of bitmap of modified virtual journal blocks
0x100 8 Size of bitmap of modified virtual journal blocks
0x108 8 Virtual offset of bitmap of free journal blocks
0x110 8 Size of bitmap of free journal blocks
0x118 8 IVFC level 1 virtual offset
0x120 8 IVFC level 1 size
0x128 8 IVFC level 2 virtual offset
0x130 8 IVFC level 2 size
0x138 8 IVFC level 3 virtual offset
0x140 8 IVFC level 3 size
0x148 8 File allocation table virtual offset
0x150 8 File allocation table size
0x158 1 Index of the active duplex master bitmap
0x160 8 [5.0.0+] File allocation table IVFC master hash offset A
0x168 8 [5.0.0+] File allocation table IVFC master hash offset B
0x170 8 [5.0.0+] File allocation table IVFC level 1 virtual offset
0x178 8 [5.0.0+] File allocation table IVFC level 1 size
0x180 8 [5.0.0+] File allocation table IVFC level 2 virtual offset
0x188 8 [5.0.0+] File allocation table IVFC level 2 size
0x200 End

Integrity verification header

  • Offsets for levels 1-3 come from the metadata remap storage
  • Offsets for level 4 comes from the main data remap storage
  • This is the same header used in NCA files
Start Length Description
0x00 4 Magic ("IVFC")
0x04 4 Version (0.2.x.x)
0x08 4 Master hash size
0xC 4 Number of levels (Unused in save files)
0x10 0x18*6 Level information for up to 6 levels
0xA0 32 Salt seed

Level information

  • 0x18 bytes long
  • Block sizes are stored as powers of 2
Start Length Description
0x00 8 Offset
0x08 8 Size
0x10 4 Block size power
0x14 4 Reserved

Journal header

Start Length Description
0x00 4 Magic ("JNGL")
0x04 4 Version (Must be 0.0.x.x or 0.1.0.0)
0x08 8 Total size (Incl. journal)
0x10 8 Journal size
0x18 8 Block size
0x20 16 Journal map header
0x200 End

Journal map header

Start Length Description
0x00 4 Version (Stored as a normal 32-bit integer. Must be 0 or 1)
0x04 4 Main data block count
0x08 4 Journal block count
0x0C 4 Padding

Extra data

Start Length Description
0x00 0x40 SaveDataAttribute
0x40 8 Save owner ID
0x48 8 Timestamp
0x50 4 Flags?
0x54 4 Unused?
0x58 8 Size of usable save data
0x60 8 Journal size
0x68 8 Commit ID
0x200 End

Remap Storage

Remap Storage is used to remap segments of data from virtual offsets to physical offsets. This allows extending the save file without having to relocate existing data.

Each Remap Storage has three components: a header, a remapping table, and the main data storage.

A remap storage can contain a varying number of segments, each representing a chunk of contiguous virtual storage. A segment can be composed of one or more entries. Each of these entries are mapped from their virtual locations to their physical locations by entries in the remapping table. A physical offset corresponds to that offset in the main data storage.

When a segment is extended a new remapping entry is appended to the physical storage, allowing expansion without relocating the existing entries.

Each virtual offset has two parts, a segment index and an offset. The size of these sections is controlled by the remap header.

Example: 0x3000000000000100
If 4 bits were reserved for the segment index, the offset would be split like this, representing offset 0x100 of segment 3.
Segment index: 0x3 Offset: 0x000000000000100

Remap storage header

Start Length Description
0x00 4 Magic ("RMAP")
0x04 4 Version (Must be 0.0.x.x or 0.1.x.x)
0x08 4 Number of remapping entries
0x0C 4 Number of remapping segments
0x10 4 Number of bits reserved for the segment index in virtual offsets
0x40 End

Remapping Entry

Start Length Description
0x00 8 Virtual offset
0x08 8 Physical offset
0x10 8 Size
0x18 4 Alignment
0x1c 4 Padding?

Duplex Storage

A Duplex Storage contains four separate elements: a header, a bitmap, and two identically-sized chunks of data.

As hinted by the name, a Duplex Storage contains two main chunks of data. To store X bytes, two chunks of data each with size X are required.

Bitmap

This main data storage is split into blocks of the size indicated in the duplex header. The bitmap contains as many bits as the main data has blocks. If the main data is 0x40000 bytes long with a block size of 0x4000 bytes, the bitmap would contain 0x10 bits.

The bitmap controls which data chunk is active for each block. e.g. If bit 3 of the bitmap is a 0 then block 3 of data chunk 0 is active and block 3 of data chunk 1 is inactive. This means that when data from block 3 is read, the data from chunk 0 will be returned and the data from chunk 1 will be completely ignored.

Hierarchical Duplex Storage

Multiple Duplex Storages can be chained together to gain various benefits. With a Hierarchical Duplex Storage, the bitmap for the main data is stored inside another Duplex Storage.

The bitmap for this second Duplex Storage is stored in a special Duplex Storage. The data of this top level contains a master bitmap that is typically 0x40 bytes long. A bit in the save file header controls which master bitmap is active.

This allows for atomic operations on the Hierarchical Duplex Storage. When writing to the storage, data will be written to the inactive blocks and inactive bitmaps. When the data is committed the bit in the save file header is flipped, changing which master bitmap is active.

Duplex header

  • Block sizes are stored as powers of 2
Start Length Description
0x00 4 Magic ("DPFS")
0x04 4 Version (0.1.x.x)
0x08 8 Master bitmap offset
0x10 8 Master bitmap size
0x18 4 Master bitmap block size power
0x1C 8 Level 1 offset
0x24 8 Level 1 size
0x2C 4 Level 1 block size power
0x30 8 Level 2 offset
0x38 8 Level 2 size
0x40 4 Level 2 block size power

Save FS

Save FS header

  • Structure is different than 3DS.
Start Length Description
0x00 4 Magic ("SAVE")
0x04 4 Version (0.6.x.x)
0x08 8 Number of blocks. Does not change if save file is resized.
0x10 8 Block Size
0x18 0x30 FAT header

File allocation table

The savedata FS uses an allocation table to keep track of block allocation. This FAT contains doubly-linked lists of the blocks allocated to each file. Each entry in the FAT is 8 bytes in size.

FAT entry 0 is reserved for the list of free blocks. Because of this, the FAT entry for block n is found at FAT index n+1. The indexes stored in FAT entries refer the index of the next/previous FAT entry in the chain, not the index of the next/previous block.

The FAT header is internally called AllocationTableControlArea. The FAT itself is called AllocationTableMeta. The actual save FS data is called AllocationTableData.

File allocation table header

Start Length Description
0x00 8 Block size
0x08 8 FAT offset
0x10 4 FAT entry count
0x14 4 Padding
0x18 8 Data offset
0x20 4 Data block count
0x24 4 Padding
0x28 4 Directory table block index
0x2C 4 File table block index

File allocation table entry

Start Length Description
0 4 (High bit) Set if entry is the first entry in the list.
0 4 (Lower 31 bits) Previous entry index. First entry in list if 0.
4 4 (High bit) Set if the allocation segment has multiple blocks.
4 4 (Lower 31 bits) Next entry index. Last entry in list if 0.

If the allocation segment has multiple blocks, the first entry will be followed by a range descriptor entry. The last entry in the segment will contain a duplicate of this entry.

Start Length Description
0 4 (High bit) Always set.
0 4 (Lower 31 bits) First entry in this segment.
4 4 (High bit) Never set.
4 4 (Lower 31 bits) Last entry in this segment.

Save File Table

The save file table is similar to the RomFS file table, except the save file table uses linked lists instead of dictionaries.

The table contains a list of directory entries and a list of file entries. Their respective types are:
SaveFsList<SaveFileTableEntry<SaveDirectoryInfo>>
SaveFsList<SaveFileTableEntry<SaveFileInfo>>

Save File Table Entry

SaveFileTableEntry<class T>

Start Length Description
0x00 4 Next entry index. A value of 0 indicates the end of the list.
0x04 sizeof(T) Value of type T.

Save File Info

Holds the information of a single file.

Start Length Description
0x00 4 Starting block index.
0x04 8 File length in bytes.
0x0C 8 Reserved.

Save Directory Info

Holds the information of a single directory.

Start Length Description
0x00 4 First child directory index. 0 if none.
0x04 4 First child file index. 0 if none.
0x08 0xC Reserved.

Save FS List

SaveFsList<class T>

This is a linked list that is used internally by Save File Table as a key-value store. Integer/string pairs are used as keys. The list is represented as a single array so that it can be easily stored and read from a file. Entry indexes 0 and 1 are reserved.

Index 0 is the start of a list containing all free entries. When an item in the list is removed, the entry it was using is added to this list for future reuse.

Index 1 is the start of a list containing all currently used entries.

The first 8 bytes of the list are used as follows. Indexes 0 and 1 are included in these counts.

Start Length Description
0x00 4 The size of the list. Freed entries that have not been reused are included in the count.
0x04 4 The current capacity of the list based on the number of bytes allocated.

Save FS List Key

Start Length Description
0x00 4 32-bit integer.
0x04 0x40 0x40-byte string.

Save FS List Entry

Start Length Description
0x00 0x44 Key.
0x44 sizeof(T) Value.
0x44 + sizeof(T) 4 Next entry node index. A value of 0 indicates the end of the list.