|
|
|
|
@@ -87,6 +87,36 @@ auto GetNacpLangEntryIndex() -> u8 {
|
|
|
|
|
return g_nacpLangTable[lang];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceId& space_id, FsSaveDataFilter& filter) {
|
|
|
|
|
std::memset(&filter, 0, sizeof(filter));
|
|
|
|
|
|
|
|
|
|
space_id = FsSaveDataSpaceId_User;
|
|
|
|
|
filter.attr.save_data_type = data_type;
|
|
|
|
|
filter.filter_by_save_data_type = true;
|
|
|
|
|
|
|
|
|
|
switch (data_type) {
|
|
|
|
|
case FsSaveDataType_System:
|
|
|
|
|
case FsSaveDataType_SystemBcat:
|
|
|
|
|
space_id = FsSaveDataSpaceId_System;
|
|
|
|
|
break;
|
|
|
|
|
case FsSaveDataType_Account:
|
|
|
|
|
space_id = FsSaveDataSpaceId_User;
|
|
|
|
|
filter.attr.uid = acc.uid;
|
|
|
|
|
filter.filter_by_user_id = true;
|
|
|
|
|
break;
|
|
|
|
|
case FsSaveDataType_Bcat:
|
|
|
|
|
case FsSaveDataType_Device:
|
|
|
|
|
space_id = FsSaveDataSpaceId_User;
|
|
|
|
|
break;
|
|
|
|
|
case FsSaveDataType_Temporary:
|
|
|
|
|
space_id = FsSaveDataSpaceId_Temporary;
|
|
|
|
|
break;
|
|
|
|
|
case FsSaveDataType_Cache:
|
|
|
|
|
space_id = FsSaveDataSpaceId_SdUser;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
|
|
|
|
|
if (meta_type & 0x80) {
|
|
|
|
|
return 1 << (meta_type - 0x80);
|
|
|
|
|
@@ -189,6 +219,139 @@ void FakeNacpEntry(ThreadResultData& e) {
|
|
|
|
|
e.control.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto GetSaveFolder(u8 data_type) -> fs::FsPath {
|
|
|
|
|
switch (data_type) {
|
|
|
|
|
case FsSaveDataType_System: return "Save System";
|
|
|
|
|
case FsSaveDataType_SystemBcat: return "Save System Bcat";
|
|
|
|
|
case FsSaveDataType_Account: return "Save";
|
|
|
|
|
case FsSaveDataType_Bcat: return "Save Bcat";
|
|
|
|
|
case FsSaveDataType_Device: return "Save Device";
|
|
|
|
|
case FsSaveDataType_Temporary: return "Save Temporary";
|
|
|
|
|
case FsSaveDataType_Cache: return "Save Cache";
|
|
|
|
|
}
|
|
|
|
|
std::unreachable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto GetSaveFolder(const Entry& e) {
|
|
|
|
|
return GetSaveFolder(e.save_data_type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://switchbrew.org/wiki/Flash_Filesystem#SystemSaveData
|
|
|
|
|
auto GetSystemSaveName(u64 system_save_data_id) -> const char* {
|
|
|
|
|
switch (system_save_data_id) {
|
|
|
|
|
case 0x8000000000000000: return "fs"; break;
|
|
|
|
|
case 0x8000000000000010: return "account"; break;
|
|
|
|
|
case 0x8000000000000011: return "account"; break;
|
|
|
|
|
case 0x8000000000000020: return "nfc"; break;
|
|
|
|
|
case 0x8000000000000030: return "ns"; break;
|
|
|
|
|
case 0x8000000000000031: return "ns"; break;
|
|
|
|
|
case 0x8000000000000040: return "ns"; break;
|
|
|
|
|
case 0x8000000000000041: return "ns"; break;
|
|
|
|
|
case 0x8000000000000043: return "ns"; break;
|
|
|
|
|
case 0x8000000000000044: return "ns"; break;
|
|
|
|
|
case 0x8000000000000045: return "ns"; break;
|
|
|
|
|
case 0x8000000000000046: return "ns"; break;
|
|
|
|
|
case 0x8000000000000047: return "ns"; break;
|
|
|
|
|
case 0x8000000000000048: return "ns"; break;
|
|
|
|
|
case 0x8000000000000049: return "ns"; break;
|
|
|
|
|
case 0x800000000000004A: return "ns"; break;
|
|
|
|
|
case 0x8000000000000050: return "settings"; break;
|
|
|
|
|
case 0x8000000000000051: return "settings"; break;
|
|
|
|
|
case 0x8000000000000052: return "settings"; break;
|
|
|
|
|
case 0x8000000000000053: return "settings"; break;
|
|
|
|
|
case 0x8000000000000054: return "settings"; break;
|
|
|
|
|
case 0x8000000000000060: return "ssl"; break;
|
|
|
|
|
case 0x8000000000000061: return "ssl"; break; // guessing
|
|
|
|
|
case 0x8000000000000070: return "nim"; break;
|
|
|
|
|
case 0x8000000000000071: return "nim"; break;
|
|
|
|
|
case 0x8000000000000072: return "nim"; break;
|
|
|
|
|
case 0x8000000000000073: return "nim"; break;
|
|
|
|
|
case 0x8000000000000074: return "nim"; break;
|
|
|
|
|
case 0x8000000000000075: return "nim"; break;
|
|
|
|
|
case 0x8000000000000076: return "nim"; break;
|
|
|
|
|
case 0x8000000000000077: return "nim"; break;
|
|
|
|
|
case 0x8000000000000078: return "nim"; break;
|
|
|
|
|
case 0x8000000000000080: return "friends"; break;
|
|
|
|
|
case 0x8000000000000081: return "friends"; break;
|
|
|
|
|
case 0x8000000000000082: return "friends"; break;
|
|
|
|
|
case 0x8000000000000090: return "bcat"; break;
|
|
|
|
|
case 0x8000000000000091: return "bcat"; break;
|
|
|
|
|
case 0x8000000000000092: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000A0: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000A1: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000A2: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000B0: return "bsdsockets"; break;
|
|
|
|
|
case 0x80000000000000C1: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000C2: return "bcat"; break;
|
|
|
|
|
case 0x80000000000000D1: return "erpt"; break;
|
|
|
|
|
case 0x80000000000000E0: return "es"; break;
|
|
|
|
|
case 0x80000000000000E1: return "es"; break;
|
|
|
|
|
case 0x80000000000000E2: return "es"; break;
|
|
|
|
|
case 0x80000000000000E3: return "es"; break;
|
|
|
|
|
case 0x80000000000000E4: return "es"; break;
|
|
|
|
|
case 0x80000000000000F0: return "ns"; break;
|
|
|
|
|
case 0x8000000000000100: return "pctl"; break;
|
|
|
|
|
case 0x8000000000000110: return "npns"; break;
|
|
|
|
|
case 0x8000000000000120: return "ncm"; break;
|
|
|
|
|
case 0x8000000000000121: return "ncm"; break;
|
|
|
|
|
case 0x8000000000000122: return "ncm"; break;
|
|
|
|
|
case 0x8000000000000130: return "migration"; break;
|
|
|
|
|
case 0x8000000000000131: return "migration"; break;
|
|
|
|
|
case 0x8000000000000132: return "migration"; break;
|
|
|
|
|
case 0x8000000000000133: return "migration"; break;
|
|
|
|
|
case 0x8000000000000140: return "capsrv"; break;
|
|
|
|
|
case 0x8000000000000150: return "olsc"; break;
|
|
|
|
|
case 0x8000000000000151: return "olsc"; break;
|
|
|
|
|
case 0x8000000000000152: return "olsc"; break;
|
|
|
|
|
case 0x8000000000000153: return "olsc"; break;
|
|
|
|
|
case 0x8000000000000180: return "sdb"; break;
|
|
|
|
|
case 0x8000000000000190: return "glue"; break;
|
|
|
|
|
case 0x8000000000000200: return "bcat"; break;
|
|
|
|
|
case 0x8000000000000210: return "account"; break;
|
|
|
|
|
case 0x8000000000000220: return "erpt"; break;
|
|
|
|
|
case 0x8000000000001010: return "qlaunch"; break;
|
|
|
|
|
case 0x8000000000001011: return "qlaunch"; break;
|
|
|
|
|
case 0x8000000000001020: return "swkbd"; break;
|
|
|
|
|
case 0x8000000000001021: return "swkbd"; break;
|
|
|
|
|
case 0x8000000000001030: return "auth"; break;
|
|
|
|
|
case 0x8000000000001040: return "miiEdit"; break;
|
|
|
|
|
case 0x8000000000001050: return "miiEdit"; break;
|
|
|
|
|
case 0x8000000000001060: return "LibAppletShop"; break;
|
|
|
|
|
case 0x8000000000001061: return "LibAppletShop"; break;
|
|
|
|
|
case 0x8000000000001070: return "LibAppletWeb"; break;
|
|
|
|
|
case 0x8000000000001071: return "LibAppletWeb"; break;
|
|
|
|
|
case 0x8000000000001080: return "LibAppletOff"; break;
|
|
|
|
|
case 0x8000000000001081: return "LibAppletOff"; break;
|
|
|
|
|
case 0x8000000000001090: return "LibAppletLns"; break;
|
|
|
|
|
case 0x8000000000001091: return "LibAppletLns"; break;
|
|
|
|
|
case 0x80000000000010A0: return "LibAppletAuth"; break;
|
|
|
|
|
case 0x80000000000010A1: return "LibAppletAuth"; break;
|
|
|
|
|
case 0x80000000000010B0: return "playerSelect"; break;
|
|
|
|
|
case 0x80000000000010C0: return "myPage"; break;
|
|
|
|
|
case 0x80000000000010E1: return "qlaunch"; break;
|
|
|
|
|
case 0x8000000000001100: return "qlaunch"; break;
|
|
|
|
|
case 0x8000000000002000: return "DevMenu"; break;
|
|
|
|
|
case 0x8000000000002020: return "ns"; break;
|
|
|
|
|
case 0x8000000000010002: return "bcat"; break;
|
|
|
|
|
case 0x8000000000010003: return "bcat"; break;
|
|
|
|
|
case 0x8000000000010004: return "bcat"; break;
|
|
|
|
|
case 0x8000000000010005: return "bcat"; break;
|
|
|
|
|
case 0x8000000000010006: return "bcat"; break;
|
|
|
|
|
case 0x8000000000010007: return "bcat"; break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "Unknown";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FakeNacpEntryForSystem(Entry& e) {
|
|
|
|
|
e.status = NacpLoadStatus::Loaded;
|
|
|
|
|
|
|
|
|
|
// fake the nacp entry
|
|
|
|
|
std::snprintf(e.lang.name, sizeof(e.lang.name), "%s | %016lX", GetSystemSaveName(e.system_save_data_id), e.system_save_data_id);
|
|
|
|
|
std::strcpy(e.lang.author, "Nintendo");
|
|
|
|
|
e.control.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool LoadControlImage(Entry& e) {
|
|
|
|
|
if (!e.image && e.control) {
|
|
|
|
|
ON_SCOPE_EXIT(e.control.reset());
|
|
|
|
|
@@ -300,9 +463,13 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
|
|
|
|
|
|
|
|
|
|
void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
|
|
|
|
if (e.status == NacpLoadStatus::None) {
|
|
|
|
|
if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) {
|
|
|
|
|
FakeNacpEntryForSystem(e);
|
|
|
|
|
} else {
|
|
|
|
|
const auto result = LoadControlEntry(e.application_id);
|
|
|
|
|
LoadResultIntoEntry(e, result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
|
|
|
|
|
LoadControlImage(e);
|
|
|
|
|
@@ -368,332 +535,14 @@ auto BuildSaveName(const Entry& e) -> fs::FsPath {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto BuildSaveBasePath(const Entry& e) -> fs::FsPath {
|
|
|
|
|
const auto name = BuildSaveName(e);
|
|
|
|
|
return fs::AppendPath("/dumps/SAVE/", name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto BuildSavePath(const AccountProfileBase& acc, const Entry& e, bool is_auto) -> fs::FsPath {
|
|
|
|
|
const auto t = std::time(NULL);
|
|
|
|
|
const auto tm = std::localtime(&t);
|
|
|
|
|
const auto base = BuildSaveBasePath(e);
|
|
|
|
|
|
|
|
|
|
fs::FsPath name_buf;
|
|
|
|
|
if (is_auto) {
|
|
|
|
|
std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname);
|
|
|
|
|
} else {
|
|
|
|
|
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
|
|
|
|
|
}
|
|
|
|
|
utilsReplaceIllegalCharacters(name_buf, true);
|
|
|
|
|
|
|
|
|
|
fs::FsPath path;
|
|
|
|
|
std::snprintf(path, sizeof(path), "%s/%s - %u.%02u.%02u @ %02u.%02u.%02u.zip", base.s, name_buf.s, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) {
|
|
|
|
|
pbox->SetTitle(e.GetName());
|
|
|
|
|
if (e.image) {
|
|
|
|
|
pbox->SetImage(e.image);
|
|
|
|
|
} else if (e.control && e.jpeg_size) {
|
|
|
|
|
pbox->SetImageDataConst({e.control->icon, e.jpeg_size});
|
|
|
|
|
} else {
|
|
|
|
|
pbox->SetImage(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
|
|
|
|
|
|
|
|
|
// try and get the journal and data size.
|
|
|
|
|
FsSaveDataExtraData extra{};
|
|
|
|
|
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
|
|
|
|
|
|
|
|
|
log_write("restoring save: %s\n", path.s);
|
|
|
|
|
zlib_filefunc64_def file_func;
|
|
|
|
|
mz::FileFuncStdio(&file_func);
|
|
|
|
|
|
|
|
|
|
auto zfile = unzOpen2_64(path, &file_func);
|
|
|
|
|
R_UNLESS(zfile, 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(unzClose(zfile));
|
|
|
|
|
log_write("opened zip\n");
|
|
|
|
|
|
|
|
|
|
bool has_meta{};
|
|
|
|
|
NXSaveMeta meta{};
|
|
|
|
|
|
|
|
|
|
// get manifest
|
|
|
|
|
if (UNZ_END_OF_LIST_OF_FILE != unzLocateFile(zfile, NX_SAVE_META_NAME, 0)) {
|
|
|
|
|
log_write("found meta file\n");
|
|
|
|
|
if (UNZ_OK == unzOpenCurrentFile(zfile)) {
|
|
|
|
|
log_write("opened meta file\n");
|
|
|
|
|
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
|
|
|
|
|
|
|
|
|
const auto len = unzReadCurrentFile(zfile, &meta, sizeof(meta));
|
|
|
|
|
if (len == sizeof(meta) && meta.magic == NX_SAVE_META_MAGIC && meta.version == NX_SAVE_META_VERSION) {
|
|
|
|
|
has_meta = true;
|
|
|
|
|
log_write("loaded meta!\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (has_meta) {
|
|
|
|
|
log_write("extending save file\n");
|
|
|
|
|
R_TRY(fsExtendSaveDataFileSystem(save_data_space_id, e.save_data_id, meta.data_size, meta.journal_size));
|
|
|
|
|
log_write("extended save file\n");
|
|
|
|
|
} else {
|
|
|
|
|
log_write("doing manual meta parse\n");
|
|
|
|
|
s64 total_size{};
|
|
|
|
|
|
|
|
|
|
// todo:: manually calculate / guess the save size.
|
|
|
|
|
unz_global_info64 ginfo;
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGetGlobalInfo64(zfile, &ginfo), 0x1);
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGoToFirstFile(zfile), 0x1);
|
|
|
|
|
|
|
|
|
|
for (s64 i = 0; i < ginfo.number_entry; i++) {
|
|
|
|
|
R_TRY(pbox->ShouldExitResult());
|
|
|
|
|
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGoToNextFile(zfile), 0x1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
R_UNLESS(UNZ_OK == unzOpenCurrentFile(zfile), 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
|
|
|
|
|
|
|
|
|
unz_file_info64 info;
|
|
|
|
|
fs::FsPath name;
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0), 0x1);
|
|
|
|
|
|
|
|
|
|
if (name == NX_SAVE_META_NAME) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
total_size += info.uncompressed_size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: untested, should work tho.
|
|
|
|
|
const auto rounded_size = total_size + (total_size % extra.journal_size);
|
|
|
|
|
log_write("extendeing manual meta parse\n");
|
|
|
|
|
R_TRY(fsExtendSaveDataFileSystem(save_data_space_id, e.save_data_id, rounded_size, extra.journal_size));
|
|
|
|
|
log_write("extended manual meta parse\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FsSaveDataAttribute attr{};
|
|
|
|
|
attr.application_id = e.application_id;
|
|
|
|
|
attr.uid = e.uid;
|
|
|
|
|
attr.system_save_data_id = e.system_save_data_id;
|
|
|
|
|
attr.save_data_type = e.save_data_type;
|
|
|
|
|
attr.save_data_rank = e.save_data_rank;
|
|
|
|
|
attr.save_data_index = e.save_data_index;
|
|
|
|
|
|
|
|
|
|
// try and open the save file system.
|
|
|
|
|
fs::FsNativeSave save_fs{save_data_space_id, &attr, false};
|
|
|
|
|
R_TRY(save_fs.GetFsOpenResult());
|
|
|
|
|
|
|
|
|
|
log_write("opened save file\n");
|
|
|
|
|
// restore save data from zip.
|
|
|
|
|
R_TRY(thread::TransferUnzipAll(pbox, zfile, &save_fs, "/", [&](const fs::FsPath& name, fs::FsPath& path) -> bool {
|
|
|
|
|
// skip restoring the meta file.
|
|
|
|
|
if (name == NX_SAVE_META_NAME) {
|
|
|
|
|
log_write("skipping meta\n");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restore everything else.
|
|
|
|
|
log_write("restoring: %s\n", path.s);
|
|
|
|
|
|
|
|
|
|
// commit after every save otherwise FsError_MappingTableFull is thrown.
|
|
|
|
|
R_TRY(save_fs.Commit());
|
|
|
|
|
return true;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
log_write("finished, doing commit\n");
|
|
|
|
|
R_TRY(save_fs.Commit());
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const AccountProfileBase& acc, const Entry& e, bool compressed, bool is_auto = false) {
|
|
|
|
|
std::unique_ptr<fs::Fs> fs;
|
|
|
|
|
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
|
|
|
|
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
|
|
|
|
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
|
|
|
|
fs = std::make_unique<fs::FsNativeSd>();
|
|
|
|
|
if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) {
|
|
|
|
|
std::snprintf(name, sizeof(name), "%016lX", e.system_save_data_id);
|
|
|
|
|
} else {
|
|
|
|
|
R_THROW(0x1);
|
|
|
|
|
name = BuildSaveName(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pbox->SetTitle(e.GetName());
|
|
|
|
|
if (e.image) {
|
|
|
|
|
pbox->SetImage(e.image);
|
|
|
|
|
} else if (e.control && e.jpeg_size) {
|
|
|
|
|
pbox->SetImageDataConst({e.control->icon, e.jpeg_size});
|
|
|
|
|
} else {
|
|
|
|
|
pbox->SetImage(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
|
|
|
|
|
|
|
|
|
// try and get the journal and data size.
|
|
|
|
|
FsSaveDataExtraData extra{};
|
|
|
|
|
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
|
|
|
|
|
|
|
|
|
FsSaveDataAttribute attr{};
|
|
|
|
|
attr.application_id = e.application_id;
|
|
|
|
|
attr.uid = e.uid;
|
|
|
|
|
attr.system_save_data_id = e.system_save_data_id;
|
|
|
|
|
attr.save_data_type = e.save_data_type;
|
|
|
|
|
attr.save_data_rank = e.save_data_rank;
|
|
|
|
|
attr.save_data_index = e.save_data_index;
|
|
|
|
|
|
|
|
|
|
// try and open the save file system
|
|
|
|
|
fs::FsNativeSave save_fs{save_data_space_id, &attr, true};
|
|
|
|
|
R_TRY(save_fs.GetFsOpenResult());
|
|
|
|
|
|
|
|
|
|
// get a list of collections.
|
|
|
|
|
filebrowser::FsDirCollections collections;
|
|
|
|
|
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
|
|
|
|
|
|
|
|
|
|
// the save file may be empty, this isn't an error, but we exit early.
|
|
|
|
|
R_UNLESS(!collections.empty(), 0x0);
|
|
|
|
|
|
|
|
|
|
const auto t = std::time(NULL);
|
|
|
|
|
const auto tm = std::localtime(&t);
|
|
|
|
|
|
|
|
|
|
// pre-calculate the time rather than calculate it in the loop.
|
|
|
|
|
zip_fileinfo zip_info_default{};
|
|
|
|
|
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
|
|
|
|
|
zip_info_default.tmz_date.tm_min = tm->tm_min;
|
|
|
|
|
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
|
|
|
|
|
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
|
|
|
|
|
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
|
|
|
|
|
zip_info_default.tmz_date.tm_year = tm->tm_year;
|
|
|
|
|
|
|
|
|
|
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(acc, e, is_auto));
|
|
|
|
|
const auto temp_path = path + ".temp";
|
|
|
|
|
|
|
|
|
|
fs->CreateDirectoryRecursivelyWithPath(temp_path);
|
|
|
|
|
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
|
|
|
|
|
|
|
|
|
|
// zip to memory if less than 1GB and not applet mode.
|
|
|
|
|
// TODO: use my mmz code from ftpsrv to stream zip creation.
|
|
|
|
|
// this will allow for zipping to memory and flushing every X bytes
|
|
|
|
|
// such as flushing every 8MB.
|
|
|
|
|
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
|
|
|
|
|
|
|
|
|
|
mz::MzMem mz_mem{};
|
|
|
|
|
zlib_filefunc64_def file_func;
|
|
|
|
|
if (!file_download) {
|
|
|
|
|
mz::FileFuncMem(&mz_mem, &file_func);
|
|
|
|
|
} else {
|
|
|
|
|
mz::FileFuncStdio(&file_func);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
auto zfile = zipOpen2_64(temp_path, APPEND_STATUS_CREATE, nullptr, &file_func);
|
|
|
|
|
R_UNLESS(zfile, 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
|
|
|
|
|
|
|
|
|
// add save meta.
|
|
|
|
|
{
|
|
|
|
|
const NXSaveMeta meta{
|
|
|
|
|
.magic = NX_SAVE_META_MAGIC,
|
|
|
|
|
.version = NX_SAVE_META_VERSION,
|
|
|
|
|
.attr = extra.attr,
|
|
|
|
|
.owner_id = extra.owner_id,
|
|
|
|
|
.timestamp = extra.timestamp,
|
|
|
|
|
.flags = extra.flags,
|
|
|
|
|
.unk_x54 = extra.unk_x54,
|
|
|
|
|
.data_size = extra.data_size,
|
|
|
|
|
.journal_size = extra.journal_size,
|
|
|
|
|
.commit_id = extra.commit_id,
|
|
|
|
|
.raw_size = e.size,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
|
|
|
|
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), 0x1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto zip_add = [&](const FsDirectoryEntry& dir_entry, const fs::FsPath& file_path) -> Result {
|
|
|
|
|
auto zip_info = zip_info_default;
|
|
|
|
|
|
|
|
|
|
// try and load the actual timestamp of the file.
|
|
|
|
|
// TODO: not supported for saves...
|
|
|
|
|
#if 1
|
|
|
|
|
FsTimeStampRaw timestamp{};
|
|
|
|
|
if (R_SUCCEEDED(save_fs.GetFileTimeStampRaw(file_path, ×tamp)) && timestamp.is_valid) {
|
|
|
|
|
const auto time = (time_t)timestamp.modified;
|
|
|
|
|
if (auto tm = localtime(&time)) {
|
|
|
|
|
zip_info.tmz_date.tm_sec = tm->tm_sec;
|
|
|
|
|
zip_info.tmz_date.tm_min = tm->tm_min;
|
|
|
|
|
zip_info.tmz_date.tm_hour = tm->tm_hour;
|
|
|
|
|
zip_info.tmz_date.tm_mday = tm->tm_mday;
|
|
|
|
|
zip_info.tmz_date.tm_mon = tm->tm_mon;
|
|
|
|
|
zip_info.tmz_date.tm_year = tm->tm_year;
|
|
|
|
|
log_write("got timestamp!\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// the file name needs to be relative to the current directory.
|
|
|
|
|
const char* file_name_in_zip = file_path.s + std::strlen("/");
|
|
|
|
|
|
|
|
|
|
// strip root path (/ or ums0:)
|
|
|
|
|
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
|
|
|
|
|
file_name_in_zip += std::strlen(save_fs.Root());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// root paths are banned in zips, they will warn when extracting otherwise.
|
|
|
|
|
if (file_name_in_zip[0] == '/') {
|
|
|
|
|
file_name_in_zip++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pbox->NewTransfer(file_name_in_zip);
|
|
|
|
|
|
|
|
|
|
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
|
|
|
|
|
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
|
|
|
|
|
log_write("failed to add zip for %s\n", file_path.s);
|
|
|
|
|
R_THROW(0x1);
|
|
|
|
|
}
|
|
|
|
|
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
|
|
|
|
|
|
|
|
|
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// loop through every save file and store to zip.
|
|
|
|
|
for (const auto& collection : collections) {
|
|
|
|
|
for (const auto& file : collection.files) {
|
|
|
|
|
const auto file_path = fs::AppendPath(collection.path, file.name);
|
|
|
|
|
R_TRY(zip_add(file, file_path));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if we dumped the save to ram, flush the data to file.
|
|
|
|
|
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
|
|
|
|
if (!file_download) {
|
|
|
|
|
pbox->NewTransfer("Flushing zip to file");
|
|
|
|
|
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0));
|
|
|
|
|
|
|
|
|
|
fs::File file;
|
|
|
|
|
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
|
|
|
|
|
|
|
|
|
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
|
|
|
|
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
|
|
|
|
size = std::min<s64>(size, mz_mem.buf.size() - off);
|
|
|
|
|
std::memcpy(data, mz_mem.buf.data() + off, size);
|
|
|
|
|
*bytes_read = size;
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
},
|
|
|
|
|
[&](const void* data, s64 off, s64 size) -> Result {
|
|
|
|
|
const auto rc = file.Write(off, data, size, FsWriteOption_None);
|
|
|
|
|
if (is_file_based_emummc) {
|
|
|
|
|
svcSleepThread(2e+6); // 2ms
|
|
|
|
|
}
|
|
|
|
|
return rc;
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs->DeleteFile(path);
|
|
|
|
|
R_TRY(fs->RenameFile(temp_path, path));
|
|
|
|
|
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
return fs::AppendPath("/dumps/" + GetSaveFolder(e), name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FreeEntry(NVGcontext* vg, Entry& e) {
|
|
|
|
|
@@ -858,7 +707,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
|
|
|
|
account_items.emplace_back(e.nickname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_entries.size()) {
|
|
|
|
|
PopupList::Items data_type_items;
|
|
|
|
|
data_type_items.emplace_back("System"_i18n);
|
|
|
|
|
data_type_items.emplace_back("Account"_i18n);
|
|
|
|
|
data_type_items.emplace_back("Bcat"_i18n);
|
|
|
|
|
data_type_items.emplace_back("Device"_i18n);
|
|
|
|
|
data_type_items.emplace_back("Temporary"_i18n);
|
|
|
|
|
data_type_items.emplace_back("Cache"_i18n);
|
|
|
|
|
data_type_items.emplace_back("SystemBcat"_i18n);
|
|
|
|
|
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryCallback>("Sort By"_i18n, [this](){
|
|
|
|
|
auto options = std::make_shared<Sidebar>("Sort Options"_i18n, Sidebar::Side::RIGHT);
|
|
|
|
|
ON_SCOPE_EXIT(App::Push(options));
|
|
|
|
|
@@ -894,8 +751,16 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryArray>("Account"_i18n, account_items, [this](s64& index_out){
|
|
|
|
|
m_account_index = index_out;
|
|
|
|
|
m_dirty = true;
|
|
|
|
|
App::PopToMenu();
|
|
|
|
|
}, m_account_index));
|
|
|
|
|
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryArray>("Data Type"_i18n, data_type_items, [this](s64& index_out){
|
|
|
|
|
m_data_type = index_out;
|
|
|
|
|
m_dirty = true;
|
|
|
|
|
App::PopToMenu();
|
|
|
|
|
}, m_data_type));
|
|
|
|
|
|
|
|
|
|
if (m_entries.size()) {
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryCallback>("Backup"_i18n, [this](){
|
|
|
|
|
std::vector<std::reference_wrapper<Entry>> entries;
|
|
|
|
|
if (m_selected_count) {
|
|
|
|
|
@@ -911,10 +776,12 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
|
|
|
|
BackupSaves(entries);
|
|
|
|
|
}, true));
|
|
|
|
|
|
|
|
|
|
if (m_entries[m_index].save_data_type == FsSaveDataType_Account || m_entries[m_index].save_data_type == FsSaveDataType_Bcat) {
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryCallback>("Restore"_i18n, [this](){
|
|
|
|
|
RestoreSave();
|
|
|
|
|
}, true));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
|
|
|
|
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
|
|
|
|
@@ -1011,12 +878,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|
|
|
|
m_thread_data.Pop(data);
|
|
|
|
|
|
|
|
|
|
for (const auto& d : data) {
|
|
|
|
|
const auto it = std::ranges::find_if(m_entries, [&d](auto& e) {
|
|
|
|
|
return e.application_id == d.id;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (it != m_entries.end()) {
|
|
|
|
|
LoadResultIntoEntry(*it, d);
|
|
|
|
|
for (auto& e : m_entries) {
|
|
|
|
|
if (e.application_id == d.id) {
|
|
|
|
|
// don't break out of loop as multiple entries may use
|
|
|
|
|
// the same tid, such as cached saves.
|
|
|
|
|
LoadResultIntoEntry(e, d);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1025,8 +892,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|
|
|
|
auto& e = m_entries[pos];
|
|
|
|
|
|
|
|
|
|
if (e.status == NacpLoadStatus::None) {
|
|
|
|
|
if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) {
|
|
|
|
|
m_thread_data.Push(e.application_id);
|
|
|
|
|
e.status = NacpLoadStatus::Progress;
|
|
|
|
|
} else {
|
|
|
|
|
FakeNacpEntryForSystem(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lazy load image
|
|
|
|
|
@@ -1037,10 +908,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto selected = pos == m_index;
|
|
|
|
|
if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) {
|
|
|
|
|
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), "");
|
|
|
|
|
} else {
|
|
|
|
|
const auto image_vec = DrawEntryNoImage(vg, theme, m_layout.Get(), v, selected, e.GetName(), e.GetAuthor(), "");
|
|
|
|
|
gfx::drawRect(vg, v, theme->GetColour(ThemeEntryID_GRID), 5);
|
|
|
|
|
gfx::drawTextArgs(vg, image_vec.x + image_vec.w / 2, image_vec.y + image_vec.w / 2, 20, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(selected ? ThemeEntryID_TEXT_SELECTED : ThemeEntryID_TEXT), GetSystemSaveName(e.system_save_data_id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.selected) {
|
|
|
|
|
gfx::drawRect(vg, v, nvgRGBA(0, 0, 0, 180), 5);
|
|
|
|
|
gfx::drawRect(vg, v, theme->GetColour(ThemeEntryID_FOCUS), 5);
|
|
|
|
|
gfx::drawText(vg, x + w / 2, y + h / 2, 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
@@ -1076,12 +953,12 @@ void Menu::ScanHomebrew() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FsSaveDataFilter filter{};
|
|
|
|
|
filter.attr.uid = m_accounts[m_account_index].uid;
|
|
|
|
|
filter.filter_by_user_id = true;
|
|
|
|
|
FsSaveDataSpaceId space_id;
|
|
|
|
|
FsSaveDataFilter filter;
|
|
|
|
|
GetFsSaveAttr(m_accounts[m_account_index], m_data_type, space_id, filter);
|
|
|
|
|
|
|
|
|
|
FsSaveDataInfoReader reader;
|
|
|
|
|
fsOpenSaveDataInfoReaderWithFilter(&reader, FsSaveDataSpaceId_User, &filter);
|
|
|
|
|
fsOpenSaveDataInfoReaderWithFilter(&reader, space_id, &filter);
|
|
|
|
|
ON_SCOPE_EXIT(fsSaveDataInfoReaderClose(&reader));
|
|
|
|
|
|
|
|
|
|
std::vector<FsSaveDataInfo> info_list(ENTRY_CHUNK_COUNT);
|
|
|
|
|
@@ -1089,6 +966,7 @@ void Menu::ScanHomebrew() {
|
|
|
|
|
s64 record_count{};
|
|
|
|
|
if (R_FAILED(fsSaveDataInfoReaderRead(&reader, info_list.data(), info_list.size(), &record_count))) {
|
|
|
|
|
log_write("failed fsSaveDataInfoReaderRead()\n");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// finished parsing all entries.
|
|
|
|
|
@@ -1177,7 +1055,7 @@ void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
|
|
|
|
|
for (auto& e : entries) {
|
|
|
|
|
// the entry may not have loaded yet.
|
|
|
|
|
LoadControlEntry(e);
|
|
|
|
|
R_TRY(BackupSaveInternal(pbox, location, m_accounts[m_account_index], e, m_compress_save_backup.Get()));
|
|
|
|
|
R_TRY(BackupSaveInternal(pbox, location, e, m_compress_save_backup.Get()));
|
|
|
|
|
}
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
}, [](Result rc){
|
|
|
|
|
@@ -1246,7 +1124,7 @@ void Menu::RestoreSave() {
|
|
|
|
|
|
|
|
|
|
if (m_auto_backup_on_restore.Get()) {
|
|
|
|
|
pbox->SetActionName("Auto backup"_i18n);
|
|
|
|
|
R_TRY(BackupSaveInternal(pbox, location, m_accounts[m_account_index], m_entries[m_index], m_compress_save_backup.Get(), true));
|
|
|
|
|
R_TRY(BackupSaveInternal(pbox, location, m_entries[m_index], m_compress_save_backup.Get(), true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pbox->SetActionName("Restore"_i18n);
|
|
|
|
|
@@ -1266,4 +1144,338 @@ void Menu::RestoreSave() {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
|
|
|
|
|
const auto t = std::time(NULL);
|
|
|
|
|
const auto tm = std::localtime(&t);
|
|
|
|
|
const auto base = BuildSaveBasePath(e);
|
|
|
|
|
|
|
|
|
|
char time[64];
|
|
|
|
|
std::snprintf(time, sizeof(time), "%u.%02u.%02u @ %02u.%02u.%02u", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
|
|
|
|
|
|
|
|
|
|
fs::FsPath path;
|
|
|
|
|
if (e.save_data_type == FsSaveDataType_Account) {
|
|
|
|
|
const auto acc = m_accounts[m_account_index];
|
|
|
|
|
|
|
|
|
|
fs::FsPath name_buf;
|
|
|
|
|
if (is_auto) {
|
|
|
|
|
std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname);
|
|
|
|
|
} else {
|
|
|
|
|
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
utilsReplaceIllegalCharacters(name_buf, true);
|
|
|
|
|
std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time);
|
|
|
|
|
} else {
|
|
|
|
|
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const {
|
|
|
|
|
pbox->SetTitle(e.GetName());
|
|
|
|
|
if (e.image) {
|
|
|
|
|
pbox->SetImage(e.image);
|
|
|
|
|
} else if (e.control && e.jpeg_size) {
|
|
|
|
|
pbox->SetImageDataConst({e.control->icon, e.jpeg_size});
|
|
|
|
|
} else {
|
|
|
|
|
pbox->SetImage(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
|
|
|
|
|
|
|
|
|
// try and get the journal and data size.
|
|
|
|
|
FsSaveDataExtraData extra{};
|
|
|
|
|
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
|
|
|
|
|
|
|
|
|
log_write("restoring save: %s\n", path.s);
|
|
|
|
|
zlib_filefunc64_def file_func;
|
|
|
|
|
mz::FileFuncStdio(&file_func);
|
|
|
|
|
|
|
|
|
|
auto zfile = unzOpen2_64(path, &file_func);
|
|
|
|
|
R_UNLESS(zfile, 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(unzClose(zfile));
|
|
|
|
|
log_write("opened zip\n");
|
|
|
|
|
|
|
|
|
|
bool has_meta{};
|
|
|
|
|
NXSaveMeta meta{};
|
|
|
|
|
|
|
|
|
|
// get manifest
|
|
|
|
|
if (UNZ_END_OF_LIST_OF_FILE != unzLocateFile(zfile, NX_SAVE_META_NAME, 0)) {
|
|
|
|
|
log_write("found meta file\n");
|
|
|
|
|
if (UNZ_OK == unzOpenCurrentFile(zfile)) {
|
|
|
|
|
log_write("opened meta file\n");
|
|
|
|
|
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
|
|
|
|
|
|
|
|
|
const auto len = unzReadCurrentFile(zfile, &meta, sizeof(meta));
|
|
|
|
|
if (len == sizeof(meta) && meta.magic == NX_SAVE_META_MAGIC && meta.version == NX_SAVE_META_VERSION) {
|
|
|
|
|
has_meta = true;
|
|
|
|
|
log_write("loaded meta!\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (has_meta) {
|
|
|
|
|
log_write("extending save file\n");
|
|
|
|
|
R_TRY(fsExtendSaveDataFileSystem(save_data_space_id, e.save_data_id, meta.data_size, meta.journal_size));
|
|
|
|
|
log_write("extended save file\n");
|
|
|
|
|
} else {
|
|
|
|
|
log_write("doing manual meta parse\n");
|
|
|
|
|
s64 total_size{};
|
|
|
|
|
|
|
|
|
|
// todo:: manually calculate / guess the save size.
|
|
|
|
|
unz_global_info64 ginfo;
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGetGlobalInfo64(zfile, &ginfo), 0x1);
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGoToFirstFile(zfile), 0x1);
|
|
|
|
|
|
|
|
|
|
for (s64 i = 0; i < ginfo.number_entry; i++) {
|
|
|
|
|
R_TRY(pbox->ShouldExitResult());
|
|
|
|
|
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGoToNextFile(zfile), 0x1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
R_UNLESS(UNZ_OK == unzOpenCurrentFile(zfile), 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
|
|
|
|
|
|
|
|
|
unz_file_info64 info;
|
|
|
|
|
fs::FsPath name;
|
|
|
|
|
R_UNLESS(UNZ_OK == unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0), 0x1);
|
|
|
|
|
|
|
|
|
|
if (name == NX_SAVE_META_NAME) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
total_size += info.uncompressed_size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: untested, should work tho.
|
|
|
|
|
const auto rounded_size = total_size + (total_size % extra.journal_size);
|
|
|
|
|
log_write("extendeing manual meta parse\n");
|
|
|
|
|
R_TRY(fsExtendSaveDataFileSystem(save_data_space_id, e.save_data_id, rounded_size, extra.journal_size));
|
|
|
|
|
log_write("extended manual meta parse\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FsSaveDataAttribute attr{};
|
|
|
|
|
attr.application_id = e.application_id;
|
|
|
|
|
attr.uid = e.uid;
|
|
|
|
|
attr.system_save_data_id = e.system_save_data_id;
|
|
|
|
|
attr.save_data_type = e.save_data_type;
|
|
|
|
|
attr.save_data_rank = e.save_data_rank;
|
|
|
|
|
attr.save_data_index = e.save_data_index;
|
|
|
|
|
|
|
|
|
|
// try and open the save file system.
|
|
|
|
|
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, false};
|
|
|
|
|
R_TRY(save_fs.GetFsOpenResult());
|
|
|
|
|
|
|
|
|
|
log_write("opened save file\n");
|
|
|
|
|
// restore save data from zip.
|
|
|
|
|
R_TRY(thread::TransferUnzipAll(pbox, zfile, &save_fs, "/", [&](const fs::FsPath& name, fs::FsPath& path) -> bool {
|
|
|
|
|
// skip restoring the meta file.
|
|
|
|
|
if (name == NX_SAVE_META_NAME) {
|
|
|
|
|
log_write("skipping meta\n");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restore everything else.
|
|
|
|
|
log_write("restoring: %s\n", path.s);
|
|
|
|
|
|
|
|
|
|
// commit after every save otherwise FsError_MappingTableFull is thrown.
|
|
|
|
|
R_TRY(save_fs.Commit());
|
|
|
|
|
return true;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
log_write("finished, doing commit\n");
|
|
|
|
|
R_TRY(save_fs.Commit());
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto) const {
|
|
|
|
|
std::unique_ptr<fs::Fs> fs;
|
|
|
|
|
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
|
|
|
|
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
|
|
|
|
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
|
|
|
|
fs = std::make_unique<fs::FsNativeSd>();
|
|
|
|
|
} else {
|
|
|
|
|
R_THROW(0x1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pbox->SetTitle(e.GetName());
|
|
|
|
|
if (e.image) {
|
|
|
|
|
pbox->SetImage(e.image);
|
|
|
|
|
} else if (e.control && e.jpeg_size) {
|
|
|
|
|
pbox->SetImageDataConst({e.control->icon, e.jpeg_size});
|
|
|
|
|
} else {
|
|
|
|
|
pbox->SetImage(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
|
|
|
|
|
|
|
|
|
// try and get the journal and data size.
|
|
|
|
|
FsSaveDataExtraData extra{};
|
|
|
|
|
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
|
|
|
|
|
|
|
|
|
FsSaveDataAttribute attr{};
|
|
|
|
|
attr.application_id = e.application_id;
|
|
|
|
|
attr.uid = e.uid;
|
|
|
|
|
attr.system_save_data_id = e.system_save_data_id;
|
|
|
|
|
attr.save_data_type = e.save_data_type;
|
|
|
|
|
attr.save_data_rank = e.save_data_rank;
|
|
|
|
|
attr.save_data_index = e.save_data_index;
|
|
|
|
|
|
|
|
|
|
// try and open the save file system
|
|
|
|
|
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true};
|
|
|
|
|
R_TRY(save_fs.GetFsOpenResult());
|
|
|
|
|
|
|
|
|
|
// get a list of collections.
|
|
|
|
|
filebrowser::FsDirCollections collections;
|
|
|
|
|
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
|
|
|
|
|
|
|
|
|
|
// the save file may be empty, this isn't an error, but we exit early.
|
|
|
|
|
R_UNLESS(!collections.empty(), 0x0);
|
|
|
|
|
|
|
|
|
|
const auto t = std::time(NULL);
|
|
|
|
|
const auto tm = std::localtime(&t);
|
|
|
|
|
|
|
|
|
|
// pre-calculate the time rather than calculate it in the loop.
|
|
|
|
|
zip_fileinfo zip_info_default{};
|
|
|
|
|
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
|
|
|
|
|
zip_info_default.tmz_date.tm_min = tm->tm_min;
|
|
|
|
|
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
|
|
|
|
|
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
|
|
|
|
|
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
|
|
|
|
|
zip_info_default.tmz_date.tm_year = tm->tm_year;
|
|
|
|
|
|
|
|
|
|
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(e, is_auto));
|
|
|
|
|
const auto temp_path = path + ".temp";
|
|
|
|
|
|
|
|
|
|
fs->CreateDirectoryRecursivelyWithPath(temp_path);
|
|
|
|
|
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
|
|
|
|
|
|
|
|
|
|
// zip to memory if less than 1GB and not applet mode.
|
|
|
|
|
// TODO: use my mmz code from ftpsrv to stream zip creation.
|
|
|
|
|
// this will allow for zipping to memory and flushing every X bytes
|
|
|
|
|
// such as flushing every 8MB.
|
|
|
|
|
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
|
|
|
|
|
|
|
|
|
|
mz::MzMem mz_mem{};
|
|
|
|
|
zlib_filefunc64_def file_func;
|
|
|
|
|
if (!file_download) {
|
|
|
|
|
mz::FileFuncMem(&mz_mem, &file_func);
|
|
|
|
|
} else {
|
|
|
|
|
mz::FileFuncStdio(&file_func);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
auto zfile = zipOpen2_64(temp_path, APPEND_STATUS_CREATE, nullptr, &file_func);
|
|
|
|
|
R_UNLESS(zfile, 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
|
|
|
|
|
|
|
|
|
// add save meta.
|
|
|
|
|
{
|
|
|
|
|
const NXSaveMeta meta{
|
|
|
|
|
.magic = NX_SAVE_META_MAGIC,
|
|
|
|
|
.version = NX_SAVE_META_VERSION,
|
|
|
|
|
.attr = extra.attr,
|
|
|
|
|
.owner_id = extra.owner_id,
|
|
|
|
|
.timestamp = extra.timestamp,
|
|
|
|
|
.flags = extra.flags,
|
|
|
|
|
.unk_x54 = extra.unk_x54,
|
|
|
|
|
.data_size = extra.data_size,
|
|
|
|
|
.journal_size = extra.journal_size,
|
|
|
|
|
.commit_id = extra.commit_id,
|
|
|
|
|
.raw_size = e.size,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), 0x1);
|
|
|
|
|
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
|
|
|
|
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), 0x1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto zip_add = [&](const FsDirectoryEntry& dir_entry, const fs::FsPath& file_path) -> Result {
|
|
|
|
|
auto zip_info = zip_info_default;
|
|
|
|
|
|
|
|
|
|
// try and load the actual timestamp of the file.
|
|
|
|
|
// TODO: not supported for saves...
|
|
|
|
|
#if 1
|
|
|
|
|
FsTimeStampRaw timestamp{};
|
|
|
|
|
if (R_SUCCEEDED(save_fs.GetFileTimeStampRaw(file_path, ×tamp)) && timestamp.is_valid) {
|
|
|
|
|
const auto time = (time_t)timestamp.modified;
|
|
|
|
|
if (auto tm = localtime(&time)) {
|
|
|
|
|
zip_info.tmz_date.tm_sec = tm->tm_sec;
|
|
|
|
|
zip_info.tmz_date.tm_min = tm->tm_min;
|
|
|
|
|
zip_info.tmz_date.tm_hour = tm->tm_hour;
|
|
|
|
|
zip_info.tmz_date.tm_mday = tm->tm_mday;
|
|
|
|
|
zip_info.tmz_date.tm_mon = tm->tm_mon;
|
|
|
|
|
zip_info.tmz_date.tm_year = tm->tm_year;
|
|
|
|
|
log_write("got timestamp!\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// the file name needs to be relative to the current directory.
|
|
|
|
|
const char* file_name_in_zip = file_path.s + std::strlen("/");
|
|
|
|
|
|
|
|
|
|
// strip root path (/ or ums0:)
|
|
|
|
|
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
|
|
|
|
|
file_name_in_zip += std::strlen(save_fs.Root());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// root paths are banned in zips, they will warn when extracting otherwise.
|
|
|
|
|
if (file_name_in_zip[0] == '/') {
|
|
|
|
|
file_name_in_zip++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pbox->NewTransfer(file_name_in_zip);
|
|
|
|
|
|
|
|
|
|
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
|
|
|
|
|
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
|
|
|
|
|
log_write("failed to add zip for %s\n", file_path.s);
|
|
|
|
|
R_THROW(0x1);
|
|
|
|
|
}
|
|
|
|
|
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
|
|
|
|
|
|
|
|
|
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// loop through every save file and store to zip.
|
|
|
|
|
for (const auto& collection : collections) {
|
|
|
|
|
for (const auto& file : collection.files) {
|
|
|
|
|
const auto file_path = fs::AppendPath(collection.path, file.name);
|
|
|
|
|
R_TRY(zip_add(file, file_path));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if we dumped the save to ram, flush the data to file.
|
|
|
|
|
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
|
|
|
|
if (!file_download) {
|
|
|
|
|
pbox->NewTransfer("Flushing zip to file");
|
|
|
|
|
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0));
|
|
|
|
|
|
|
|
|
|
fs::File file;
|
|
|
|
|
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
|
|
|
|
|
|
|
|
|
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
|
|
|
|
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
|
|
|
|
size = std::min<s64>(size, mz_mem.buf.size() - off);
|
|
|
|
|
std::memcpy(data, mz_mem.buf.data() + off, size);
|
|
|
|
|
*bytes_read = size;
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
},
|
|
|
|
|
[&](const void* data, s64 off, s64 size) -> Result {
|
|
|
|
|
const auto rc = file.Write(off, data, size, FsWriteOption_None);
|
|
|
|
|
if (is_file_based_emummc) {
|
|
|
|
|
svcSleepThread(2e+6); // 2ms
|
|
|
|
|
}
|
|
|
|
|
return rc;
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs->DeleteFile(path);
|
|
|
|
|
R_TRY(fs->RenameFile(temp_path, path));
|
|
|
|
|
|
|
|
|
|
R_SUCCEED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace sphaira::ui::menu::save
|
|
|
|
|
|