GodMode9/arm9/source/game/saveexsv.c

261 lines
9.7 KiB
C
Raw Normal View History

#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);
}
}