diff --git a/arm9/source/filesys/fsdrive.c b/arm9/source/filesys/fsdrive.c index ce3e21b..be0053f 100644 --- a/arm9/source/filesys/fsdrive.c +++ b/arm9/source/filesys/fsdrive.c @@ -49,7 +49,7 @@ int DriveType(const char* path) { type = DRV_VIRTUAL | DRV_SYSNAND; } else if (vsrc == VRT_EMUNAND) { type = DRV_VIRTUAL | DRV_EMUNAND; - } else if ((vsrc == VRT_IMGNAND) || (vsrc == VRT_DISADIFF) || (vsrc == VRT_BDRI)) { + } else if ((vsrc == VRT_IMGNAND) || (vsrc == VRT_DISADIFF) || (vsrc == VRT_BDRI) || (vsrc == VRT_SAVE)) { type = DRV_VIRTUAL | DRV_IMAGE; } else if (vsrc == VRT_XORPAD) { type = DRV_VIRTUAL | DRV_XORPAD; @@ -123,6 +123,10 @@ bool GetRootDirContentsWorker(DirStruct* contents) { snprintf(entry->name, 252, "[%s] %s (%s)", drvnum[i], drvname[i], carttype); else if (*(drvnum[i]) == '0') // SD card handling snprintf(entry->name, 252, "[%s] %s (%s)", drvnum[i], drvname[i], sdlabel); + else if (*(drvnum[i]) == 'F') // Save/Extdata handling + snprintf(entry->name, 252, "[%s] %s", drvnum[i], + (GetMountState() & SYS_DIFF) ? STR_LAB_EXTDATA_IMAGE : + (GetMountState() & SYS_DISA) ? STR_LAB_SAVE_FILE_IMAGE : "UNK"); else snprintf(entry->name, 252, "[%s] %s", drvnum[i], drvname[i]); entry->size = GetTotalSpace(entry->path); entry->type = T_ROOT; diff --git a/arm9/source/filesys/fsdrive.h b/arm9/source/filesys/fsdrive.h index e41c461..4bd11f8 100644 --- a/arm9/source/filesys/fsdrive.h +++ b/arm9/source/filesys/fsdrive.h @@ -37,7 +37,7 @@ STR_LAB_EMUNAND_CTRNAND, STR_LAB_EMUNAND_TWLN, STR_LAB_EMUNAND_TWLP, STR_LAB_EMUNAND_SD, STR_LAB_EMUNAND_VIRTUAL, \ STR_LAB_IMGNAND_CTRNAND, STR_LAB_IMGNAND_TWLN, STR_LAB_IMGNAND_TWLP, STR_LAB_IMGNAND_VIRTUAL, \ STR_LAB_GAMECART, \ - STR_LAB_GAME_IMAGE, STR_LAB_AESKEYDB_IMAGE, STR_LAB_BDRI_IMAGE, STR_LAB_DISA_DIFF_IMAGE, \ + STR_LAB_GAME_IMAGE, STR_LAB_AESKEYDB_IMAGE, STR_LAB_BDRI_IMAGE, STR_LAB_DISA_DIFF_IMAGE, STR_LAB_SAVE_FILE_IMAGE, \ STR_LAB_MEMORY_VIRTUAL, \ STR_LAB_VRAM_VIRTUAL, \ STR_LAB_TITLE_MANAGER, \ @@ -45,7 +45,7 @@ #define FS_DRVNUM \ "0:", "1:", "2:", "3:", "A:", "S:", "4:", "5:", "6:", "B:", "E:", "7:", "8:", "9:", \ - "I:", "C:", "G:", "K:", "T:", "D:", "M:", "V:", "Y:", "Z:" + "I:", "C:", "G:", "K:", "T:", "D:", "F:", "M:", "V:", "Y:", "Z:" /** Function to identify the type of a drive **/ int DriveType(const char* path); diff --git a/arm9/source/game/bdri.c b/arm9/source/game/bdri.c index 5349fc2..02aa386 100644 --- a/arm9/source/game/bdri.c +++ b/arm9/source/game/bdri.c @@ -1,7 +1,14 @@ +#include "fsinit.h" +#include "fsperm.h" +#include "ifat_common.h" #include "bdri.h" +#include "image.h" +#include "language.h" +#include "nandcmac.h" +#include "ui.h" #include "vff.h" -#define FAT_ENTRY_SIZE 2 * sizeof(u32) +#define FAT_ENTRY_SIZE sizeof(IFatEntry) #define REPLACE_SIZE_MISMATCH 2 #define getfatflag(uv) (((uv) & 0x80000000UL) != 0) @@ -9,49 +16,31 @@ #define buildfatuv(index, flag) ((index) | ((flag) ? 0x80000000UL : 0)) typedef struct { - char magic[4]; // "BDRI" - u32 version; // == 0x30000 - u64 info_offset; // == 0x20 - u64 image_size; // in blocks; and including the pre-header - u32 image_block_size; - u8 padding1[4]; - u8 unknown[4]; - u32 data_block_size; - u64 dht_offset; - u32 dht_bucket_count; - u8 padding2[4]; - u64 fht_offset; - u32 fht_bucket_count; - u8 padding3[4]; - u64 fat_offset; - u32 fat_entry_count; // exculdes 0th entry - u8 padding4[4]; - u64 data_offset; - u32 data_block_count; // == fat_entry_count - u8 padding5[4]; - u32 det_start_block; - u32 det_block_count; - u32 max_dir_count; - u8 padding6[4]; - u32 fet_start_block; - u32 fet_block_count; - u32 max_file_count; - u8 padding7[4]; -} __attribute__((packed)) BDRIFsHeader; + IFatPreHeader pre_header; + IFatFsInfo fs_info; +} PACKED_STRUCT BDRIFsHeader; typedef struct { char magic[8]; // varies based on media type and importdb vs titledb - u8 reserved[0x78]; + u32 version; // should always be 0 + u8 reserved[0x74]; +} PACKED_STRUCT TitleDBPreHeader; + +typedef struct { + TitleDBPreHeader pre_header; BDRIFsHeader fs_header; -} __attribute__((packed)) TitleDBPreHeader; +} PACKED_STRUCT TitleDBHeader; typedef struct { char magic[4]; // "TICK" - u32 unknown1; // usually (assuming always) == 1 - u32 unknown2; - u32 unknown3; + u32 version; // always 1 + u8 padding[8]; +} PACKED_STRUCT TickDBPreHeader; + +typedef struct { + TickDBPreHeader pre_header; BDRIFsHeader fs_header; -} __attribute__((packed)) TickDBPreHeader; +} PACKED_STRUCT TickDBHeader; typedef struct { u32 parent_index; @@ -62,18 +51,34 @@ typedef struct { u64 size; // in bytes u8 padding2[8]; u32 hash_bucket_next_index; -} __attribute__((packed)) TdbFileEntry; +} __attribute__((packed)) BDRIFileEntry; typedef struct { u32 total_entry_count; u32 max_entry_count; // == max_file_count + 1 u8 padding[32]; u32 next_dummy_index; -} __attribute__((packed)) DummyFileEntry; +} __attribute__((packed)) BDRIDummyFileEntry; typedef struct { - u32 unknown; // usually (assuming always) == 1 - u32 ticket_size; // == 0x350 == sizeof(Ticket) + u64 id; + u32 next_sibling_index; + u32 first_subdir_index; + u32 first_subfile_index; + u8 pad0[4]; + u32 hash_bucket_next_index; +} __attribute__((packed)) BDRIDirectoryEntry; + +typedef struct { + u32 total_entry_count; + u32 max_entry_count; + u8 padding[16]; + u32 next_dummy_index; +} __attribute__((packed)) BDRIDummyDirectoryEntry; + +typedef struct { + u32 ticket_count; // usually == 1 + u32 ticket_size; // usually == 0x350 == sizeof(Ticket) Ticket ticket; } __attribute__((packed, aligned(4))) TicketEntry; @@ -104,14 +109,14 @@ static FRESULT BDRIWrite(UINT ofs, UINT btw, const void* buf) { } bool CheckDBMagic(const u8* pre_header, bool tickdb) { - const TitleDBPreHeader* title = (TitleDBPreHeader*) pre_header; - const TickDBPreHeader* tick = (TickDBPreHeader*) pre_header; + const TitleDBHeader* title = (TitleDBHeader*) (void *) pre_header; + const TickDBHeader* tick = (TickDBHeader*) (void *) pre_header; - return (tickdb ? ((strncmp(tick->magic, "TICK", 4) == 0) && (tick->unknown1 == 1)) : - ((strcmp(title->magic, "NANDIDB") == 0) || (strcmp(title->magic, "NANDTDB") == 0) || - (strcmp(title->magic, "TEMPIDB") == 0) || (strcmp(title->magic, "TEMPTDB") == 0))) && - (strcmp((tickdb ? tick->fs_header : title->fs_header).magic, "BDRI") == 0) && - ((tickdb ? tick->fs_header : title->fs_header).version == 0x30000); + return (tickdb ? ((strncmp(tick->pre_header.magic, "TICK", 4) == 0) && (tick->pre_header.version == 1)) : + ((strcmp(title->pre_header.magic, "NANDIDB") == 0) || (strcmp(title->pre_header.magic, "NANDTDB") == 0) || + (strcmp(title->pre_header.magic, "TEMPIDB") == 0) || (strcmp(title->pre_header.magic, "TEMPTDB") == 0))) && + (strcmp((tickdb ? tick->fs_header : title->fs_header).pre_header.magic_str, "BDRI") == 0) && + ((tickdb ? tick->fs_header : title->fs_header).pre_header.version == 0x30000); } // This function was taken, with slight modification, from https://3dbrew.org/wiki/Inner_FAT @@ -128,18 +133,19 @@ static u32 GetHashBucket(const u8* tid, u32 parent_dir_index, u32 bucket_count) } static u32 GetBDRIEntrySize(const BDRIFsHeader* fs_header, const u32 fs_header_offset, const u8* title_id, u32* size) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) // Could be more thorough + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) // Could be more thorough return 1; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; - const u32 fht_offset = fs_header_offset + fs_header->fht_offset; + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fht_offset = fs_header_offset + fs_header->fs_info.file_hashtbl.outfat_offset; u32 index = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; u64 tid_be = getbe64(title_id); u8* title_id_be = (u8*) &tid_be; - const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fht_bucket_count); + const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fs_info.file_hashtbl.outfat_count); if (BDRIRead(fht_offset + hash_bucket * sizeof(u32), sizeof(u32), &(file_entry.hash_bucket_next_index)) != FR_OK) return 1; @@ -151,7 +157,7 @@ static u32 GetBDRIEntrySize(const BDRIFsHeader* fs_header, const u32 fs_header_o index = file_entry.hash_bucket_next_index; - if (BDRIRead(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; } while (memcmp(title_id_be, file_entry.title_id, 8) != 0); @@ -161,19 +167,20 @@ static u32 GetBDRIEntrySize(const BDRIFsHeader* fs_header, const u32 fs_header_o } static u32 ReadBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offset, const u8* title_id, u8* entry, const u32 expected_size) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) // Could be more thorough + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) // Could be more thorough return 1; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; - const u32 fht_offset = fs_header_offset + fs_header->fht_offset; - const u32 fat_offset = fs_header_offset + fs_header->fat_offset; + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fht_offset = fs_header_offset + fs_header->fs_info.file_hashtbl.outfat_offset; + const u32 fat_offset = fs_header_offset + fs_header->fs_info.fat.outfat_offset; u32 index = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; u64 tid_be = getbe64(title_id); u8* title_id_be = (u8*) &tid_be; - const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fht_bucket_count); + const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fs_info.file_hashtbl.outfat_count); if (BDRIRead(fht_offset + hash_bucket * sizeof(u32), sizeof(u32), &(file_entry.hash_bucket_next_index)) != FR_OK) return 1; @@ -185,7 +192,7 @@ static u32 ReadBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offs index = file_entry.hash_bucket_next_index; - if (BDRIRead(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; } while (memcmp(title_id_be, file_entry.title_id, 8) != 0); @@ -223,8 +230,8 @@ static u32 ReadBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offs index = next_index; - u32 btr = min(file_entry.size - bytes_read, read_count * fs_header->data_block_size); - if (entry && (BDRIRead(data_offset + read_start * fs_header->data_block_size, btr, entry + bytes_read) != FR_OK)) + u32 btr = min(file_entry.size - bytes_read, read_count * fs_header->fs_info.data_region_blocksize); + if (entry && (BDRIRead(data_offset + read_start * fs_header->fs_info.data_region_blocksize, btr, entry + bytes_read) != FR_OK)) return 1; bytes_read += btr; @@ -234,17 +241,18 @@ static u32 ReadBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offs } static u32 RemoveBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offset, const u8* title_id) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) // Could be more thorough + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) // Could be more thorough return 1; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 det_offset = data_offset + fs_header->det_start_block * fs_header->data_block_size; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; - const u32 fht_offset = fs_header_offset + fs_header->fht_offset; - const u32 fat_offset = fs_header_offset + fs_header->fat_offset; + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 det_offset = data_offset + fs_header->fs_info.dirtable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fht_offset = fs_header_offset + fs_header->fs_info.file_hashtbl.outfat_offset; + const u32 fat_offset = fs_header_offset + fs_header->fs_info.fat.outfat_offset; u32 index = 0, previous_index = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; u64 tid_be = getbe64(title_id); u8* title_id_be = (u8*) &tid_be; @@ -260,25 +268,25 @@ static u32 RemoveBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_of if (index == 0) return 1; - if (BDRIRead(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; } while (memcmp(title_id_be, file_entry.title_id, 8) != 0); - DummyFileEntry dummy_entry; + BDRIDummyFileEntry dummy_entry; // Read the 0th entry in the FET, which is always a dummy entry - if (BDRIRead(fet_offset, sizeof(DummyFileEntry), &dummy_entry) != FR_OK) + if (BDRIRead(fet_offset, sizeof(BDRIDummyFileEntry), &dummy_entry) != FR_OK) return 1; - if (dummy_entry.max_entry_count != fs_header->max_file_count + 1) + if (dummy_entry.max_entry_count != fs_header->fs_info.filetable_info.max_entry_count + 1) return 1; - if ((BDRIWrite(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &dummy_entry) != FR_OK) || + if ((BDRIWrite(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &dummy_entry) != FR_OK) || (BDRIWrite(fet_offset + 0x28, sizeof(u32), &index) != FR_OK) || - (BDRIWrite((previous_index == 0) ? det_offset + 0x2C : fet_offset + previous_index * sizeof(TdbFileEntry) + 0xC, sizeof(u32), &(file_entry.next_sibling_index)) != FR_OK)) + (BDRIWrite((previous_index == 0) ? det_offset + 0x2C : fet_offset + previous_index * sizeof(BDRIFileEntry) + 0xC, sizeof(u32), &(file_entry.next_sibling_index)) != FR_OK)) return 1; - const u32 hash_bucket = GetHashBucket(file_entry.title_id, file_entry.parent_index, fs_header->fht_bucket_count); + const u32 hash_bucket = GetHashBucket(file_entry.title_id, file_entry.parent_index, fs_header->fs_info.file_hashtbl.outfat_count); u32 index_hash = 0; if (BDRIRead(fht_offset + hash_bucket * sizeof(u32), sizeof(u32), &index_hash) != FR_OK) @@ -294,11 +302,11 @@ static u32 RemoveBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_of break; prev_hash_index = index_hash; - if (BDRIRead(fet_offset + index_hash * sizeof(TdbFileEntry) + 0x28, sizeof(u32), &index_hash) != FR_OK) + if (BDRIRead(fet_offset + index_hash * sizeof(BDRIFileEntry) + 0x28, sizeof(u32), &index_hash) != FR_OK) return 1; } while (index_hash != index); - if ((prev_hash_index != 0) && BDRIWrite(fet_offset + prev_hash_index * sizeof(TdbFileEntry) + 0x28, sizeof(u32), &(file_entry.hash_bucket_next_index)) != FR_OK) + if ((prev_hash_index != 0) && BDRIWrite(fet_offset + prev_hash_index * sizeof(BDRIFileEntry) + 0x28, sizeof(u32), &(file_entry.hash_bucket_next_index)) != FR_OK) return 1; } @@ -348,22 +356,127 @@ static u32 RemoveBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_of return 0; } +static u32 AllocateBDRIBlocks(const BDRIFsHeader* fs_header, const u32 fs_header_offset, const u32 size_blocks, u32 *out_fat_index) { + u32 fat_entry[2]; + u32 fat_index = 0; + const u32 fat_offset = fs_header_offset + fs_header->fs_info.fat.outfat_offset; + + if (BDRIRead(fat_offset, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + if (getfatflag(fat_entry[1]) || (fat_entry[0] != 0)) + return 1; + + u32 next_fat_index = getfatindex(fat_entry[1]), node_size = 0; + + // Find contiguous free space in the FAT for the entry. Technically there could be a case of enough space existing, but not in a contiguous fasion, but this would never realistically happen + do { + if (next_fat_index == 0) + return 1; // Reached the end of the free node chain without finding enough contiguous free space - this should never realistically happen + + fat_index = next_fat_index; + + if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + next_fat_index = getfatindex(fat_entry[1]); + + if (getfatflag(fat_entry[1])) { // Multi-entry node + if (BDRIRead(fat_offset + (fat_index + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + if (!getfatflag(fat_entry[0]) || getfatflag(fat_entry[1]) || (getfatindex(fat_entry[0]) != fat_index) || (getfatindex(fat_entry[0]) >= getfatindex(fat_entry[1]))) + return 1; + + node_size = getfatindex(fat_entry[1]) + 1 - fat_index; + } else { // Single-entry node + node_size = 1; + } + } while (node_size < size_blocks); + + const bool shrink_free_node = node_size > size_blocks; + + if (shrink_free_node) { + if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + if (node_size - size_blocks == 1) + fat_entry[1] = buildfatuv(getfatindex(fat_entry[1]), false); + + if (BDRIWrite(fat_offset + (fat_index + size_blocks) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + if (node_size - size_blocks > 1) { + if (BDRIRead(fat_offset + (fat_index + node_size - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + fat_entry[0] = buildfatuv(fat_index + size_blocks, true); + + if ((BDRIWrite(fat_offset + (fat_index + node_size - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) || + (BDRIWrite(fat_offset + (fat_index + size_blocks + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK)) + return 1; + } + } + + if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + const u32 previous_free_index = getfatindex(fat_entry[0]), next_free_index = getfatindex(fat_entry[1]); + + fat_entry[0] = buildfatuv(0, true); + fat_entry[1] = buildfatuv(0, size_blocks > 1); + + if (BDRIWrite(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + if (size_blocks > 1) { + fat_entry[0] = buildfatuv(fat_index, true); + fat_entry[1] = buildfatuv(fat_index + size_blocks - 1, false); + + if ((BDRIWrite(fat_offset + (fat_index + size_blocks - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) || + (BDRIWrite(fat_offset + (fat_index + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK)) + return 1; + } + + if (next_free_index != 0) { + if (BDRIRead(fat_offset + next_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + fat_entry[0] = buildfatuv(shrink_free_node ? fat_index + size_blocks : previous_free_index, (!shrink_free_node && (previous_free_index == 0))); + + if (BDRIWrite(fat_offset + next_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + } + + if (BDRIRead(fat_offset + previous_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + fat_entry[1] = buildfatuv(shrink_free_node ? fat_index + size_blocks : next_free_index, getfatflag(fat_entry[1])); + + if (BDRIWrite(fat_offset + previous_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + return 1; + + *out_fat_index = fat_index; + return 0; +} + static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offset, const u8* title_id, const u8* entry, const u32 size, bool replace) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) // Could be more thorough + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) // Could be more thorough return 1; if (!entry || !size) return 1; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 det_offset = data_offset + fs_header->det_start_block * fs_header->data_block_size; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; - const u32 fht_offset = fs_header_offset + fs_header->fht_offset; - const u32 fat_offset = fs_header_offset + fs_header->fat_offset; - const u32 size_blocks = (size / fs_header->data_block_size) + (((size % fs_header->data_block_size) == 0) ? 0 : 1); + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 det_offset = data_offset + fs_header->fs_info.dirtable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fht_offset = fs_header_offset + fs_header->fs_info.file_hashtbl.outfat_offset; + const u32 fat_offset = fs_header_offset + fs_header->fs_info.fat.outfat_offset; + const u32 size_blocks = (size / fs_header->fs_info.data_region_blocksize) + (((size % fs_header->fs_info.data_region_blocksize) == 0) ? 0 : 1); u32 index = 0, max_index = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; u64 tid_be = getbe64(title_id); u8* title_id_be = (u8*) &tid_be; bool do_replace = false; @@ -377,7 +490,7 @@ static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offse index = file_entry.next_sibling_index; max_index = max(index, max_index); - if (BDRIRead(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; // If an entry for the tid already existed that is already the specified size and replace was specified, just replace the existing entry @@ -395,101 +508,11 @@ static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offse u32 fat_index = 0; if (!do_replace) { - if (BDRIRead(fat_offset, FAT_ENTRY_SIZE, fat_entry) != FR_OK) + if (AllocateBDRIBlocks(fs_header, fs_header_offset, size_blocks, &fat_index) != 0) return 1; - - if (getfatflag(fat_entry[1]) || (fat_entry[0] != 0)) - return 1; - - u32 next_fat_index = getfatindex(fat_entry[1]), node_size = 0; - - // Find contiguous free space in the FAT for the entry. Technically there could be a case of enough space existing, but not in a contiguous fasion, but this would never realistically happen - do { - if (next_fat_index == 0) - return 1; // Reached the end of the free node chain without finding enough contiguous free space - this should never realistically happen - - fat_index = next_fat_index; - - if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - next_fat_index = getfatindex(fat_entry[1]); - - if (getfatflag(fat_entry[1])) { // Multi-entry node - if (BDRIRead(fat_offset + (fat_index + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - if (!getfatflag(fat_entry[0]) || getfatflag(fat_entry[1]) || (getfatindex(fat_entry[0]) != fat_index) || (getfatindex(fat_entry[0]) >= getfatindex(fat_entry[1]))) - return 1; - - node_size = getfatindex(fat_entry[1]) + 1 - fat_index; - } else { // Single-entry node - node_size = 1; - } - } while (node_size < size_blocks); - - const bool shrink_free_node = node_size > size_blocks; - - if (shrink_free_node) { - if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - if (node_size - size_blocks == 1) - fat_entry[1] = buildfatuv(getfatindex(fat_entry[1]), false); - - if (BDRIWrite(fat_offset + (fat_index + size_blocks) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - if (node_size - size_blocks > 1) { - if (BDRIRead(fat_offset + (fat_index + node_size - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - fat_entry[0] = buildfatuv(fat_index + size_blocks, true); - - if ((BDRIWrite(fat_offset + (fat_index + node_size - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) || - (BDRIWrite(fat_offset + (fat_index + size_blocks + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK)) - return 1; - } - } - - if (BDRIRead(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - const u32 previous_free_index = getfatindex(fat_entry[0]), next_free_index = getfatindex(fat_entry[1]); - - fat_entry[0] = buildfatuv(0, true); - fat_entry[1] = buildfatuv(0, size_blocks > 1); - - if (BDRIWrite(fat_offset + fat_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - if (size_blocks > 1) { - fat_entry[0] = buildfatuv(fat_index, true); - fat_entry[1] = buildfatuv(fat_index + size_blocks - 1, false); - - if ((BDRIWrite(fat_offset + (fat_index + size_blocks - 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) || - (BDRIWrite(fat_offset + (fat_index + 1) * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK)) - return 1; - } - - if (next_free_index != 0) { - if (BDRIRead(fat_offset + next_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - fat_entry[0] = buildfatuv(shrink_free_node ? fat_index + size_blocks : previous_free_index, (!shrink_free_node && (previous_free_index == 0))); - - if (BDRIWrite(fat_offset + next_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - } - - if (BDRIRead(fat_offset + previous_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - - fat_entry[1] = buildfatuv(shrink_free_node ? fat_index + size_blocks : next_free_index, getfatflag(fat_entry[1])); - - if (BDRIWrite(fat_offset + previous_free_index * FAT_ENTRY_SIZE, FAT_ENTRY_SIZE, fat_entry) != FR_OK) - return 1; - } else fat_index = file_entry.start_block_index + 1; + } else { + fat_index = file_entry.start_block_index + 1; + } u32 bytes_written = 0, fat_index_write = fat_index; @@ -520,21 +543,21 @@ static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offse fat_index_write = next_index; - u32 btw = min(size - bytes_written, write_count * fs_header->data_block_size); - if (BDRIWrite(data_offset + write_start * fs_header->data_block_size, btw, entry + bytes_written) != FR_OK) + u32 btw = min(size - bytes_written, write_count * fs_header->fs_info.data_region_blocksize); + if (BDRIWrite(data_offset + write_start * fs_header->fs_info.data_region_blocksize, btw, entry + bytes_written) != FR_OK) return 1; bytes_written += btw; } if (!do_replace) { - DummyFileEntry dummy_entry; + BDRIDummyFileEntry dummy_entry; // Read the 0th entry in the FET, which is always a dummy entry - if (BDRIRead(fet_offset, sizeof(DummyFileEntry), &dummy_entry) != FR_OK) + if (BDRIRead(fet_offset, sizeof(BDRIDummyFileEntry), &dummy_entry) != FR_OK) return 1; - if (dummy_entry.max_entry_count != fs_header->max_file_count + 1) + if (dummy_entry.max_entry_count != fs_header->fs_info.filetable_info.max_entry_count + 1) return 1; if (dummy_entry.next_dummy_index == 0) { // If the 0th entry is the only dummy entry, make a new entry @@ -546,31 +569,31 @@ static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offse } else { // If there's at least one extraneous dummy entry, replace it file_entry.next_sibling_index = dummy_entry.next_dummy_index; - if ((BDRIRead(fet_offset + dummy_entry.next_dummy_index * sizeof(DummyFileEntry), sizeof(DummyFileEntry), &dummy_entry) != FR_OK) || - (BDRIWrite(fet_offset, sizeof(DummyFileEntry), &dummy_entry) != FR_OK)) + if ((BDRIRead(fet_offset + dummy_entry.next_dummy_index * sizeof(BDRIDummyFileEntry), sizeof(BDRIDummyFileEntry), &dummy_entry) != FR_OK) || + (BDRIWrite(fet_offset, sizeof(BDRIDummyFileEntry), &dummy_entry) != FR_OK)) return 1; } - if (BDRIWrite((index == 0) ? det_offset + 0x2C : fet_offset + index * sizeof(TdbFileEntry) + 0xC, sizeof(u32), &(file_entry.next_sibling_index)) != FR_OK) + if (BDRIWrite((index == 0) ? det_offset + 0x2C : fet_offset + index * sizeof(BDRIFileEntry) + 0xC, sizeof(u32), &(file_entry.next_sibling_index)) != FR_OK) return 1; index = file_entry.next_sibling_index; - const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fht_bucket_count); + const u32 hash_bucket = GetHashBucket(title_id_be, 1, fs_header->fs_info.file_hashtbl.outfat_count); u32 index_hash = 0; if ((BDRIRead(fht_offset + hash_bucket * sizeof(u32), sizeof(u32), &index_hash) != FR_OK) || (BDRIWrite(fht_offset + hash_bucket * sizeof(u32), sizeof(u32), &index) != FR_OK)) return 1; - memset(&file_entry, 0, sizeof(TdbFileEntry)); + memset(&file_entry, 0, sizeof(BDRIFileEntry)); file_entry.parent_index = 1; memcpy(file_entry.title_id, title_id_be, 8); file_entry.start_block_index = fat_index - 1; file_entry.size = (u64) size; file_entry.hash_bucket_next_index = index_hash; - if (BDRIWrite(fet_offset + index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIWrite(fet_offset + index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; } @@ -578,15 +601,16 @@ static u32 AddBDRIEntry(const BDRIFsHeader* fs_header, const u32 fs_header_offse } static u32 GetNumBDRIEntries(const BDRIFsHeader* fs_header, const u32 fs_header_offset) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) // Could be more thorough + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) // Could be more thorough return 0; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 det_offset = data_offset + fs_header->det_start_block * fs_header->data_block_size; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 det_offset = data_offset + fs_header->fs_info.dirtable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; u32 num_entries = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; // Read the index of the first file entry from the directory entry table if (BDRIRead(det_offset + 0x2C, sizeof(u32), &(file_entry.next_sibling_index)) != FR_OK) @@ -594,7 +618,7 @@ static u32 GetNumBDRIEntries(const BDRIFsHeader* fs_header, const u32 fs_header_ while (file_entry.next_sibling_index != 0) { num_entries++; - if (BDRIRead(fet_offset + file_entry.next_sibling_index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + file_entry.next_sibling_index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 0; } @@ -602,15 +626,16 @@ static u32 GetNumBDRIEntries(const BDRIFsHeader* fs_header, const u32 fs_header_ } static u32 ListBDRIEntryTitleIDs(const BDRIFsHeader* fs_header, const u32 fs_header_offset, u8* title_ids, u32 max_title_ids) { - if ((fs_header->info_offset != 0x20) || (fs_header->fat_entry_count != fs_header->data_block_count)) + if ((fs_header->pre_header.fs_info_offset != 0x20) || + (fs_header->fs_info.fat.outfat_count != fs_header->fs_info.data_region.outfat_count)) return 0; - const u32 data_offset = fs_header_offset + fs_header->data_offset; - const u32 det_offset = data_offset + fs_header->det_start_block * fs_header->data_block_size; - const u32 fet_offset = data_offset + fs_header->fet_start_block * fs_header->data_block_size; + const u32 data_offset = fs_header_offset + fs_header->fs_info.data_region.outfat_offset; + const u32 det_offset = data_offset + fs_header->fs_info.dirtable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; + const u32 fet_offset = data_offset + fs_header->fs_info.filetable_info.starting_block_index * fs_header->fs_info.data_region_blocksize; u32 num_entries = 0; - TdbFileEntry file_entry; + BDRIFileEntry file_entry; for (u32 i = 0; i < max_title_ids; i++) memset(title_ids, 0, max_title_ids * 8); @@ -620,7 +645,7 @@ static u32 ListBDRIEntryTitleIDs(const BDRIFsHeader* fs_header, const u32 fs_hea return 1; while ((file_entry.next_sibling_index != 0) && (num_entries < max_title_ids)) { - if (BDRIRead(fet_offset + file_entry.next_sibling_index * sizeof(TdbFileEntry), sizeof(TdbFileEntry), &file_entry) != FR_OK) + if (BDRIRead(fet_offset + file_entry.next_sibling_index * sizeof(BDRIFileEntry), sizeof(BDRIFileEntry), &file_entry) != FR_OK) return 1; u64 tid_be = getbe64(file_entry.title_id); @@ -634,21 +659,21 @@ static u32 ListBDRIEntryTitleIDs(const BDRIFsHeader* fs_header, const u32 fs_hea u32 GetNumTitleInfoEntries(const char* path) { FIL file; - TitleDBPreHeader pre_header; + TitleDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) return 0; bdrifp = &file; - if ((BDRIRead(0, sizeof(TitleDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TitleDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, false)) { fvx_close(bdrifp); bdrifp = NULL; return 0; } - u32 num = GetNumBDRIEntries(&(pre_header.fs_header), sizeof(TitleDBPreHeader) - sizeof(BDRIFsHeader)); + u32 num = GetNumBDRIEntries(&(pre_header.fs_header), sizeof(TitleDBHeader) - sizeof(BDRIFsHeader)); fvx_close(bdrifp); bdrifp = NULL; @@ -657,21 +682,21 @@ u32 GetNumTitleInfoEntries(const char* path) { u32 GetNumTickets(const char* path) { FIL file; - TickDBPreHeader pre_header; + TickDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) return 0; bdrifp = &file; - if ((BDRIRead(0, sizeof(TickDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TickDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, true)) { fvx_close(bdrifp); bdrifp = NULL; return 0; } - u32 num = GetNumBDRIEntries(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader)); + u32 num = GetNumBDRIEntries(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader)); fvx_close(bdrifp); bdrifp = NULL; @@ -680,16 +705,16 @@ u32 GetNumTickets(const char* path) { u32 ListTitleInfoEntryTitleIDs(const char* path, u8* title_ids, u32 max_title_ids) { FIL file; - TitleDBPreHeader pre_header; + TitleDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TitleDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TitleDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, false) || - (ListBDRIEntryTitleIDs(&(pre_header.fs_header), sizeof(TitleDBPreHeader) - sizeof(BDRIFsHeader), title_ids, max_title_ids) != 0)) { + (ListBDRIEntryTitleIDs(&(pre_header.fs_header), sizeof(TitleDBHeader) - sizeof(BDRIFsHeader), title_ids, max_title_ids) != 0)) { fvx_close(bdrifp); bdrifp = NULL; return 1; @@ -702,16 +727,16 @@ u32 ListTitleInfoEntryTitleIDs(const char* path, u8* title_ids, u32 max_title_id u32 ListTicketTitleIDs(const char* path, u8* title_ids, u32 max_title_ids) { FIL file; - TickDBPreHeader pre_header; + TickDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TickDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TickDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, true) || - (ListBDRIEntryTitleIDs(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_ids, max_title_ids) != 0)) { + (ListBDRIEntryTitleIDs(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_ids, max_title_ids) != 0)) { fvx_close(bdrifp); bdrifp = NULL; return 1; @@ -724,16 +749,16 @@ u32 ListTicketTitleIDs(const char* path, u8* title_ids, u32 max_title_ids) { u32 ReadTitleInfoEntryFromDB(const char* path, const u8* title_id, TitleInfoEntry* tie) { FIL file; - TitleDBPreHeader pre_header; + TitleDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TitleDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TitleDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, false) || - (ReadBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBPreHeader) - sizeof(BDRIFsHeader), title_id, (u8*) tie, + (ReadBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBHeader) - sizeof(BDRIFsHeader), title_id, (u8*) tie, sizeof(TitleInfoEntry)) != 0)) { fvx_close(bdrifp); bdrifp = NULL; @@ -747,7 +772,7 @@ u32 ReadTitleInfoEntryFromDB(const char* path, const u8* title_id, TitleInfoEntr u32 ReadTicketFromDB(const char* path, const u8* title_id, Ticket** ticket) { FIL file; - TickDBPreHeader pre_header; + TickDBHeader pre_header; TicketEntry* te = NULL; u32 entry_size; @@ -757,12 +782,12 @@ u32 ReadTicketFromDB(const char* path, const u8* title_id, Ticket** ticket) { bdrifp = &file; - if ((BDRIRead(0, sizeof(TickDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TickDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, true) || - (GetBDRIEntrySize(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id, &entry_size) != 0) || + (GetBDRIEntrySize(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id, &entry_size) != 0) || entry_size < sizeof(TicketEntry) + 0x14 || (te = (TicketEntry*)malloc(entry_size), te == NULL) || - (ReadBDRIEntry(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id, (u8*) te, + (ReadBDRIEntry(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id, (u8*) te, entry_size) != 0)) { free(te); // if allocated fvx_close(bdrifp); @@ -793,16 +818,16 @@ u32 ReadTicketFromDB(const char* path, const u8* title_id, Ticket** ticket) { u32 RemoveTitleInfoEntryFromDB(const char* path, const u8* title_id) { FIL file; - TitleDBPreHeader pre_header; + TitleDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TitleDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TitleDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, false) || - (RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBPreHeader) - sizeof(BDRIFsHeader), title_id) != 0)) { + (RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBHeader) - sizeof(BDRIFsHeader), title_id) != 0)) { fvx_close(bdrifp); bdrifp = NULL; return 1; @@ -815,16 +840,16 @@ u32 RemoveTitleInfoEntryFromDB(const char* path, const u8* title_id) { u32 RemoveTicketFromDB(const char* path, const u8* title_id) { FIL file; - TickDBPreHeader pre_header; + TickDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TickDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TickDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, true) || - (RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id) != 0)) { + (RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id) != 0)) { fvx_close(bdrifp); bdrifp = NULL; return 1; @@ -837,16 +862,16 @@ u32 RemoveTicketFromDB(const char* path, const u8* title_id) { u32 AddTitleInfoEntryToDB(const char* path, const u8* title_id, const TitleInfoEntry* tie, bool replace) { FIL file; - TitleDBPreHeader pre_header; + TitleDBHeader pre_header; if (fvx_open(&file, path, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) return 1; bdrifp = &file; - if ((BDRIRead(0, sizeof(TitleDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TitleDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, false) || - (AddBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBPreHeader) - sizeof(BDRIFsHeader), title_id, + (AddBDRIEntry(&(pre_header.fs_header), sizeof(TitleDBHeader) - sizeof(BDRIFsHeader), title_id, (const u8*) tie, sizeof(TitleInfoEntry), replace) != 0)) { fvx_close(bdrifp); bdrifp = NULL; @@ -860,7 +885,7 @@ u32 AddTitleInfoEntryToDB(const char* path, const u8* title_id, const TitleInfoE u32 AddTicketToDB(const char* path, const u8* title_id, const Ticket* ticket, bool replace) { FIL file; - TickDBPreHeader pre_header; + TickDBHeader pre_header; u32 entry_size = sizeof(TicketEntry) + GetTicketContentIndexSize(ticket); TicketEntry* te = (TicketEntry*)malloc(entry_size); @@ -868,7 +893,7 @@ u32 AddTicketToDB(const char* path, const u8* title_id, const Ticket* ticket, bo return 1; } - te->unknown = 1; + te->ticket_count = 1; te->ticket_size = GetTicketSize(ticket); memcpy(&te->ticket, ticket, te->ticket_size); if (fvx_open(&file, path, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) { @@ -880,12 +905,12 @@ u32 AddTicketToDB(const char* path, const u8* title_id, const Ticket* ticket, bo u32 add_bdri_res = 0; - if ((BDRIRead(0, sizeof(TickDBPreHeader), &pre_header) != FR_OK) || + if ((BDRIRead(0, sizeof(TickDBHeader), &pre_header) != FR_OK) || !CheckDBMagic((u8*) &pre_header, true) || - ((add_bdri_res = AddBDRIEntry(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id, + ((add_bdri_res = AddBDRIEntry(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id, (const u8*) te, entry_size, replace)) == 1) || - (add_bdri_res == REPLACE_SIZE_MISMATCH && ((RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id) != 0) || - (AddBDRIEntry(&(pre_header.fs_header), sizeof(TickDBPreHeader) - sizeof(BDRIFsHeader), title_id, + (add_bdri_res == REPLACE_SIZE_MISMATCH && ((RemoveBDRIEntry(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id) != 0) || + (AddBDRIEntry(&(pre_header.fs_header), sizeof(TickDBHeader) - sizeof(BDRIFsHeader), title_id, (const u8*) te, entry_size, replace) != 0)))) { free(te); fvx_close(bdrifp); @@ -898,3 +923,396 @@ u32 AddTicketToDB(const char* path, const u8* title_id, const Ticket* ticket, bo bdrifp = NULL; return 0; } + +static u32 CalcNumHashBuckets(u32 num_entries) +{ + if (num_entries <= 3) + return 3; + + if (num_entries <= 19) + return num_entries | 1; + + for (int i = 0; i < 100; ++i) + { + u32 ret = num_entries + i; + + if (ret & 1 && ret % 3 && ret % 5 && ret % 7 && ret % 11 && ret % 13 && ret % 17) + return ret; + } + + return num_entries | 1; +} + +u32 CreateBDRI(const char *path, u64 image_offset, u64 image_size, u32 blocksize, u32 num_files) { + BDRIFsHeader bdri; + memset(&bdri, 0, sizeof(BDRIFsHeader)); + + IFatPreHeader *pre_header = &bdri.pre_header; + IFatFsInfo *fs_info = &bdri.fs_info; + + const u64 img_num_blocks = image_size / blocksize; + const u32 file_hashbucket_count = CalcNumHashBuckets(num_files); + + memcpy(pre_header->magic_str, "BDRI", 4); + pre_header->version = 0x30000; + pre_header->fs_image_blocksize = blocksize; + pre_header->fs_image_size_blocks = img_num_blocks; + pre_header->fs_info_offset = sizeof(IFatPreHeader); + + const u64 full_image_size = pre_header->fs_image_blocksize * pre_header->fs_image_size_blocks; + + u32 num_data_blocks = img_num_blocks; + u32 data_region_offs = 0; + + // dynamically adjust the number of data region blocks to fit inside the given image size + while (1) { + if (!num_data_blocks) + return 1; // image will not fit inside given size + + data_region_offs = + align_pow2( + sizeof(IFatPreHeader) + sizeof(IFatFsInfo) + /* headers */ + (4 * 1) + (4 * file_hashbucket_count) + /* hashtables (dir, file) */ + (num_data_blocks + 1) * sizeof(IFatEntry), /* FAT entries, including the freelist entry */ + blocksize); + + if (full_image_size >= (num_data_blocks * blocksize) + data_region_offs) + break; // found a satisfactory number of data blocks + + --num_data_blocks; + } + + // create file system info + + fs_info->data_region_blocksize = blocksize; + + fs_info->dir_hashtbl.outfat_offset = pre_header->fs_info_offset + sizeof(IFatFsInfo); + fs_info->dir_hashtbl.outfat_count = 1; + + fs_info->file_hashtbl.outfat_offset = fs_info->dir_hashtbl.outfat_offset + 4; + fs_info->file_hashtbl.outfat_count = file_hashbucket_count; + + fs_info->fat.outfat_offset = fs_info->file_hashtbl.outfat_offset + file_hashbucket_count * 4; + fs_info->fat.outfat_count = num_data_blocks; + + fs_info->data_region.outfat_offset = data_region_offs; + fs_info->data_region.outfat_count = num_data_blocks; + + // dummy freelist dir entry [0] + root dir entry [1] + uint32_t dir_entries_num_blocks = ceil_div(2 * sizeof(BDRIDirectoryEntry), blocksize); + + // dummy freelist file entry [0] + file entries [n] + uint32_t file_entries_num_blocks = ceil_div((num_files + 1) * sizeof(BDRIFileEntry), blocksize); + + fs_info->dirtable_info.max_entry_count = 1; + fs_info->dirtable_info.block_count = dir_entries_num_blocks; + fs_info->dirtable_info.starting_block_index = 0xFFFFFFFF; // to be allocated in FAT later using above block_count + + fs_info->filetable_info.max_entry_count = num_files; + fs_info->filetable_info.block_count = file_entries_num_blocks; + fs_info->filetable_info.starting_block_index = 0xFFFFFFFF; // to be allocated in FAT later using above block_count + + FIL file; + + if (fvx_open(&file, path, FA_READ | FA_WRITE | FA_OPEN_EXISTING) != FR_OK) { + bdrifp = NULL; + return 1; + } + + bdrifp = &file; + + // create new FAT + + u32 fatentries[2][2]; + + fatentries[0][0] = buildfatuv(0, 0); + fatentries[0][1] = buildfatuv(1, 0); + + fatentries[1][0] = buildfatuv(0, true); + fatentries[1][1] = buildfatuv(0, num_data_blocks > 1); + +#define FATOFFSET(n) (image_offset + fs_info->fat.outfat_offset + (n) * FAT_ENTRY_SIZE) + + if (BDRIWrite(FATOFFSET(0), FAT_ENTRY_SIZE * 2, fatentries) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + if (num_data_blocks > 1) { + u32 *fatentry = fatentries[0]; + + fatentry[0] = buildfatuv(1, true); + fatentry[1] = buildfatuv(num_data_blocks, false); + + if (BDRIWrite(FATOFFSET(2), FAT_ENTRY_SIZE, fatentry) != FR_OK || + BDRIWrite(FATOFFSET(num_data_blocks), FAT_ENTRY_SIZE, fatentry) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + } + + // allocate file and directory entry tables in FAT + + if (AllocateBDRIBlocks(&bdri, image_offset, dir_entries_num_blocks, &fs_info->dirtable_info.starting_block_index) != 0 || + AllocateBDRIBlocks(&bdri, image_offset, file_entries_num_blocks, &fs_info->filetable_info.starting_block_index) != 0) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + // because of the extra freelist sentinel in fat[0], the actual data region index is fat_index - 1 + fs_info->dirtable_info.starting_block_index -= 1; + fs_info->filetable_info.starting_block_index -= 1; + + if (BDRIWrite(image_offset, sizeof(BDRIFsHeader), &bdri) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + // initialize file/dir hash tables & entries + + // the directory hashtable has just one entry, pointing to the root directory + // no need to do the calculation + u32 dir_hash_value = 1; + + if (BDRIWrite(image_offset + fs_info->dir_hashtbl.outfat_offset, sizeof(u32), &dir_hash_value) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + u8 zero[0x200]; + memset(zero, 0, sizeof(zero)); + + u32 remaining_fht = file_hashbucket_count * 4; + u32 fht_cur_offset = fs_info->file_hashtbl.outfat_offset; + + // fill the file hashtable with zeros + while (remaining_fht) { + u32 writesize = min(remaining_fht, sizeof(zero)); + + if (BDRIWrite(image_offset + fht_cur_offset, writesize, zero) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + remaining_fht -= writesize; + fht_cur_offset += writesize; + } + + // initialize directory entry table + BDRIDummyDirectoryEntry dummy_dir; + BDRIDirectoryEntry root_dir; + memset(&dummy_dir, 0, sizeof(dummy_dir)); + memset(&root_dir, 0, sizeof(root_dir)); + + dummy_dir.next_dummy_index = 0; + dummy_dir.total_entry_count = 2; // this (dummy), and root (the entry after) + dummy_dir.max_entry_count = 3; // even though only two are "used" + + u64 det_offset = fs_info->data_region.outfat_offset + fs_info->dirtable_info.starting_block_index * fs_info->data_region_blocksize; + + if (BDRIWrite(image_offset + det_offset, sizeof(dummy_dir), &dummy_dir) != FR_OK || + BDRIWrite(image_offset + det_offset + sizeof(dummy_dir), sizeof(root_dir), &root_dir) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + // initialize file entry table + BDRIDummyFileEntry dummy_file; + memset(&dummy_file, 0, sizeof(dummy_file)); + + dummy_file.next_dummy_index = 0; + dummy_file.total_entry_count = 1; // currently, just this (dummy) + dummy_file.max_entry_count = num_files + 1; // this (dummy), and however many file entries should exist + + u64 fet_offset = fs_info->data_region.outfat_offset + fs_info->filetable_info.starting_block_index * fs_info->data_region_blocksize; + + if (BDRIWrite(image_offset + fet_offset, sizeof(dummy_file), &dummy_file) != FR_OK) { + fvx_close(&file); + bdrifp = NULL; + return 1; + } + + fvx_close(&file); + bdrifp = NULL; + + return 0; +} + +typedef struct DbFileConfig { + const char *path; + const char *magic; + u32 version; + u32 full_data_size; + u32 image_offset; // also pre-header size + u32 image_size; + u32 image_blocksize; + u32 num_entries; +} DbFileConfig; + +typedef enum DbType { + NAND_TICKET_DB = 0, NAND_IMPORT_DB, NAND_TITLE_DB, NAND_TMP_I_DB, NAND_TMP_T_DB, + SDMC_TITLE_DB, SDMC_IMPORT_DB, +} DbType; + +static const DbFileConfig DbConfigs[7] = { + // NAND dbs + { "/dbs/ticket.db", "TICK" , 1, 0x10A2010, 0x10, 0x10A2000, 0x200, 8192 }, // ticket.db is the only db where the image size is correct... + { "/dbs/import.db", "NANDIDB", 0, 0x17800 , 0x80, 0x17800 , 0x80 , 512 }, // --- + { "/dbs/title.db" , "NANDTDB", 0, 0x2ED80 , 0x80, 0x2ED80 , 0x80, 1024 }, // | *all* of these are missing 0x80 bytes at the end. + { "/dbs/tmp_i.db" , "TEMPIDB", 0, 0xE00 , 0x80, 0xE00 , 0x80, 16 }, // | the image size should be file size - 0x80 because + { "/dbs/tmp_t.db" , "TEMPIDB", 0, 0x6000 , 0x80, 0x6000 , 0x80, 128 }, // | of the pre-header. keeping it the same just for + // SDMC dbs // | consistency with Process9, really. + { "/dbs/title.db" , "TEMPTDB", 0, 0x175A80 , 0x80, 0x175A80 , 0x80, 8192 }, // | + { "/dbs/import.db", "TEMPTDB", 0, 0x175A80 , 0x80, 0x175A80 , 0x80 , 8192 }, // --- +}; + +static const DbFileConfig *GetDbFileConfigBuildPath(char *outpath, u32 pathsize, u32 type, bool emunand) { + if (type >= countof(DbConfigs)) + return NULL; + + bool nand = type <= NAND_TMP_T_DB; + + char dest_drive = + nand ? + emunand ? '4' : '1' : + emunand ? 'B' : 'A'; + + const DbFileConfig *config = &DbConfigs[type]; + + snprintf(outpath, pathsize, "%c:%s", dest_drive, config->path); + + return config; +} + +u32 CreateDbFile(u32 type, bool emunand) { + char path[256] = { 0 }; + + const DbFileConfig *config = GetDbFileConfigBuildPath(path, sizeof(path), type, emunand); + if (!config) + return 1; + + u64 reqsize = BuildDiffCalcRequiredSize(config->full_data_size, false, true); + + if (!CheckWritePermissions(path)) + return 1; + + // ensure remounting the old mount path + char path_store[256] = { 0 }; + char* path_bak = NULL; + strncpy(path_store, GetMountPath(), 256); + if (*path_store) path_bak = path_store; + + // unmount any mounted image since we're about to create a DIFF that will need to be mounted + InitImgFS(NULL); + + // create the raw file + if (fvx_qcreate(path, reqsize) != FR_OK) { + InitImgFS(path_bak); + return 1; + } + + // create the DIFF container and mount it + if (CreateDiff(path, config->full_data_size, false, true, NULL) != 0 || + !InitImgFS(path)) { + InitImgFS(path_bak); + fvx_unlink(path); + return 1; + } + + char db_hdr[0x80]; + memset(db_hdr, 0, sizeof(db_hdr)); + u32 version_offset = align_pow2(strlen(config->magic), 4); + memcpy(db_hdr, config->magic, strlen(config->magic)); + memcpy(&db_hdr[version_offset], &config->version, 4); + + UINT written = 0; + + // write the BDRI filesystem image + if (fvx_qwrite("D:/partitionA.bin", db_hdr, 0, config->image_offset, &written) != 0 || + CreateBDRI("D:/partitionA.bin", config->image_offset, config->image_size, config->image_blocksize, config->num_entries) != 0) { + InitImgFS(path_bak); + fvx_unlink(path); + return 1; + } + + // remount previously mounted image, if there was one + InitImgFS(path_bak); + + // make sure CMAC is fixed + if (FixFileCmac(path, true) != 0) { + fvx_unlink(path); + return 1; + } + + return 0; +} + +u32 CreateDbFilesForDrive(const char *destdrv, bool silent, bool force_overwrite) { + static const DbType db_types_nand[5] = { NAND_TICKET_DB, NAND_IMPORT_DB, NAND_TITLE_DB, NAND_TMP_I_DB, NAND_TMP_T_DB }; + static const DbType db_types_sdmc[2] = { SDMC_TITLE_DB, SDMC_IMPORT_DB }; + + if (*destdrv != 'A' && *destdrv != 'B' && *destdrv != '1' && *destdrv != '4') { + return 1; + } + + bool emunand = *destdrv == 'B' /* EmuNAND SD */ || *destdrv == '4' /* EmuNAND CTRNAND */; + bool sd = *destdrv == 'A' || *destdrv == 'B' /* Sys/EmuNAND SD */; + const DbType *dbs_to_check = sd ? db_types_sdmc : db_types_nand; + u32 db_count = sd ? countof(db_types_sdmc) : countof(db_types_nand); + + u32 num_to_create = db_count; + u32 num_already_exist = 0; + u32 num_failed = 0; + u32 num_created_ok = 0; + + char db_path[256] = { 0 }; + const DbFileConfig *config = NULL; + + for (u32 i = 0; i < db_count; i++) { + if (!(config = GetDbFileConfigBuildPath(db_path, sizeof(db_path), dbs_to_check[i], emunand))) { + return 1; + } + + if (fvx_stat(db_path, NULL) == FR_OK) { + if (!force_overwrite && fvx_qsize(db_path) == BuildDiffCalcRequiredSize(config->full_data_size, false, true)) { + ++num_already_exist; + --num_to_create; + continue; + } + } + + if (CreateDbFile(dbs_to_check[i], emunand) == 0) { + ++num_created_ok; + } else { + ++num_failed; + if (!silent) { + ShowPrompt(false, STR_PATH_CREATE_DB_FILE_FAILED, db_path); + } + } + } + + if (!silent) { + if (num_already_exist) { + if (num_already_exist == db_count) { + ShowPrompt(false, "%s", STR_NO_MISSING_DB_FILES_DETECTED); + } else { + ShowPrompt(false, STR_CREATE_DB_FILES_N_N_N_OK_FAILED_ALREADY_EXIST, num_created_ok, num_failed, num_already_exist); + } + } else { + if (num_created_ok == db_count) { + ShowPrompt(false, "%s", STR_SUCCESSFULLY_CREATED_DB_FILES_FOR_DRIVE); + } else { + ShowPrompt(false, STR_CREATE_DB_FILES_N_N_OK_FAILED, num_created_ok, num_failed); + } + } + } + + return num_to_create == num_created_ok ? 0 : 1; +} \ No newline at end of file diff --git a/arm9/source/game/bdri.h b/arm9/source/game/bdri.h index 0dc4c65..f71e103 100644 --- a/arm9/source/game/bdri.h +++ b/arm9/source/game/bdri.h @@ -16,3 +16,6 @@ u32 RemoveTitleInfoEntryFromDB(const char* path, const u8* title_id); u32 RemoveTicketFromDB(const char* path, const u8* title_id); u32 AddTitleInfoEntryToDB(const char* path, const u8* title_id, const TitleInfoEntry* tie, bool replace); u32 AddTicketToDB(const char* path, const u8* title_id, const Ticket* ticket, bool replace); + +u32 CreateBDRI(const char *path, u64 image_offset, u64 image_size, u32 blocksize, u32 num_files); +u32 CreateDbFilesForDrive(const char *destdrv, bool silent, bool force_overwrite); \ No newline at end of file diff --git a/arm9/source/game/disadiff.c b/arm9/source/game/disadiff.c index 295258c..423723f 100644 --- a/arm9/source/game/disadiff.c +++ b/arm9/source/game/disadiff.c @@ -4,6 +4,8 @@ #include "sha.h" #define GET_DPFS_BIT(b, lvl) (((((u32*) (void*) lvl)[b >> 5]) >> (31 - (b % 32))) & 1) +#define LVL(h,n) ((h)->level[(n) - 1]) +#define L(n) ((n) - 1) typedef struct { u8 magic[8]; // "DISA" 0x00040000 @@ -54,40 +56,27 @@ typedef struct { } PACKED_STRUCT DifiHeader; typedef struct { - u8 magic[8]; // "IVFC" 0x00020000 - u64 size_hash; // same as the one in DIFI, may include padding - u64 offset_lvl1; - u64 size_lvl1; - u32 log_lvl1; + u64 offset; + u64 size; + u32 blocksize_log2; u8 padding0[4]; - u64 offset_lvl2; - u64 size_lvl2; - u32 log_lvl2; - u8 padding1[4]; - u64 offset_lvl3; - u64 size_lvl3; - u32 log_lvl3; - u8 padding2[4]; - u64 offset_lvl4; - u64 size_lvl4; - u64 log_lvl4; - u64 size_ivfc; // 0x78 -} PACKED_STRUCT IvfcDescriptor; +} PACKED_STRUCT IvfcLevel; typedef struct { - u8 magic[8]; // "DPFS" 0x00010000 - u64 offset_lvl1; - u64 size_lvl1; - u32 log_lvl1; - u8 padding0[4]; - u64 offset_lvl2; - u64 size_lvl2; - u32 log_lvl2; - u8 padding1[4]; - u64 offset_lvl3; - u64 size_lvl3; - u32 log_lvl3; - u8 padding2[4]; + u8 magic[8]; // "IVFC" 0x00020000 + u32 size_hash;// same as the one in DIFI + u8 padding0[4]; + IvfcLevel level[4]; + u32 size_ivfc; + u32 offset_tree; +} PACKED_STRUCT IvfcDescriptor; + +// same layout +typedef IvfcLevel DpfsLevel; + +typedef struct { + u8 magic[8]; // "DPFS" 0x00010000 + DpfsLevel level[3]; } PACKED_STRUCT DpfsDescriptor; typedef struct { @@ -98,6 +87,52 @@ typedef struct { u8 padding[4]; // all zeroes when encrypted } PACKED_STRUCT DifiStruct; +STATIC_ASSERT(sizeof(DifiHeader) == 0x44); +STATIC_ASSERT(sizeof(IvfcDescriptor) == 0x78); +STATIC_ASSERT(sizeof(DpfsDescriptor) == 0x50); +STATIC_ASSERT(sizeof(DiffHeader) == 0x100); + +static const u8 disa_magic[] = { DISA_MAGIC }; +static const u8 diff_magic[] = { DIFF_MAGIC }; +static const u8 ivfc_magic[] = { IVFC_MAGIC }; +static const u8 dpfs_magic[] = { DPFS_MAGIC }; +static const u8 difi_magic[] = { DIFI_MAGIC }; + +typedef struct { + u32 ivfc_blocksizes[4]; + u32 dpfs_lv2_blocksize; + u32 dpfs_lv3_blocksize; +} IvfcDpfsConfig; + +static const IvfcDpfsConfig SaveExsvIvfcDpfsConfig = { + .ivfc_blocksizes = { 0x200, 0x200, 0x1000, 0x1000 }, + .dpfs_lv2_blocksize = 0x80, + .dpfs_lv3_blocksize = 0x1000, +}; + +static const IvfcDpfsConfig DbIvfcDpfsConfig = { + .ivfc_blocksizes = { 0x200, 0x200, 0x200, 0x200 }, + .dpfs_lv2_blocksize = 0x80, + .dpfs_lv3_blocksize = 0x200, +}; + +const IvfcDpfsConfig *GetIvfcDpfsConfig(bool db) { + return db ? &DbIvfcDpfsConfig : &SaveExsvIvfcDpfsConfig; +} + +u32 CalcIvfcDpfsConfigBlockSize(const IvfcDpfsConfig *config) { + // calculates the largest block size across both IVFC and DPFS + u32 ret = 0; + + for (int i = 1; i < 4 + 1; i++) + ret |= (config->ivfc_blocksizes[L(i)] - 1); + + ret |= config->dpfs_lv2_blocksize - 1; + ret |= config->dpfs_lv3_blocksize - 1; + + return ret + 1; +} + static FIL ddfile; static FIL* ddfp = NULL; @@ -159,12 +194,6 @@ inline static FRESULT DisaDiffQWrite(const TCHAR* path, const void* buf, UINT of } u32 GetDisaDiffRWInfo(const char* path, DisaDiffRWInfo* info, bool partitionB) { - static const u8 disa_magic[] = { DISA_MAGIC }; - static const u8 diff_magic[] = { DIFF_MAGIC }; - static const u8 ivfc_magic[] = { IVFC_MAGIC }; - static const u8 dpfs_magic[] = { DPFS_MAGIC }; - static const u8 difi_magic[] = { DIFI_MAGIC }; - // reset reader info memset(info, 0x00, sizeof(DisaDiffRWInfo)); @@ -200,6 +229,7 @@ u32 GetDisaDiffRWInfo(const char* path, DisaDiffRWInfo* info, bool partitionB) { DiffHeader* diff = (DiffHeader*) (void*) header; offset_partition = (u32) diff->offset_partition; size_partition = (u32) diff->size_partition; + info->unique_id = diff->unique_id; info->offset_table = (diff->active_table) ? diff->offset_table1 : diff->offset_table0; info->size_table = diff->size_table; offset_difi = info->offset_table; @@ -240,70 +270,82 @@ u32 GetDisaDiffRWInfo(const char* path, DisaDiffRWInfo* info, bool partitionB) { // check & get data from DPFS descriptor const DpfsDescriptor* dpfs = &(difis.dpfs); - if ((dpfs->offset_lvl1 + dpfs->size_lvl1 > dpfs->offset_lvl2) || - (dpfs->offset_lvl2 + dpfs->size_lvl2 > dpfs->offset_lvl3) || - (dpfs->offset_lvl3 + dpfs->size_lvl3 > size_partition) || - (2 > dpfs->log_lvl2) || (dpfs->log_lvl2 > dpfs->log_lvl3) || - !dpfs->size_lvl1 || !dpfs->size_lvl2 || !dpfs->size_lvl3) + if ((LVL(dpfs, 1).offset + LVL(dpfs, 1).size > LVL(dpfs, 2).offset) || + (LVL(dpfs, 2).offset + LVL(dpfs, 2).size > LVL(dpfs, 3).offset) || + (LVL(dpfs, 3).offset + LVL(dpfs, 3).size > size_partition) || + (2 > LVL(dpfs, 2).blocksize_log2) || (LVL(dpfs, 2).blocksize_log2 > LVL(dpfs, 3).blocksize_log2) || + !LVL(dpfs, 1).size || !LVL(dpfs, 2).size || !LVL(dpfs, 3).size) return 1; - info->offset_dpfs_lvl1 = (u32) (offset_partition + dpfs->offset_lvl1); - info->offset_dpfs_lvl2 = (u32) (offset_partition + dpfs->offset_lvl2); - info->offset_dpfs_lvl3 = (u32) (offset_partition + dpfs->offset_lvl3); - info->size_dpfs_lvl1 = (u32) dpfs->size_lvl1; - info->size_dpfs_lvl2 = (u32) dpfs->size_lvl2; - info->size_dpfs_lvl3 = (u32) dpfs->size_lvl3; - info->log_dpfs_lvl2 = (u32) dpfs->log_lvl2; - info->log_dpfs_lvl3 = (u32) dpfs->log_lvl3; + info->offset_dpfs_lvl1 = (u32) (offset_partition + LVL(dpfs, 1).offset); + info->offset_dpfs_lvl2 = (u32) (offset_partition + LVL(dpfs, 2).offset); + info->offset_dpfs_lvl3 = (u32) (offset_partition + LVL(dpfs, 3).offset); + info->size_dpfs_lvl1 = (u32) LVL(dpfs, 1).size; + info->size_dpfs_lvl2 = (u32) LVL(dpfs, 2).size; + info->size_dpfs_lvl3 = (u32) LVL(dpfs, 3).size; + info->log_dpfs_lvl2 = (u32) LVL(dpfs, 2).blocksize_log2; + info->log_dpfs_lvl3 = (u32) LVL(dpfs, 3).blocksize_log2; // check & get data from IVFC descriptor const IvfcDescriptor* ivfc = &(difis.ivfc); if ((ivfc->size_hash != difi->size_hash) || (ivfc->size_ivfc != sizeof(IvfcDescriptor)) || - (ivfc->offset_lvl1 + ivfc->size_lvl1 > ivfc->offset_lvl2) || - (ivfc->offset_lvl2 + ivfc->size_lvl2 > ivfc->offset_lvl3) || - (ivfc->offset_lvl3 + ivfc->size_lvl3 > dpfs->size_lvl3)) + (ivfc->offset_tree != 0) || + (LVL(ivfc, 1).offset + LVL(ivfc, 1).size > LVL(ivfc, 2).offset) || + (LVL(ivfc, 2).offset + LVL(ivfc, 2).size > LVL(ivfc, 3).offset) || + (LVL(ivfc, 3).offset + LVL(ivfc, 3).size > LVL(dpfs, 3).size)) return 1; if (!info->ivfc_use_extlvl4) { - if ((ivfc->offset_lvl3 + ivfc->size_lvl3 > ivfc->offset_lvl4) || - (ivfc->offset_lvl4 + ivfc->size_lvl4 > dpfs->size_lvl3)) + if ((LVL(ivfc, 3).offset + LVL(ivfc, 3).size > LVL(ivfc, 4).offset) || + (LVL(ivfc, 4).offset + LVL(ivfc, 4).size > LVL(dpfs, 3).size)) return 1; - info->offset_ivfc_lvl4 = (u32) ivfc->offset_lvl4; - } else if (info->offset_ivfc_lvl4 + ivfc->size_lvl4 > offset_partition + size_partition) + info->offset_ivfc_lvl4 = (u32) LVL(ivfc, 4).offset; + } else if (info->offset_ivfc_lvl4 + LVL(ivfc, 4).size > offset_partition + size_partition) return 1; - info->log_ivfc_lvl1 = (u32) ivfc->log_lvl1; - info->log_ivfc_lvl2 = (u32) ivfc->log_lvl2; - info->log_ivfc_lvl3 = (u32) ivfc->log_lvl3; - info->log_ivfc_lvl4 = (u32) ivfc->log_lvl4; - info->offset_ivfc_lvl1 = (u32) ivfc->offset_lvl1; - info->offset_ivfc_lvl2 = (u32) ivfc->offset_lvl2; - info->offset_ivfc_lvl3 = (u32) ivfc->offset_lvl3; - info->size_ivfc_lvl1 = (u32) ivfc->size_lvl1; - info->size_ivfc_lvl2 = (u32) ivfc->size_lvl2; - info->size_ivfc_lvl3 = (u32) ivfc->size_lvl3; - info->size_ivfc_lvl4 = (u32) ivfc->size_lvl4; + info->log_ivfc_lvl1 = (u32) LVL(ivfc, 1).blocksize_log2; + info->log_ivfc_lvl2 = (u32) LVL(ivfc, 2).blocksize_log2; + info->log_ivfc_lvl3 = (u32) LVL(ivfc, 3).blocksize_log2; + info->log_ivfc_lvl4 = (u32) LVL(ivfc, 4).blocksize_log2; + info->offset_ivfc_lvl1 = (u32) LVL(ivfc, 1).offset; + info->offset_ivfc_lvl2 = (u32) LVL(ivfc, 2).offset; + info->offset_ivfc_lvl3 = (u32) LVL(ivfc, 3).offset; + info->size_ivfc_lvl1 = (u32) LVL(ivfc, 1).size; + info->size_ivfc_lvl2 = (u32) LVL(ivfc, 2).size; + info->size_ivfc_lvl3 = (u32) LVL(ivfc, 3).size; + info->size_ivfc_lvl4 = (u32) LVL(ivfc, 4).size; return 0; } u32 BuildDisaDiffDpfsLvl2Cache(const char* path, const DisaDiffRWInfo* info, u8* cache, u32 cache_size) { - const u32 min_cache_bits = (info->size_dpfs_lvl3 + (1 << info->log_dpfs_lvl3) - 1) >> info->log_dpfs_lvl3; - const u32 min_cache_size = ((min_cache_bits + 31) >> (3 + 2)) << 2; + const u32 blocksize_lvl2 = 1u << info->log_dpfs_lvl2; + const u32 blocksize_lvl3 = 1u << info->log_dpfs_lvl3; + + // each lvl3 block maps to exactly one bit in lvl2 + const u32 lv2_min_num_bits = ceil_div(info->size_dpfs_lvl3, blocksize_lvl3); + // the number of bits in lvl2 are rounded up to a byte-boundary, 8 bits + const u32 lv2_min_num_bytes = ceil_div(lv2_min_num_bits, 8); + // and the number of bytes lvl2 consists of is rounded up to the lvl2 blocksize + // so that each lvl2 block maps to exactly one bit of lvl1, respectively + const u32 lv2_min_num_blocks = ceil_div(lv2_min_num_bytes, blocksize_lvl2); + // thus, the minimum size of lvl2 is the lvl2-blockaligned number of lvl2 bytes (which itself are the 8-bit aligned number of lvl3 blocks) + const u32 lv2_min_size = lv2_min_num_blocks * blocksize_lvl2; + const u32 offset_lvl1 = info->offset_dpfs_lvl1 + ((info->dpfs_lvl1_selector) ? info->size_dpfs_lvl1 : 0); // safety (this still assumes all the checks from GetDisaDiffRWInfo()) - if ((cache_size < min_cache_size) || - (min_cache_size > info->size_dpfs_lvl2) || - (min_cache_size > (info->size_dpfs_lvl1 << (3 + info->log_dpfs_lvl2)))) { + if ((cache_size < lv2_min_size) || + (lv2_min_size > info->size_dpfs_lvl2) || + (lv2_min_size > (info->size_dpfs_lvl1 << (3 + info->log_dpfs_lvl2)))) { return 1; } // allocate memory u8* lvl1 = (u8*) malloc(info->size_dpfs_lvl1); - if (!lvl1) return 1; // this is never more than 8 byte in reality -___- + if (!lvl1) return 1; // open file pointer if (DisaDiffOpen(path) != FR_OK) { @@ -315,19 +357,27 @@ u32 BuildDisaDiffDpfsLvl2Cache(const char* path, const DisaDiffRWInfo* info, u8* u32 ret = 0; if ((ret != 0) || DisaDiffRead(lvl1, info->size_dpfs_lvl1, offset_lvl1)) ret = 1; - // read full lvl2_0 to cache + // read full lvl2_0 to cache. this is the baseline, and we'll replace blocks that are actually in lv2_1 later if ((ret != 0) || DisaDiffRead(cache, info->size_dpfs_lvl2, info->offset_dpfs_lvl2)) ret = 1; - // cherry-pick lvl2_1 - u32 log_lvl2 = info->log_dpfs_lvl2; u32 offset_lvl2_1 = info->offset_dpfs_lvl2 + info->size_dpfs_lvl2; - for (u32 i = 0; (ret == 0) && ((i << (3 + log_lvl2)) < min_cache_size); i += 4) { - u32 dword = *(u32*) (void*) (lvl1 + i); - for (u32 b = 0; b < 32; b++) { - if ((dword >> (31 - b)) & 1) { - u32 offset = ((i << 3) + b) << log_lvl2; - if (DisaDiffRead((u8*) cache + offset, 1 << log_lvl2, offset_lvl2_1 + offset) != FR_OK) ret = 1; - } + + for (u32 i = 0; i < lv2_min_num_blocks; i++) { + if (!GET_DPFS_BIT(i, lvl1)) { + // this lv2 block is not in lv2_1, so we don't need to replace it + continue; + } + + u32 offset = i * blocksize_lvl2; + if (offset > cache_size || offset + blocksize_lvl2 > cache_size || blocksize_lvl2 > cache_size) { + // this was known to corrupt the heap before, and shouldn't ever happen, but still + ret = 1; + break; + } + + if (DisaDiffRead((u8*) cache + offset, blocksize_lvl2, offset_lvl2_1 + offset) != FR_OK) { + ret = 1; + break; } } @@ -570,4 +620,201 @@ u32 WriteDisaDiffIvfcLvl4(const char* path, const DisaDiffRWInfo* info, u32 offs DisaDiffClose(); if (cache) free(cache); return size; +} + +static inline u64 CalcIvfcTreeSize(const IvfcDescriptor *ivfc, bool ext_lv4) { + int end_level = ext_lv4 ? 3 : 4; + return LVL(ivfc, end_level).offset + LVL(ivfc, end_level).size - LVL(ivfc, 1).offset; +} + +static inline u64 CalcDpfsTreeSize(const DpfsDescriptor *dpfs) { + return LVL(dpfs, 3).offset + LVL(dpfs, 3).size * 2; +} + +static inline u64 CalcDifiDescriptorSize(const DifiHeader *difi) { + return difi->offset_hash + difi->size_hash; +} + +static inline u64 CalcDiffFileSize(const DiffHeader *diff) { + return diff->offset_partition + diff->size_partition; +} + +static void BuildIvfcDescriptor(IvfcDescriptor *ivfc, u64 data_size, bool db) { + const IvfcDpfsConfig *config = GetIvfcDpfsConfig(db); + + memset(ivfc, 0, sizeof(IvfcDescriptor)); + memcpy(ivfc, ivfc_magic, sizeof(ivfc_magic)); + ivfc->size_ivfc = sizeof(IvfcDescriptor); + ivfc->offset_tree = 0; + + LVL(ivfc, 4).size = data_size; + LVL(ivfc, 3).size = 32 * ceil_div(data_size, config->ivfc_blocksizes[L(4)]); + LVL(ivfc, 2).size = 32 * ceil_div(LVL(ivfc, 3).size, config->ivfc_blocksizes[L(3)]); + LVL(ivfc, 1).size = 32 * ceil_div(LVL(ivfc, 2).size, config->ivfc_blocksizes[L(2)]); + + ivfc->size_hash = 32 * ceil_div(LVL(ivfc, 1).size, config->ivfc_blocksizes[L(1)]); + + u64 cur_offs = 0; + + for (int i = 1; i < 4 + 1; i++) { + u64 cur_lvl_offs = 0; + if (LVL(ivfc, i).size < 4 * config->ivfc_blocksizes[L(i)]) { + cur_lvl_offs = align_pow2(cur_offs, 8); + } else { + cur_lvl_offs = align_pow2(cur_offs, config->ivfc_blocksizes[L(i)]); + } + + LVL(ivfc, i).offset = cur_lvl_offs; + LVL(ivfc, i).blocksize_log2 = log_2(config->ivfc_blocksizes[L(i)]); + + cur_offs = LVL(ivfc, i).offset + LVL(ivfc, i).size; + } +} + +static void BuildDpfsDescriptor(DpfsDescriptor *dpfs, u64 data_dupsize, bool db) { + const IvfcDpfsConfig *config = GetIvfcDpfsConfig(db); + + memset(dpfs, 0, sizeof(DpfsDescriptor)); + memcpy(dpfs, dpfs_magic, sizeof(dpfs_magic)); + + u32 lv2_num_block = ceil_div(data_dupsize, config->dpfs_lv3_blocksize); + u32 lv2_num_byte = ceil_div(lv2_num_block, 8); + u32 lv2_size = align_pow2(lv2_num_byte, config->dpfs_lv2_blocksize); + + u32 lv1_num_block = ceil_div(lv2_size, config->dpfs_lv2_blocksize); + u32 lv1_num_byte = ceil_div(lv1_num_block, 8); + u32 lv1_size = align_pow2(lv1_num_byte, 4); + + LVL(dpfs, 1).offset = 0; + LVL(dpfs, 1).size = lv1_size; + LVL(dpfs, 1).blocksize_log2 = 0; + + LVL(dpfs, 2).offset = lv1_size * 2; + LVL(dpfs, 2).size = lv2_size; + LVL(dpfs, 2).blocksize_log2 = log_2(config->dpfs_lv2_blocksize); + + LVL(dpfs, 3).offset = align_pow2(LVL(dpfs, 2).offset + lv2_size * 2, config->dpfs_lv3_blocksize); + LVL(dpfs, 3).size = (u32)data_dupsize; + LVL(dpfs, 3).blocksize_log2 = log_2(config->dpfs_lv3_blocksize); +} + +static void BuildDifiDescriptor(DifiHeader *difi, IvfcDescriptor *ivfc, DpfsDescriptor *dpfs, u64 data_size, bool ext_lv4, bool db) { + const IvfcDpfsConfig *config = GetIvfcDpfsConfig(db); + + BuildIvfcDescriptor(ivfc, data_size, db); + + u64 dpfs_duped_size = align_pow2(CalcIvfcTreeSize(ivfc, ext_lv4), config->dpfs_lv3_blocksize); + + BuildDpfsDescriptor(dpfs, dpfs_duped_size, db); + + memset(difi, 0, sizeof(DifiHeader)); + memcpy(difi, difi_magic, sizeof(difi_magic)); + difi->size_ivfc = sizeof(IvfcDescriptor); + difi->size_dpfs = sizeof(DpfsDescriptor); + difi->size_hash = ivfc->size_hash; + difi->dpfs_lvl1_selector = 1; + difi->ivfc_use_extlvl4 = ext_lv4 ? 1 : 0; + + difi->offset_ivfc = sizeof(DifiHeader); + difi->offset_dpfs = align_pow2(difi->offset_ivfc + difi->size_ivfc, 4); + difi->offset_hash = align_pow2(difi->offset_dpfs + difi->size_dpfs, 4); + + if (ext_lv4) { + difi->ivfc_offset_extlvl4 = align_pow2(CalcDpfsTreeSize(dpfs), config->ivfc_blocksizes[L(4)]); + } else { + difi->ivfc_offset_extlvl4 = 0; + } +} + +static void BuildDiffHeader(DiffHeader *diff, DifiHeader *difi, IvfcDescriptor *ivfc, DpfsDescriptor *dpfs, u64 data_size, bool ext_lv4, bool db, u64 *out_uid) { + const IvfcDpfsConfig *config = GetIvfcDpfsConfig(db); + + BuildDifiDescriptor(difi, ivfc, dpfs, data_size, ext_lv4, db); + + memset(diff, 0, sizeof(DiffHeader)); + + memcpy(diff, diff_magic, sizeof(diff_magic)); + + if (db) { // db files have a 0 in the unique id field + diff->unique_id = 0; + } else { // non-db DIFFs (mainly extdata files) have a random 8-byte unique id + u32 *uid = (u32 *)&diff->unique_id; + *uid++ = (u32)rand(); + *uid = (u32)rand(); + } + + if (out_uid) + *out_uid = diff->unique_id; + + diff->size_table = CalcDifiDescriptorSize(difi); + + if (ext_lv4) { + diff->size_partition = difi->ivfc_offset_extlvl4 + data_size; + } else { + diff->size_partition = CalcDpfsTreeSize(dpfs); + } + + diff->offset_table1 = sizeof(DiffHeader) + 0x100 /* CMAC section size */; + diff->offset_table0 = align_pow2(diff->offset_table1 + diff->size_table, 8); + diff->offset_partition = align_pow2(diff->offset_table0 + diff->size_table, CalcIvfcDpfsConfigBlockSize(config)); + diff->active_table = 1; +} + +u64 BuildDiffCalcRequiredSize(u64 data_size, bool ext_lv4, bool db) { + IvfcDescriptor ivfc; + DpfsDescriptor dpfs; + DifiHeader difi; + DiffHeader diff; + + BuildDiffHeader(&diff, &difi, &ivfc, &dpfs, data_size, ext_lv4, db, NULL); + + return diff.offset_partition + diff.size_partition; +} + +u32 CreateDiff(const char *path, u64 data_size, bool ext_lv4, bool db, u64 *out_uid) { + IvfcDescriptor ivfc; + DpfsDescriptor dpfs; + DifiHeader difi; + DiffHeader diff; + u8 cmac[0x100]; + + BuildDiffHeader(&diff, &difi, &ivfc, &dpfs, data_size, ext_lv4, db, out_uid); + + memset(cmac, 0, sizeof(cmac)); + + u8 *part_desc = (u8 *)malloc(diff.size_table); + if (!part_desc) + return 1; + u8 *desc = part_desc; + + memcpy(desc, &difi, sizeof(DifiHeader)); desc += sizeof(DifiHeader); + memcpy(desc, &ivfc, sizeof(IvfcDescriptor)); desc += sizeof(IvfcDescriptor); + memcpy(desc, &dpfs, sizeof(DpfsDescriptor)); desc += sizeof(DpfsDescriptor); + memset(desc, 0, difi.size_hash); + + sha_quick(diff.hash_table, part_desc, diff.size_table, SHA256_MODE); + + FIL fp; + UINT written = 0; + + if (fvx_open(&fp, path, FA_WRITE | FA_OPEN_EXISTING) != FR_OK) { + free(part_desc); + return 1; + } + + if (fvx_size(&fp) < CalcDiffFileSize(&diff) || + (fvx_write(&fp, cmac, sizeof(cmac), &written) != FR_OK || written != sizeof(cmac)) || + (fvx_write(&fp, &diff, sizeof(diff), &written) != FR_OK || written != sizeof(diff)) || + fvx_lseek(&fp, diff.offset_table1) != FR_OK || + (fvx_write(&fp, part_desc, diff.size_table, &written) != FR_OK || written != diff.size_table) || + fvx_lseek(&fp, diff.offset_table0) != FR_OK || + (fvx_write(&fp, part_desc, diff.size_table, &written) != FR_OK || written != diff.size_table)) { + free(part_desc); + fvx_close(&fp); + return 1; + } + + free(part_desc); + fvx_close(&fp); + return 0; } \ No newline at end of file diff --git a/arm9/source/game/disadiff.h b/arm9/source/game/disadiff.h index 33b0efb..8b46b58 100644 --- a/arm9/source/game/disadiff.h +++ b/arm9/source/game/disadiff.h @@ -16,6 +16,7 @@ // condensed info to enable reading/writing IVFC lvl4 typedef struct { + u64 unique_id; u32 offset_table; u32 size_table; u32 offset_partition_hash; @@ -46,9 +47,13 @@ typedef struct { u8* dpfs_lvl2_cache; // optional, NULL when unused } __attribute__((packed)) DisaDiffRWInfo; +u64 BuildDiffCalcRequiredSize(u64 data_size, bool ext_lv4, bool db); +u32 CreateDiff(const char *path, u64 data_size, bool ext_lv4, bool db, u64 *out_uid); + u32 GetDisaDiffRWInfo(const char* path, DisaDiffRWInfo* info, bool partitionB); u32 BuildDisaDiffDpfsLvl2Cache(const char* path, const DisaDiffRWInfo* info, u8* cache, u32 cache_size); u32 ReadDisaDiffIvfcLvl4(const char* path, const DisaDiffRWInfo* info, u32 offset, u32 size, void* buffer); u32 WriteDisaDiffIvfcLvl4(const char* path, const DisaDiffRWInfo* info, u32 offset, u32 size, const void* buffer); + // Not intended for external use other than vdisadiff u32 FixDisaDiffIvfcLevel(const DisaDiffRWInfo* info, u32 level, u32 offset, u32 size, u32* next_offset, u32* next_size); \ No newline at end of file diff --git a/arm9/source/game/ifat_common.h b/arm9/source/game/ifat_common.h new file mode 100644 index 0000000..156be2d --- /dev/null +++ b/arm9/source/game/ifat_common.h @@ -0,0 +1,76 @@ +#pragma once + +#include "types.h" + +// These structures are shared across 3 different inner-FAT (named ifat for brevity) types: +// - SAVE: save data +// - EXSV: extdata +// - IRDB (BDRI): title.db & ticket.db + +// Each ifat type has its own type of file and directory entry. + +typedef struct { + union { + // this describes data inside the FAT (and thus, in the data region) + struct { + u32 starting_block_index; + u32 block_count; + u32 max_entry_count; + } PACKED_STRUCT; + + // this describes data outside the FAT (and thus, outside the data region) + struct { + u64 outfat_offset; + u32 outfat_count; + } PACKED_STRUCT; + }; + u32 _pad; +} PACKED_STRUCT IFatTableInfo; + +typedef struct { + union { + u32 magic; + char magic_str[4]; + }; + u32 version; + u64 fs_info_offset; + u64 fs_image_size_blocks; + u32 fs_image_blocksize; + u32 __pad; +} PACKED_STRUCT IFatPreHeader; + +typedef struct { + u32 _unk; + u32 data_region_blocksize; + IFatTableInfo dir_hashtbl; + IFatTableInfo file_hashtbl; + IFatTableInfo fat; + IFatTableInfo data_region; + IFatTableInfo dirtable_info; + IFatTableInfo filetable_info; +} PACKED_STRUCT IFatFsInfo; + + +/* + * for node head, + * - U.index --> index of previous node head. + * - U.flag --> set if this is the first node head. + * - V.index --> index of next node head. + * - V.flag --> whether or not this node has extended entries (multiple entries) + */ +/* + * for extended node, + * - U.index --> index of previous entry in this node. + * - U.flag --> always set. + * - V.index --> index of next entry in this node. + * - V.flag --> never set. + */ + +typedef struct __attribute__((packed)) IFatEntryHalf { + u32 index: 31; + u32 flag: 1; +} IFatEntryHalf; + +typedef struct __attribute__((packed)) IFatEntry { + IFatEntryHalf U, V; +} IFatEntry; \ No newline at end of file diff --git a/arm9/source/game/saveexsv.c b/arm9/source/game/saveexsv.c new file mode 100644 index 0000000..9e7a865 --- /dev/null +++ b/arm9/source/game/saveexsv.c @@ -0,0 +1,261 @@ +#include "saveexsv.h" +#include "common.h" +#include "fsutil.h" +#include "types.h" +#include "vff.h" +#include "ff.h" + +#define PART_A_PATH "D:/partitionA.bin" +#define PART_B_PATH "D:/partitionB.bin" + +static int read_alloc_chunk(void **output, FIL *file, u64 offset, u32 size) { + UINT nread = 0; + *output = NULL; + if (!(*output = malloc(size)) || + fvx_lseek(file, offset) != FR_OK || + fvx_read(file, *output, size, &nread) != FR_OK || nread != size) { + if (*output) { free(*output); *output = NULL; }; + return 1; + } + return 0; +} + +static FIL part_a = { 0 }, part_b = { 0 }; +static FIL *data_part = NULL; + +int SaveExsvFileInit(SaveExsvFile *sav) { + UINT nread = 0; + int retval = 0; + + // this shouldn't happen, but if it does, it should be a no-op + if (sav->init_ok) { + return 0; + } + + // partitionA is mandatory + if (fvx_open(&part_a, PART_A_PATH, FA_READ) != FR_OK) + goto err_exit; + + if (PathExist(PART_B_PATH)) { + // if partitionB exists, we need it + if (fvx_open(&part_b, PART_B_PATH, FA_READ) != FR_OK) + goto err_exit; + + data_part = &part_b; + } else { + data_part = &part_a; + } + + sav->duplicate_meta = data_part == &part_b; + + u32 dir_table_offset = 0, dir_table_size = 0; + u32 file_table_offset = 0, file_table_size = 0; + + if (fvx_read(&part_a, &sav->pre_header, sizeof(IFatPreHeader), &nread) != FR_OK || nread != sizeof(IFatPreHeader)) + goto err_exit; + + u32 full_header_size = sizeof(IFatPreHeader) + sizeof(IFatFsInfo); + + if (sav->pre_header.magic == 0x45564153 ) { /* SAVE */ + if (sav->pre_header.version != 0x40000 || sav->pre_header.fs_info_offset != 0x20) + goto err_exit; + } else if (sav->pre_header.magic == 0x45585356) { /* EXSV (VSXE) */ + if (sav->pre_header.version != 0x30000 || sav->pre_header.fs_info_offset != 0x138 || + fvx_read(&part_a, &sav->exsv_extra_hdr, sizeof(ExsvExtraHeader), &nread) != FR_OK || nread != sizeof(ExsvExtraHeader)) + goto err_exit; + + full_header_size += sizeof(ExsvExtraHeader); + sav->is_exsv = true; + } else { + goto err_exit; + } + + if (fvx_read(&part_a, &sav->fs_info, sizeof(IFatFsInfo), &nread) != FR_OK || nread != sizeof(IFatFsInfo)) + goto err_exit; + + u64 part_a_size = fvx_size(&part_a); + + if (!sav->duplicate_meta) { + + dir_table_offset = sav->fs_info.data_region.outfat_offset + sav->fs_info.dirtable_info.starting_block_index * sav->fs_info.data_region_blocksize; + dir_table_size = sav->fs_info.data_region_blocksize * sav->fs_info.dirtable_info.block_count; + + file_table_offset = sav->fs_info.data_region.outfat_offset + sav->fs_info.filetable_info.starting_block_index * sav->fs_info.data_region_blocksize; + file_table_size = sav->fs_info.data_region_blocksize * sav->fs_info.filetable_info.block_count; + + // duplicate data has the dir and file tables in the data region, so we don't need to count them separately + u64 min_part_a_size = align(full_header_size + + sav->fs_info.dir_hashtbl.outfat_count * sizeof(u32) + + sav->fs_info.file_hashtbl.outfat_count * sizeof(u32) + + (sav->fs_info.fat.outfat_count + 1) * sizeof(IFatEntry), + sav->pre_header.fs_image_blocksize) + + sav->fs_info.data_region.outfat_count * sav->fs_info.data_region_blocksize; + + if (part_a_size < min_part_a_size) { + goto err_exit; + } + } else { + // it should not be possible for the extdata main DIFF to be duplicate meta + if (sav->is_exsv) + goto err_exit; + + dir_table_offset = sav->fs_info.dirtable_info.outfat_offset; + dir_table_size = (sav->fs_info.dirtable_info.outfat_count + 2) * sizeof(SaveDirectoryEntry); + + file_table_offset = sav->fs_info.filetable_info.outfat_offset; + file_table_size = (sav->fs_info.filetable_info.outfat_count + 1) * sizeof(SaveFileEntry); + + // duplicate meta has the dir and file tables outside the data region + u64 part_b_size = fvx_size(&part_b); + + u64 min_part_a_size = align(full_header_size + + sav->fs_info.dir_hashtbl.outfat_count * sizeof(u32) + + sav->fs_info.file_hashtbl.outfat_count * sizeof(u32) + + (sav->fs_info.fat.outfat_count + 1) * sizeof(IFatEntry) + + dir_table_size + + file_table_size, + sav->pre_header.fs_image_blocksize); + u64 min_part_b_size = sav->fs_info.data_region.outfat_count * sav->fs_info.data_region_blocksize; + + if (part_a_size < min_part_a_size || part_b_size < min_part_b_size) + goto err_exit; + } + + if (!dir_table_offset || !dir_table_size || !file_table_offset || !file_table_size) + goto err_exit; + + sav->max_num_file_entries = file_table_size / sizeof(SaveFileEntry); + sav->max_num_dir_entries = dir_table_size / sizeof(SaveDirectoryEntry); + + if (read_alloc_chunk((void **)&sav->dir_hashtbl, &part_a, sav->fs_info.dir_hashtbl.outfat_offset, sav->fs_info.dir_hashtbl.outfat_count * 4) || + read_alloc_chunk((void **)&sav->file_hashtbl, &part_a, sav->fs_info.file_hashtbl.outfat_offset, sav->fs_info.file_hashtbl.outfat_count * 4) || + read_alloc_chunk((void **)&sav->fat_entries, &part_a, sav->fs_info.fat.outfat_offset, (sav->fs_info.fat.outfat_count + 1) * sizeof(IFatEntry)) || + read_alloc_chunk((void **)&sav->dir_entries, &part_a, dir_table_offset, dir_table_size) || + read_alloc_chunk((void **)&sav->file_entries, &part_a, file_table_offset, file_table_size)) { + goto err_exit; + } +ret: + fvx_close(&part_a); + fvx_close(&part_b); + sav->init_ok = !retval; + return retval; +err_exit: + retval = 1; + goto ret; +} + +void SaveExsvFileFree(SaveExsvFile *sav) { + data_part = NULL; + if (sav->dir_hashtbl) { free(sav->dir_hashtbl); sav->dir_hashtbl = NULL; } + if (sav->file_hashtbl) { free(sav->file_hashtbl); sav->file_hashtbl = NULL; } + if (sav->fat_entries) { free(sav->fat_entries); sav->fat_entries = NULL; } + if (sav->dir_entries) { free(sav->dir_entries); sav->dir_entries = NULL; } + if (sav->file_entries) { free(sav->file_entries); sav->file_entries = NULL; } + sav->max_num_file_entries = sav->max_num_dir_entries = 0; + sav->init_ok = false; + sav->is_exsv = false; +} + +typedef struct ReadFileData { + u32 offset; + u32 size; + char *outbuf; +} ReadFileData; + +static int ReadFile(u32 part_offset, u32 file_offset, u32 size, void *arg) { + ReadFileData *data = (ReadFileData *)arg; + + UINT nread = 0; + + if (file_offset <= data->offset && data->offset < file_offset + size) { + u32 in_block_offs = data->offset - file_offset; + u32 in_block_size = min(size - in_block_offs, data->size); + + if (fvx_lseek(data_part, part_offset + in_block_offs) != FR_OK || + fvx_read(data_part, data->outbuf, in_block_size, &nread) != FR_OK || nread != in_block_size) { + return 1; + } + + data->offset += in_block_size; + data->outbuf += in_block_size; + data->size -= in_block_size; + } + + if (!data->size) return 2; + + return 0; +} + +// 0: OK/continue +// 1: error +// 2: OK, early exit processing chain +typedef int (* follow_cb)(u32 in_part_offset, u32 file_offset, u32 size, void *arg); + +static int ProcessFatDataBlock(SaveExsvFile *save, u32 index, u32 *file_offset, follow_cb cb, void *cb_arg) { + if (!index) + return 0; // not yet + + int cb_ret = cb(save->fs_info.data_region.outfat_offset + (index - 1) * save->fs_info.data_region_blocksize, *file_offset, save->fs_info.data_region_blocksize, cb_arg); + if (cb_ret) + return cb_ret; + + *file_offset += save->fs_info.data_region_blocksize; + return 0; +} + +static int ProcessFatChain(SaveExsvFile *save, u32 initial_index, follow_cb cb, void *arg) { + u32 idx = initial_index; + u32 file_offset = 0; + int ret = 0; + + do { + if (idx >= save->fs_info.fat.outfat_count) // shouldn't happen + return 1; + + IFatEntry *ent = &save->fat_entries[idx]; + + ret = ProcessFatDataBlock(save, idx, &file_offset, cb, arg); + if (ret == 2) return 0; + else if (ret == 1) return 1; + + /* process extended nodes first */ + if (ent->V.flag) { + if (idx + 1 >= save->fs_info.fat.outfat_count) // same as above, shouldn't happen, but still + return 1; + u32 end_index = save->fat_entries[idx + 1].V.index; + for (u32 i = idx + 1; i < end_index + 1; i++) { + ret = ProcessFatDataBlock(save, i, &file_offset, cb, arg); + if (ret == 2) return 0; + else if (ret == 1) return 1; + } + } + + if (ent->V.index) + idx = ent->V.index; /* go to next node, if we have one. */ + else + idx = 0; /* no next node, we're done */ + } + while (idx); + + return 0; +} + +int SaveExsvReadFatFile(SaveExsvFile *sav, u32 index, void *buffer, u32 offset, u32 count) { + if (!sav->init_ok || index >= sav->max_num_file_entries) + return 1; + + if (sav->is_exsv) { + // Extdata uses physical files. Therefore, reading extdata files this way should be impossible. + return 1; + } else { + SaveFileEntry *fent = &sav->file_entries[index]; + + // empty file + if (fent->ent.first_block_index == 0x80000000) + return 1; + + ReadFileData data = { .offset = offset, .size = count, .outbuf = (char *)buffer }; + + return ProcessFatChain(sav, fent->ent.first_block_index + 1, ReadFile, &data); + } +} \ No newline at end of file diff --git a/arm9/source/game/saveexsv.h b/arm9/source/game/saveexsv.h new file mode 100644 index 0000000..f742146 --- /dev/null +++ b/arm9/source/game/saveexsv.h @@ -0,0 +1,97 @@ +#pragma once + +#include "types.h" +#include "ifat_common.h" + +// SAVE images may be created in two modes: +// - "duplicate data" mode, in which both filesystem metadata and the actual data FAT are duplicated +// (partitionA: file metadata and FAT, duplicated by DPFS tree) +// - "duplicate meta" mode, in which only the filesystem metadata is duplicated, and the FAT is not +// (partitionA: file metadata, duplicated by DPFS tree. partitionB: data FAT) + +typedef enum ExsvAction +{ + EXSV_ACTION_NONE = 0x0, // no last action + EXSV_ACTION_DELETE_FILE = 0x1, // last action was a file deletion + EXSV_ACTION_CREATE_FILE = 0x2, // last action was a file creation +} ExsvAction; + +typedef struct __attribute__((packed)) ExsvExtraHeader { + u64 sub_file_base_id; + u32 pending_action; + u32 unk2; + u64 last_file_id; + char LastFilePath[256]; +} ExsvExtraHeader; + +// same for files and directories +typedef struct __attribute__((packed)) SaveEntryKey { + u32 parent_dir_idx; + char name[16]; +} SaveEntryKey; + +typedef struct SaveDirectoryEntryData { + SaveEntryKey key; + u32 next_sibling_dir_index; + u32 first_subdir_index; + u32 first_file_index; /* in file entry table */ + u32 __pad; + u32 next_dir_in_bucket_index; +} SaveDirectoryEntryData; + +typedef struct SaveDummyDirectoryEntryData { + u32 current_total_entry_count; + u32 max_entry_count; /* max dir count + 2 */ + u8 __pad[0x1C]; + u32 next_dummy_index; +} SaveDummyDirectoryEntryData; + +typedef union __attribute__((packed)) SaveDirectoryEntry { + SaveDummyDirectoryEntryData dmy; + SaveDirectoryEntryData ent; +} SaveDirectoryEntry; + +typedef struct __attribute__((packed)) SaveFileEntryData { + SaveEntryKey key; + u32 next_sibling_index; + u32 __pad; + u32 first_block_index; /* in data region; 0x80000000 if no data */ + union { + u64 file_size; // for SAVE + u64 file_unique_id; // for EXSV + }; + u32 _unk_pad; + u32 next_file_in_bucket_index; +} SaveFileEntryData; + +typedef struct SaveDummyFileEntryData { + u32 current_total_entry_count; + u32 max_entry_count; /* max file count + 1 */ + u8 __pad[0x24]; + u32 next_dummy_index; +} SaveDummyFileEntryData; + +typedef union SaveFileEntry { + SaveDummyFileEntryData dmy; + SaveFileEntryData ent; +} SaveFileEntry; + +typedef struct SaveExsvFile { + ExsvExtraHeader exsv_extra_hdr; + IFatFsInfo fs_info; + IFatPreHeader pre_header; + u32 *dir_hashtbl; + u32 *file_hashtbl; + IFatEntry *fat_entries; + SaveDirectoryEntry *dir_entries; + u32 max_num_dir_entries; + SaveFileEntry *file_entries; + u32 max_num_file_entries; + bool duplicate_meta; + bool is_exsv; + bool init_ok; +} SaveExsvFile; + +int SaveExsvFileInit(SaveExsvFile *sav); +int SaveExsvReadFatFile(SaveExsvFile *sav, u32 index, void *buffer, u32 offset, u32 count); +void SaveExsvFileFree(SaveExsvFile *sav); \ No newline at end of file diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index 5839f69..a427e04 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -1071,7 +1071,7 @@ u32 CartRawDump(void) { if (ret) ShowPrompt(false, STR_FAILED_DUMPING_CART, cname); else ShowPrompt(false, STR_PATH_DUMPED_TO_OUT, cname, OUTPUT_PATH); - + free(buf); free(cdata); return ret; @@ -1566,7 +1566,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan ShowSelectPrompt(n_opt, optionstr, STR_PATH_N_FILES_SELECTED, pathstr, n_marked) : ShowSelectPrompt(n_opt, optionstr, "%s%s", pathstr, tidstr)); if (user_select == mount) { // -> mount file as image - const char* mnt_drv_paths[] = { "7:", "G:", "K:", "T:", "I:", "D:" }; // maybe move that to fsdrive.h + const char* mnt_drv_paths[] = { "7:", "G:", "K:", "T:", "I:", "F:", "D:" }; // maybe move that to fsdrive.h if (clipboard->n_entries && (DriveType(clipboard->entry[0].path) & DRV_IMAGE)) clipboard->n_entries = 0; // remove last mounted image clipboard entries SetTitleManagerMode(false); // disable title manager mode @@ -2654,7 +2654,7 @@ u32 GodMode(int entrypoint) { // basic navigation commands if ((pad_state & BUTTON_A) && (curr_entry->type != T_FILE) && (curr_entry->type != T_DOTDOT)) { // for dirs if (switched && !(DriveType(curr_entry->path) & (DRV_SEARCH|DRV_TITLEMAN))) { // exclude Y/Z - const char* optionstr[8] = { NULL }; + const char* optionstr[9] = { NULL }; char tpath[16], copyToOut[UTF_BUFFER_BYTESIZE(64)], dumpToOut[UTF_BUFFER_BYTESIZE(64)]; snprintf(tpath, sizeof(tpath), "%2.2s/dbs/title.db", curr_entry->path); snprintf(copyToOut, sizeof(copyToOut), STR_COPY_TO_OUT, OUTPUT_PATH); @@ -2664,6 +2664,7 @@ u32 GodMode(int entrypoint) { ((strncmp(curr_entry->path, tpath, 16) == 0) || (!*current_path && PathExist(tpath)))) ? ++n_opt : -1; int srch_f = ++n_opt; + int createdbs = (!*current_path && (strspn(curr_entry->path, "14AB") == 1)) ? ++n_opt : -1; int fixcmac = (!*current_path && ((strspn(curr_entry->path, "14AB") == 1) || ((GetMountState() == IMG_NAND) && (*(curr_entry->path) == '7')))) ? ++n_opt : -1; int dirnfo = ++n_opt; @@ -2671,6 +2672,7 @@ u32 GodMode(int entrypoint) { int rawdump = (!*current_path && (DriveType(curr_entry->path) & DRV_CART)) ? ++n_opt : -1; if (tman > 0) optionstr[tman-1] = STR_OPEN_TITLE_MANAGER; if (srch_f > 0) optionstr[srch_f-1] = STR_SEARCH_FOR_FILES; + if (createdbs > 0) optionstr[createdbs-1] = STR_CREATE_MISSING_DB_FILES; if (fixcmac > 0) optionstr[fixcmac-1] = STR_FIX_CMACS_FOR_DRIVE; if (dirnfo > 0) optionstr[dirnfo-1] = (*current_path) ? STR_SHOW_DIRECTORY_INFO : STR_SHOW_DRIVE_INFO; if (stdcpy > 0) optionstr[stdcpy-1] = copyToOut; @@ -2698,6 +2700,9 @@ u32 GodMode(int entrypoint) { cursor = 1; scroll = 0; } + } else if (user_select == createdbs) { + char drv[3] = { *curr_entry->path, ':', '\0' }; + CreateDbFilesForDrive(drv, false, false); } else if (user_select == fixcmac) { RecursiveFixFileCmac(curr_entry->path); ShowPrompt(false, "%s", STR_FIX_CMACS_FOR_DRIVE_FINISHED); diff --git a/arm9/source/lua/gm9internalfs.c b/arm9/source/lua/gm9internalfs.c index a9ce38c..759bd76 100644 --- a/arm9/source/lua/gm9internalfs.c +++ b/arm9/source/lua/gm9internalfs.c @@ -80,7 +80,7 @@ static int internalfs_remove(lua_State* L) { if (!(PathDelete(path))) { return luaL_error(L, "PathDelete failed on %s", path); } - + return 0; } @@ -184,6 +184,22 @@ static int internalfs_fix_cmacs(lua_State* L) { return 0; } +static int internalfs_create_dbs(lua_State* L) { + bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.create_dbs"); + const char* destdrv = luaL_checkstring(L, 1); + + u32 flags = 0; + if (extra) { + flags = GetFlagsFromTable(L, 2, flags, SILENT | OVERWRITE_ALL); + } + + if (CreateDbFilesForDrive(destdrv, (flags & SILENT), (flags & OVERWRITE_ALL)) != 0) { + return luaL_error(L, "createdbs failed"); + } + + return 0; +} + static int internalfs_stat_fs(lua_State* L) { CheckLuaArgCount(L, 1, "_fs.stat_fs"); const char* path = luaL_checkstring(L, 1); @@ -421,7 +437,7 @@ static int internalfs_write_file(lua_State* L) { const char* data = luaL_checklstring(L, 3, &data_length); CheckWritePermissionsLuaError(L, path); - + UINT bytes_written = 0; FRESULT res = fvx_qwrite(path, data, offset, data_length, &bytes_written); if (res != FR_OK) { @@ -517,9 +533,9 @@ static int internalfs_img_mount(lua_State* L) { static int internalfs_img_umount(lua_State* L) { CheckLuaArgCount(L, 0, "_fs.img_umount"); - + InitImgFS(NULL); - + return 0; } @@ -793,6 +809,7 @@ static const luaL_Reg internalfs_lib[] = { {"sd_is_mounted", internalfs_sd_is_mounted}, {"sd_switch", internalfs_sd_switch}, {"fix_cmacs", internalfs_fix_cmacs}, + {"create_dbs", internalfs_create_dbs}, {"key_dump", internalfs_key_dump}, {"cart_dump", internalfs_cart_dump}, {NULL, NULL} diff --git a/arm9/source/utils/gameutil.c b/arm9/source/utils/gameutil.c index 8635129..a214efc 100644 --- a/arm9/source/utils/gameutil.c +++ b/arm9/source/utils/gameutil.c @@ -4359,4 +4359,4 @@ u32 GetGoodName(char* name, const char* path, bool quick) { } return 0; -} +} \ No newline at end of file diff --git a/arm9/source/virtual/vdisadiff.c b/arm9/source/virtual/vdisadiff.c index a4ced47..c091d65 100644 --- a/arm9/source/virtual/vdisadiff.c +++ b/arm9/source/virtual/vdisadiff.c @@ -1,3 +1,4 @@ +#include "vsaveexsv.h" // for mounting savedata/extdata #include "vdisadiff.h" #include "disadiff.h" #include "common.h" @@ -194,7 +195,9 @@ u64 InitVDisaDiffDrive(void) { partitionB_info->rw_info = info; } + // could be either of these InitVBDRIDrive(); + InitVSaveDrive(); return type; } diff --git a/arm9/source/virtual/virtual.c b/arm9/source/virtual/virtual.c index e960e03..a4fc27e 100644 --- a/arm9/source/virtual/virtual.c +++ b/arm9/source/virtual/virtual.c @@ -5,6 +5,7 @@ #include "vbdri.h" #include "vkeydb.h" #include "vcart.h" +#include "vsaveexsv.h" #include "vvram.h" #include "vdisadiff.h" #include "ff.h" @@ -29,13 +30,14 @@ u32 GetVirtualSource(const char* path) { void DeinitVirtualImageDrive(void) { DeinitVGameDrive(); DeinitVBDRIDrive(); + DeinitVSaveDrive(); DeinitVKeyDbDrive(); DeinitVDisaDiffDrive(); } bool InitVirtualImageDrive(void) { DeinitVirtualImageDrive(); - return InitVGameDrive() || InitVDisaDiffDrive() || InitVKeyDbDrive(); + return InitVGameDrive() || InitVDisaDiffDrive() /* calls InitVBDRIDrive and InitVSaveDrive */ || InitVKeyDbDrive(); } bool CheckVirtualDrive(const char* path) { @@ -52,6 +54,8 @@ bool CheckVirtualDrive(const char* path) { return CheckVKeyDbDrive(); else if (virtual_src & VRT_DISADIFF) return CheckVDisaDiffDrive(); + else if (virtual_src & VRT_SAVE) + return CheckVSaveDrive(); return virtual_src; // this is safe for SysNAND & memory } @@ -74,6 +78,8 @@ bool ReadVirtualDir(VirtualFile* vfile, VirtualDir* vdir) { ret = ReadVVramDir(vfile, vdir); } else if (virtual_src & VRT_DISADIFF) { ret = ReadVDisaDiffDir(vfile, vdir); + } else if (virtual_src & VRT_SAVE) { + ret = ReadVSaveDir(vfile, vdir); } vfile->flags |= virtual_src; // add source flag return ret; @@ -185,6 +191,8 @@ int ReadVirtualFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 coun return ReadVVramFile(vfile, buffer, offset, count); } else if (vfile->flags & VRT_DISADIFF) { return ReadVDisaDiffFile(vfile, buffer, offset, count); + } else if (vfile->flags & VRT_SAVE) { + return ReadVSaveFile(vfile, buffer, offset, count); } return -1; @@ -253,5 +261,7 @@ u64 GetVirtualDriveSize(const char* path) { return GetVCartDriveSize(); else if (virtual_src & VRT_VRAM) return GetVVramDriveSize(); + else if (virtual_src & VRT_SAVE) + return GetVSaveDriveSize(); return 0; } diff --git a/arm9/source/virtual/virtual.h b/arm9/source/virtual/virtual.h index 6543803..ea1e411 100644 --- a/arm9/source/virtual/virtual.h +++ b/arm9/source/virtual/virtual.h @@ -14,25 +14,26 @@ #define VRT_CART (1UL<<8) #define VRT_VRAM (1UL<<9) #define VRT_DISADIFF (1UL<<10) +#define VRT_SAVE (1UL<<11) -#define VRT_SOURCE (VRT_SYSNAND|VRT_EMUNAND|VRT_IMGNAND|VRT_XORPAD|VRT_MEMORY|VRT_GAME|VRT_BDRI|VRT_KEYDB|VRT_CART|VRT_VRAM|VRT_DISADIFF) +#define VRT_SOURCE (VRT_SYSNAND|VRT_EMUNAND|VRT_IMGNAND|VRT_XORPAD|VRT_MEMORY|VRT_GAME|VRT_BDRI|VRT_KEYDB|VRT_CART|VRT_VRAM|VRT_DISADIFF|VRT_SAVE) -#define VFLAG_DIR (1UL<<11) -#define VFLAG_ROOT (1UL<<12) -#define VFLAG_READONLY (1UL<<13) -#define VFLAG_DELETABLE (1UL<<14) -#define VFLAG_LV3 (1UL<<15) +#define VFLAG_DIR (1UL<<12) +#define VFLAG_ROOT (1UL<<13) +#define VFLAG_READONLY (1UL<<14) +#define VFLAG_DELETABLE (1UL<<15) +#define VFLAG_LV3 (1UL<<16) #define VRT_DRIVES {'S', VRT_SYSNAND}, {'E', VRT_EMUNAND}, {'I', VRT_IMGNAND}, {'X', VRT_XORPAD }, \ {'M', VRT_MEMORY}, {'G', VRT_GAME}, {'K', VRT_KEYDB}, {'T', VRT_BDRI}, \ - {'C', VRT_CART}, {'V', VRT_VRAM}, {'D', VRT_DISADIFF} + {'C', VRT_CART}, {'V', VRT_VRAM}, {'D', VRT_DISADIFF}, {'F', VRT_SAVE} // virtual file flag (subject to change): // bits 0...3 : reserved for NAND virtual sources and info -// bits 4...10 : reserved for other virtual sources -// bits 11...15: reserved for external flags -// bits 16...31: reserved for internal flags (different per source, see vgame.c) +// bits 4...11 : reserved for other virtual sources +// bits 12...16: reserved for external flags +// bits 17...31: reserved for internal flags (different per source, see vgame.c) typedef struct { char name[32]; u64 offset; // must be a multiple of 0x200 (for NAND access) diff --git a/arm9/source/virtual/vsaveexsv.c b/arm9/source/virtual/vsaveexsv.c new file mode 100644 index 0000000..ad8db7d --- /dev/null +++ b/arm9/source/virtual/vsaveexsv.c @@ -0,0 +1,172 @@ +#include "disadiff.h" +#include "image.h" +#include "vdisadiff.h" +#include "vsaveexsv.h" +#include "saveexsv.h" +#include "filetype.h" + +static SaveExsvFile sav = { 0 }; + +static char exsv_root_path[256] = { 0 }; +static char exsv_file_tmp_path[256] = { 0 }; +static DisaDiffRWInfo exsv_cur_file_cache = { 0 }; +static u64 last_exsv_file_id = 0; + +static int LoadExsvDiffFile(u64 id, u64 uniq) { + if (last_exsv_file_id == id && exsv_cur_file_cache.unique_id == uniq) { + return 0; + } + + u64 dir = id / 126; + if (dir > 0xFFFFFFFF) // there can be no more than 0xFFFFFFFF dirs (including dir 0) + return 1; + u32 file = id % 126; + + snprintf(exsv_file_tmp_path, sizeof(exsv_file_tmp_path), "%s%08lX/%08lX", exsv_root_path, (u32)dir, file); + if (GetDisaDiffRWInfo(exsv_file_tmp_path, &exsv_cur_file_cache, false) || exsv_cur_file_cache.unique_id != uniq) { + return 1; + } + + last_exsv_file_id = id; + return 0; +} + +u64 InitVSaveDrive(void) { // prerequisite: save mounted as virtual disa image + u64 mount_state = CheckVDisaDiffDrive(); + if (!(mount_state & (SYS_DISA | SYS_DIFF))) return 0; + + DeinitVSaveDrive(); + + if (SaveExsvFileInit(&sav) != 0) { + DeinitVSaveDrive(); + return 0; + } + + if (sav.is_exsv) { + // ensure we're initializing extdata from a correct structure + const char *exsv_root = GetMountPath(); + int pathlen = strlen(exsv_root); + + if (pathlen < 20 || strcmp(&exsv_root[pathlen - 17], "00000000/00000001") != 0) { + DeinitVSaveDrive(); + return 0; + } + + strncpy(exsv_root_path, exsv_root, pathlen - 17); + exsv_root_path[pathlen - 17] = 0; + } + + return mount_state; +} + +u64 CheckVSaveDrive(void) { + u64 mount_state = CheckVDisaDiffDrive(); + return ((mount_state & (SYS_DISA | SYS_DIFF)) && sav.init_ok) ? mount_state : 0; +} + +void DeinitVSaveDrive() { + SaveExsvFileFree(&sav); + memset(exsv_root_path, 0, sizeof(exsv_root_path)); + memset(exsv_file_tmp_path, 0, sizeof(exsv_file_tmp_path)); + memset(&exsv_cur_file_cache, 0, sizeof(exsv_cur_file_cache)); + last_exsv_file_id = 0; +} + +bool ReadVSaveDir(VirtualFile* vfile, VirtualDir* vdir) { + if (!CheckVSaveDrive()) + return false; + + if (vdir->index == -1) + vdir->index = 0; + + + if (vdir->index == 0) { + u32 dir_offset = vdir->offset == 0 ? 1 : vdir->offset; + SaveDirectoryEntry *dir = &sav.dir_entries[dir_offset]; + if (!dir->ent.first_file_index) { + vdir->index = 2; + } else { + vdir->index = 1; + vdir->offset = dir->ent.first_file_index; + } + } + + if (vdir->index == 1) { + SaveFileEntry *fent = &sav.file_entries[vdir->offset]; + vfile->offset = vdir->offset; + vfile->flags = VFLAG_READONLY; + if (sav.is_exsv) { + if (LoadExsvDiffFile(sav.exsv_extra_hdr.sub_file_base_id + vdir->offset, fent->ent.file_unique_id) != 0) { + vfile->size = 0; + } else { + vfile->size = exsv_cur_file_cache.size_ivfc_lvl4; + } + } else { + vfile->size = fent->ent.file_size; + } + memset(vfile->name, 0, sizeof(vfile->name)); + memcpy(vfile->name, fent->ent.key.name, sizeof(fent->ent.key.name)); + + if (fent->ent.next_sibling_index) { + vdir->offset = fent->ent.next_sibling_index; + } else { + vdir->offset = fent->ent.key.parent_dir_idx; + vdir->index = 2; + } + + return true; + } + + if (vdir->index == 2) { + SaveDirectoryEntry *dir = &sav.dir_entries[vdir->offset]; + if (!dir->ent.first_subdir_index) + return false; + else { + vdir->index = 3; + vdir->offset = dir->ent.first_subdir_index; + } + } + + if (vdir->index == 3) { + SaveDirectoryEntry *dir = &sav.dir_entries[vdir->offset]; + vfile->offset = vdir->offset; + vfile->flags = VFLAG_READONLY | VFLAG_DIR; + memset(vfile->name, 0, sizeof(vfile->name)); + memcpy(vfile->name, dir->ent.key.name, sizeof(dir->ent.key.name)); + + if (dir->ent.next_sibling_dir_index) { + vdir->offset = dir->ent.next_sibling_dir_index; + } else { + vdir->index = 4; + } + + return true; + } + + return false; +} + +int ReadVSaveFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count) { + if (vfile->flags & VFLAG_DIR) + return 1; + if (sav.is_exsv) { + SaveFileEntry *fent = &sav.file_entries[vfile->offset]; + if (LoadExsvDiffFile(sav.exsv_extra_hdr.sub_file_base_id + vfile->offset, fent->ent.file_unique_id) != 0) + return 1; + + return ReadDisaDiffIvfcLvl4(exsv_file_tmp_path, &exsv_cur_file_cache, offset, count, buffer) == count ? 0 : 1; + } else { + return SaveExsvReadFatFile(&sav, vfile->offset, buffer, (u32)offset, count); + } +} + +u64 GetVSaveDriveSize(void) { + if (!CheckVSaveDrive()) + return 0; + + if (sav.is_exsv) { + return 0; // maybe implement Quota.dat reading for this at some point + } else { + return sav.pre_header.fs_image_size_blocks * sav.pre_header.fs_image_blocksize; + } +} diff --git a/arm9/source/virtual/vsaveexsv.h b/arm9/source/virtual/vsaveexsv.h new file mode 100644 index 0000000..14df219 --- /dev/null +++ b/arm9/source/virtual/vsaveexsv.h @@ -0,0 +1,12 @@ +#pragma once + +#include "virtual.h" + +void DeinitVSaveDrive(void); +u64 InitVSaveDrive(void); +u64 CheckVSaveDrive(void); + +bool ReadVSaveDir(VirtualFile* vfile, VirtualDir* vdir); +int ReadVSaveFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count); + +u64 GetVSaveDriveSize(void); \ No newline at end of file diff --git a/common/common.h b/common/common.h index ff1f1b3..44e0ee0 100755 --- a/common/common.h +++ b/common/common.h @@ -49,6 +49,15 @@ #define align(v,a) \ (((v) % (a)) ? ((v) + (a) - ((v) % (a))) : (v)) +#define align_pow2(v,a) \ + (((v) + (a) - 1) & ~((a) - 1)) + +#define ceil_div(v,a) \ + (((v) + (a) - 1) / (a)) + +#define log_2(x) \ + (31 - __builtin_clz(x)) + #define countof(x) \ (sizeof(x) / sizeof(*(x))) diff --git a/data/luapackages/fs.lua b/data/luapackages/fs.lua index e223beb..32992f0 100644 --- a/data/luapackages/fs.lua +++ b/data/luapackages/fs.lua @@ -30,6 +30,7 @@ fs.allow = _fs.allow fs.sd_is_mounted = _fs.sd_is_mounted fs.sd_switch = _fs.sd_switch fs.fix_cmacs = _fs.fix_cmacs +fs.create_dbs = _fs.create_dbs fs.key_dump = _fs.key_dump fs.cart_dump = _fs.cart_dump @@ -38,27 +39,27 @@ function os.remove(path) local success, allowed, stat, error success, stat = pcall(fs.stat, path) if not success then - return nil, path..": No such file or directory", 2 + return nil, path .. ": No such file or directory", 2 end if stat.type == "dir" then -- os.remove can remove an empty directory, so we gotta check success, dir_list = pcall(fs.list_dir, path) if not success then - return nil, "Error occurred listing directory: "..dir_list, 2001 + return nil, "Error occurred listing directory: " .. dir_list, 2001 end if #dir_list ~= 0 then - return nil, path..": Directory not empty", 39 + return nil, path .. ": Directory not empty", 39 end end allowed = fs.allow(path) if not allowed then - return nil, path..": Operation not permitted", 1 + return nil, path .. ": Operation not permitted", 1 end - success, error = pcall(fs.remove, path, {recursive=true}) + success, error = pcall(fs.remove, path, { recursive = true }) if success then return true else - return nil, "Error occurred removing item: "..error, 2001 + return nil, "Error occurred removing item: " .. error, 2001 end end @@ -100,7 +101,7 @@ end function fs.verify_with_sha_file(path) local success, file_hash, sha_data, path_sha - path_sha = path..".sha" + path_sha = path .. ".sha" success, sha_data = pcall(fs.read_file, path_sha, 0, 0x20) if not success then return nil diff --git a/resources/languages/source.json b/resources/languages/source.json index da47f5d..07cb764 100644 --- a/resources/languages/source.json +++ b/resources/languages/source.json @@ -825,5 +825,14 @@ "VERIFY_SIGNATURES": "Verify signatures", "STANDARD_CRYPTO": "Standard encryption", "ORIGINAL_CRYPTO": "Original encryption", - "SELECT_TYPE_OF_ENCRYPTION": "Select type of encryption" + "SELECT_TYPE_OF_ENCRYPTION": "Select type of encryption", + "LAB_SAVE_FILE_IMAGE": "SAVE FILE IMAGE", + "LAB_EXTDATA_IMAGE": "EXTDATA IMAGE", + "CREATE_MISSING_DB_FILES": "Create missing .db files", + "PATH_FILE_ALREADY_EXISTS_RECREATE_IT_ANYWAY": "%s\nFile already exists. Recreate it anyway?", + "CREATE_DB_FILES_N_N_N_OK_FAILED_ALREADY_EXIST": "Create .db files ok/failed/already exist %d/%d/%d", + "CREATE_DB_FILES_N_N_OK_FAILED": "Create .db files ok/failed %d/%d", + "PATH_CREATE_DB_FILE_FAILED": "%s\nCreate .db file failed", + "NO_MISSING_DB_FILES_DETECTED": "No missing .db files detected.", + "SUCCESSFULLY_CREATED_DB_FILES_FOR_DRIVE": "Successfully created .db files for drive." } diff --git a/resources/lua-doc.md b/resources/lua-doc.md index c59df4b..b05e68e 100644 --- a/resources/lua-doc.md +++ b/resources/lua-doc.md @@ -284,7 +284,7 @@ The image must not be larger than 400 pixels horizontal or 240 pixels vertical. * **Arguments** * `path` - Path to PNG file -* **Throws** +* **Throws** * `"Could not read (file)"` - file does not exist or there was another read error * `"Invalid PNG file"` - file is not a valid PNG * `"PNG too large"` - too large horizontal or vertical, or an out-of-memory error @@ -756,6 +756,24 @@ Fix CMACs for a directory. * **Throws** * `fixcmac failed` - user denied permission, or fixing failed +#### fs.create_dbs + +* `void fs.create_dbs(string destdrv[, table opts {bool silent, bool overwrite}])` + +(Re)create potentially missing .db files (except certs.db) for the given drive. + +Supported drives are: +- SysNAND/EmuNAND CTRNAND (`1:`/`4:`): `import.db`, `ticket.db`, `title.db`, `tmp_i.db`, `tmp_t.db` +- SysNAND/EmuNAND SD (`A:`/`B:`): `import.db`, `title.db` + +* **Arguments** + * `destdrv` - Drive to (re)create .db files for. +* `opts` (optional) - Option flags + * `overwrite` - Overwrite existing .db files (use with caution!) + * `silent` - Do not show any messages during the process (e.g. which specific file couldn't be created, number of created/already existing files, etc.) +* **Throws** + * `createdbs failed` - user denied permission, or .db file(s) creation failed + #### fs.read_file * `string fs.read_file(string path, int offset, int/string size)` @@ -1168,9 +1186,9 @@ VERSION_LOW | 0x1 | Minor version of the MCU firmware. RESET_EVENTS | 0x2 | @ref mcu.reset_event_flags VCOM_TOP | 0x3 | Flicker/VCOM value for the top screen. VCOM_BOTTOM | 0x4 | Flicker/VCOM value for the bottom screen. -FIRMWARE_UPLOAD_0 | 0x5 | Firmware upload register. +FIRMWARE_UPLOAD_0 | 0x5 | Firmware upload register. FIRMWARE_UPLOAD_1 | 0x6 | Firmware upload register. -FIRMWARE_UPLOAD_2 | 0x7 | Firmware upload register. +FIRMWARE_UPLOAD_2 | 0x7 | Firmware upload register. RAW_3D_SLIDER_POSITION | 0x8 | Position of the 3D slider. VOLUME_SLIDER_POSITION | 0x9 | Position of the volume slider. BATTERY_PCB_TEMPERATURE | 0xA | Temperature of the battery, measured on a sensor on the PCB.