From b03ad4ade3c1b1e282cac41f1dd462fe81e9f14e Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:55:04 +0100 Subject: [PATCH] support for all save types (system, bcat, cache, device). --- sphaira/include/app.hpp | 2 +- sphaira/include/fs.hpp | 12 +- sphaira/include/ui/menus/save_menu.hpp | 6 + sphaira/source/ui/menus/save_menu.cpp | 974 +++++++++++++++---------- 4 files changed, 608 insertions(+), 386 deletions(-) diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index da2d4d2..5628593 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -195,7 +195,7 @@ public: static auto GetAccountList() -> std::vector { std::vector out; - AccountUid uids[8]; + AccountUid uids[ACC_USER_LIST_SIZE]; s32 account_count; if (R_SUCCEEDED(accountListAllUsers(uids, std::size(uids), &account_count))) { for (s32 i = 0; i < account_count; i++) { diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 99d9388..8b0cc81 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -551,11 +551,15 @@ struct FsNativeGameCard final : FsNative { }; struct FsNativeSave final : FsNative { - FsNativeSave(FsSaveDataSpaceId save_data_space_id, const FsSaveDataAttribute *attr, bool read_only) { - if (read_only) { - m_open_result = fsOpenReadOnlySaveDataFileSystem(&m_fs, save_data_space_id, attr); + FsNativeSave(FsSaveDataType data_type, FsSaveDataSpaceId save_data_space_id, const FsSaveDataAttribute *attr, bool read_only) { + if (data_type == FsSaveDataType_System || data_type == FsSaveDataType_SystemBcat) { + m_open_result = fsOpenSaveDataFileSystemBySystemSaveDataId(&m_fs, FsSaveDataSpaceId_System, attr); } else { - m_open_result = fsOpenSaveDataFileSystem(&m_fs, save_data_space_id, attr); + if (read_only) { + m_open_result = fsOpenReadOnlySaveDataFileSystem(&m_fs, save_data_space_id, attr); + } else { + m_open_result = fsOpenSaveDataFileSystem(&m_fs, save_data_space_id, attr); + } } } }; diff --git a/sphaira/include/ui/menus/save_menu.hpp b/sphaira/include/ui/menus/save_menu.hpp index c661b4a..a9e7afd 100644 --- a/sphaira/include/ui/menus/save_menu.hpp +++ b/sphaira/include/ui/menus/save_menu.hpp @@ -4,6 +4,7 @@ #include "ui/list.hpp" #include "fs.hpp" #include "option.hpp" +#include "dumper.hpp" #include #include #include @@ -126,6 +127,10 @@ private: void BackupSaves(std::vector>& entries); void RestoreSave(); + auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath; + Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const; + Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const; + private: static constexpr inline const char* INI_SECTION = "saves"; @@ -138,6 +143,7 @@ private: std::vector m_accounts{}; s64 m_account_index{}; + u8 m_data_type{FsSaveDataType_Account}; ThreadData m_thread_data{}; Thread m_thread{}; diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index b374523..2f2e1c9 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -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,8 +463,12 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) { void LoadControlEntry(Entry& e, bool force_image_load = false) { if (e.status == NacpLoadStatus::None) { - const auto result = LoadControlEntry(e.application_id); - LoadResultIntoEntry(e, result); + 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) { @@ -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); + fs::FsPath name; + 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 { - 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); + name = BuildSaveName(e); } - 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; - if (location.entry.type == dump::DumpLocationType_Stdio) { - fs = std::make_unique(true, location.stdio[location.entry.index].mount); - } else if (location.entry.type == dump::DumpLocationType_SdCard) { - fs = std::make_unique(); - } 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{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(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,44 +707,60 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { account_items.emplace_back(e.nickname); } + 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("Sort By"_i18n, [this](){ + auto options = std::make_shared("Sort Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + SidebarEntryArray::Items sort_items; + sort_items.push_back("Updated"_i18n); + + SidebarEntryArray::Items order_items; + order_items.push_back("Descending"_i18n); + order_items.push_back("Ascending"_i18n); + + SidebarEntryArray::Items layout_items; + layout_items.push_back("List"_i18n); + layout_items.push_back("Icon"_i18n); + layout_items.push_back("Grid"_i18n); + + options->Add(std::make_shared("Sort"_i18n, sort_items, [this](s64& index_out){ + m_sort.Set(index_out); + SortAndFindLastFile(false); + }, m_sort.Get())); + + options->Add(std::make_shared("Order"_i18n, order_items, [this](s64& index_out){ + m_order.Set(index_out); + SortAndFindLastFile(false); + }, m_order.Get())); + + options->Add(std::make_shared("Layout"_i18n, layout_items, [this](s64& index_out){ + m_layout.Set(index_out); + OnLayoutChange(); + }, m_layout.Get())); + })); + + options->Add(std::make_shared("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("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("Sort By"_i18n, [this](){ - auto options = std::make_shared("Sort Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(options)); - - SidebarEntryArray::Items sort_items; - sort_items.push_back("Updated"_i18n); - - SidebarEntryArray::Items order_items; - order_items.push_back("Descending"_i18n); - order_items.push_back("Ascending"_i18n); - - SidebarEntryArray::Items layout_items; - layout_items.push_back("List"_i18n); - layout_items.push_back("Icon"_i18n); - layout_items.push_back("Grid"_i18n); - - options->Add(std::make_shared("Sort"_i18n, sort_items, [this](s64& index_out){ - m_sort.Set(index_out); - SortAndFindLastFile(false); - }, m_sort.Get())); - - options->Add(std::make_shared("Order"_i18n, order_items, [this](s64& index_out){ - m_order.Set(index_out); - SortAndFindLastFile(false); - }, m_order.Get())); - - options->Add(std::make_shared("Layout"_i18n, layout_items, [this](s64& index_out){ - m_layout.Set(index_out); - OnLayoutChange(); - }, m_layout.Get())); - })); - - options->Add(std::make_shared("Account"_i18n, account_items, [this](s64& index_out){ - m_account_index = index_out; - m_dirty = true; - }, m_account_index)); - options->Add(std::make_shared("Backup"_i18n, [this](){ std::vector> entries; if (m_selected_count) { @@ -911,9 +776,11 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { BackupSaves(entries); }, true)); - options->Add(std::make_shared("Restore"_i18n, [this](){ - RestoreSave(); - }, 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("Restore"_i18n, [this](){ + RestoreSave(); + }, true)); + } } options->Add(std::make_shared("Advanced"_i18n, [this](){ @@ -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) { - m_thread_data.Push(e.application_id); - e.status = NacpLoadStatus::Progress; + 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; - DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), ""); + 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 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>& 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; + if (location.entry.type == dump::DumpLocationType_Stdio) { + fs = std::make_unique(true, location.stdio[location.entry.index].mount); + } else if (location.entry.type == dump::DumpLocationType_SdCard) { + fs = std::make_unique(); + } 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(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