From 7abe1085e3834e26dfcda71ab4d46674f2c1f0b3 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 31 Dec 2020 16:59:25 +0100 Subject: [PATCH 1/5] IOS/ES: Pass relevant caller title information to ImportTmd() and ExportTitleInit(). --- Source/Core/Core/IOS/ES/ES.h | 6 +++-- Source/Core/Core/IOS/ES/TitleManagement.cpp | 26 ++++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Source/Core/Core/IOS/ES/ES.h b/Source/Core/Core/IOS/ES/ES.h index 7255c79107..b54d3eeddb 100644 --- a/Source/Core/Core/IOS/ES/ES.h +++ b/Source/Core/Core/IOS/ES/ES.h @@ -126,7 +126,8 @@ public: ReturnCode ImportTicket(const std::vector& ticket_bytes, const std::vector& cert_chain, TicketImportType type = TicketImportType::PossiblyPersonalised, VerifySignature verify_signature = VerifySignature::Yes); - ReturnCode ImportTmd(Context& context, const std::vector& tmd_bytes); + ReturnCode ImportTmd(Context& context, const std::vector& tmd_bytes, u64 caller_title_id, + u32 caller_title_flags); ReturnCode ImportTitleInit(Context& context, const std::vector& tmd_bytes, const std::vector& cert_chain, VerifySignature verify_signature = VerifySignature::Yes); @@ -135,7 +136,8 @@ public: ReturnCode ImportContentEnd(Context& context, u32 content_fd); ReturnCode ImportTitleDone(Context& context); ReturnCode ImportTitleCancel(Context& context); - ReturnCode ExportTitleInit(Context& context, u64 title_id, u8* tmd, u32 tmd_size); + ReturnCode ExportTitleInit(Context& context, u64 title_id, u8* tmd, u32 tmd_size, + u64 caller_title_id, u32 caller_title_flags); ReturnCode ExportContentBegin(Context& context, u64 title_id, u32 content_id); ReturnCode ExportContentData(Context& context, u32 content_fd, u8* data, u32 data_size); ReturnCode ExportContentEnd(Context& context, u32 content_fd); diff --git a/Source/Core/Core/IOS/ES/TitleManagement.cpp b/Source/Core/Core/IOS/ES/TitleManagement.cpp index 8669491f48..48a515c70b 100644 --- a/Source/Core/Core/IOS/ES/TitleManagement.cpp +++ b/Source/Core/Core/IOS/ES/TitleManagement.cpp @@ -107,15 +107,14 @@ IPCCommandResult ES::ImportTicket(const IOCtlVRequest& request) constexpr std::array NULL_KEY{}; // Used for exporting titles and importing them back (ImportTmd and ExportTitleInit). -static ReturnCode InitBackupKey(const IOS::ES::TMDReader& tmd, IOSC& iosc, IOSC::Handle* key) +static ReturnCode InitBackupKey(u64 tid, u32 title_flags, IOSC& iosc, IOSC::Handle* key) { // Some versions of IOS have a bug that causes it to use a zeroed key instead of the PRNG key. // When Nintendo decided to fix it, they added checks to keep using the zeroed key only in // affected titles to avoid making existing exports useless. // Ignore the region byte. - const u64 title_id = tmd.GetTitleId() | 0xff; - const u32 title_flags = tmd.GetTitleFlags(); + const u64 title_id = tid | 0xff; const u32 affected_type = IOS::ES::TITLE_TYPE_0x10 | IOS::ES::TITLE_TYPE_DATA; if (title_id == Titles::SYSTEM_MENU || (title_flags & affected_type) != affected_type || !(title_id == 0x00010005735841ff || title_id - 0x00010005735a41ff <= 0x700)) @@ -136,7 +135,8 @@ static void ResetTitleImportContext(ES::Context* context, IOSC& iosc) context->title_import_export = {}; } -ReturnCode ES::ImportTmd(Context& context, const std::vector& tmd_bytes) +ReturnCode ES::ImportTmd(Context& context, const std::vector& tmd_bytes, u64 caller_title_id, + u32 caller_title_flags) { INFO_LOG_FMT(IOS_ES, "ImportTmd"); @@ -166,8 +166,8 @@ ReturnCode ES::ImportTmd(Context& context, const std::vector& tmd_bytes) return ES_EIO; } - ret = - InitBackupKey(m_title_context.tmd, m_ios.GetIOSC(), &context.title_import_export.key_handle); + ret = InitBackupKey(caller_title_id, caller_title_flags, m_ios.GetIOSC(), + &context.title_import_export.key_handle); if (ret != IPC_SUCCESS) { ERROR_LOG_FMT(IOS_ES, "ImportTmd: InitBackupKey failed with error {}", ret); @@ -189,7 +189,8 @@ IPCCommandResult ES::ImportTmd(Context& context, const IOCtlVRequest& request) std::vector tmd(request.in_vectors[0].size); Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size); - return GetDefaultReply(ImportTmd(context, tmd)); + return GetDefaultReply(ImportTmd(context, tmd, m_title_context.tmd.GetTitleId(), + m_title_context.tmd.GetTitleFlags())); } static ReturnCode InitTitleImportKey(const std::vector& ticket_bytes, IOSC& iosc, @@ -651,7 +652,8 @@ IPCCommandResult ES::DeleteContent(const IOCtlVRequest& request) Memory::Read_U32(request.in_vectors[1].address))); } -ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u32 tmd_size) +ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u32 tmd_size, + u64 caller_title_id, u32 caller_title_flags) { // No concurrent title import/export is allowed. if (context.title_import_export.valid) @@ -664,8 +666,8 @@ ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u3 ResetTitleImportContext(&context, m_ios.GetIOSC()); context.title_import_export.tmd = tmd; - const ReturnCode ret = - InitBackupKey(m_title_context.tmd, m_ios.GetIOSC(), &context.title_import_export.key_handle); + const ReturnCode ret = InitBackupKey(caller_title_id, caller_title_flags, m_ios.GetIOSC(), + &context.title_import_export.key_handle); if (ret != IPC_SUCCESS) return ret; @@ -688,7 +690,9 @@ IPCCommandResult ES::ExportTitleInit(Context& context, const IOCtlVRequest& requ u8* tmd_bytes = Memory::GetPointer(request.io_vectors[0].address); const u32 tmd_size = request.io_vectors[0].size; - return GetDefaultReply(ExportTitleInit(context, title_id, tmd_bytes, tmd_size)); + return GetDefaultReply(ExportTitleInit(context, title_id, tmd_bytes, tmd_size, + m_title_context.tmd.GetTitleId(), + m_title_context.tmd.GetTitleFlags())); } ReturnCode ES::ExportContentBegin(Context& context, u64 title_id, u32 content_id) From 46e4c17db3f43be968a9ce9ff4f4e86a22a7a872 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 31 Dec 2020 18:51:34 +0100 Subject: [PATCH 2/5] WiiUtils: Add utility functions to handle prep-work for importing 'SD-card export' style Wii saves. --- Source/Core/Core/WiiUtils.cpp | 62 +++++++++++++++++++++++++++++++++++ Source/Core/Core/WiiUtils.h | 24 ++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/Source/Core/Core/WiiUtils.cpp b/Source/Core/Core/WiiUtils.cpp index 0abc8de162..07db5c7210 100644 --- a/Source/Core/Core/WiiUtils.cpp +++ b/Source/Core/Core/WiiUtils.cpp @@ -19,6 +19,7 @@ #include #include +#include "Common/Align.h" #include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/FileUtil.h" @@ -223,6 +224,67 @@ bool IsTitleInstalled(u64 title_id) [](const std::string& file) { return file != "title.tmd"; }); } +bool IsTMDImported(IOS::HLE::FS::FileSystem& fs, u64 title_id) +{ + const auto entries = fs.ReadDirectory(0, 0, Common::GetTitleContentPath(title_id)); + return entries && std::any_of(entries->begin(), entries->end(), + [](const std::string& file) { return file == "title.tmd"; }); +} + +IOS::ES::TMDReader FindBackupTMD(IOS::HLE::FS::FileSystem& fs, u64 title_id) +{ + auto file = fs.OpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL, + "/title/00000001/00000002/data/tmds.sys", IOS::HLE::FS::Mode::Read); + if (!file) + return {}; + + // structure of this file is as follows: + // - 32 bytes descriptor of a TMD, which contains a title ID and a length + // - the TMD, with padding aligning to 32 bytes + // - repeat for as many TMDs as stored + while (true) + { + std::array descriptor; + if (!file->Read(descriptor.data(), descriptor.size())) + return {}; + + const u64 tid = Common::swap64(descriptor.data()); + const u32 tmd_length = Common::swap32(descriptor.data() + 8); + if (tid == title_id) + { + // found the right TMD + std::vector tmd_bytes(tmd_length); + if (!file->Read(tmd_bytes.data(), tmd_length)) + return {}; + return IOS::ES::TMDReader(std::move(tmd_bytes)); + } + + // not the right TMD, skip this one and go to the next + if (!file->Seek(Common::AlignUp(tmd_length, 32), IOS::HLE::FS::SeekMode::Current)) + return {}; + } +} + +bool EnsureTMDIsImported(IOS::HLE::FS::FileSystem& fs, IOS::HLE::Device::ES& es, u64 title_id) +{ + if (IsTMDImported(fs, title_id)) + return true; + + auto tmd = FindBackupTMD(fs, title_id); + if (!tmd.IsValid()) + return false; + + IOS::HLE::Device::ES::Context context; + context.uid = IOS::SYSMENU_UID; + context.gid = IOS::SYSMENU_GID; + const auto import_result = + es.ImportTmd(context, tmd.GetBytes(), Titles::SYSTEM_MENU, IOS::ES::TITLE_TYPE_DEFAULT); + if (import_result != IOS::HLE::IPC_SUCCESS) + return false; + + return es.ImportTitleDone(context) == IOS::HLE::IPC_SUCCESS; +} + // Common functionality for system updaters. class SystemUpdater { diff --git a/Source/Core/Core/WiiUtils.h b/Source/Core/Core/WiiUtils.h index 3936dfac8a..1c496b92ee 100644 --- a/Source/Core/Core/WiiUtils.h +++ b/Source/Core/Core/WiiUtils.h @@ -6,10 +6,12 @@ #include #include +#include #include #include #include "Common/CommonTypes.h" +#include "Core/IOS/ES/Formats.h" // Small utility functions for common Wii related tasks. @@ -23,6 +25,16 @@ namespace IOS::HLE class Kernel; } +namespace IOS::HLE::FS +{ +class FileSystem; +} + +namespace IOS::HLE::Device +{ +class ES; +} + namespace WiiUtils { enum class InstallType @@ -40,6 +52,18 @@ bool UninstallTitle(u64 title_id); bool IsTitleInstalled(u64 title_id); +// Checks if there's a title.tmd imported for the given title ID. +bool IsTMDImported(IOS::HLE::FS::FileSystem& fs, u64 title_id); + +// Searches for a TMD matching the given title ID in /title/00000001/00000002/data/tmds.sys. +// Returns it if it exists, otherwise returns an empty invalid TMD. +IOS::ES::TMDReader FindBackupTMD(IOS::HLE::FS::FileSystem& fs, u64 title_id); + +// Checks if there's a title.tmd imported for the given title ID. If there is not, we attempt to +// re-import it from the TMDs stored in /title/00000001/00000002/data/tmds.sys. +// Returns true if, after this function call, we have an imported title.tmd, or false if not. +bool EnsureTMDIsImported(IOS::HLE::FS::FileSystem& fs, IOS::HLE::Device::ES& es, u64 title_id); + enum class UpdateResult { Succeeded, From 700d53e00f285b6471734563c8564c01cf5381f4 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 31 Dec 2020 19:33:48 +0100 Subject: [PATCH 3/5] WiiSave: In Import(), make sure the TMD exists or can be reinstalled before allowing save to be imported. --- Source/Core/Core/HW/WiiSave.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Core/Core/HW/WiiSave.cpp b/Source/Core/Core/HW/WiiSave.cpp index 7552686a6a..dd4108a59b 100644 --- a/Source/Core/Core/HW/WiiSave.cpp +++ b/Source/Core/Core/HW/WiiSave.cpp @@ -40,6 +40,7 @@ #include "Core/IOS/IOS.h" #include "Core/IOS/IOSC.h" #include "Core/IOS/Uids.h" +#include "Core/WiiUtils.h" namespace WiiSave { @@ -475,6 +476,14 @@ bool Import(const std::string& data_bin_path, std::function can_overwrit ERROR_LOG_FMT(CORE, "WiiSave::Import: Failed to read header"); return false; } + + if (!WiiUtils::EnsureTMDIsImported(*ios.GetFS(), *ios.GetES(), header->tid)) + { + ERROR_LOG_FMT(CORE, "WiiSave::Import: Failed to find or import TMD for title {:16x}", + header->tid); + return false; + } + const auto nand = MakeNandStorage(ios.GetFS().get(), header->tid); if (nand->SaveExists() && !can_overwrite()) return false; From d9c686db3074360698b12d9beaf425b95a4348fd Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 31 Dec 2020 19:35:29 +0100 Subject: [PATCH 4/5] WiiSave: Delete existing save, if any, before importing one. --- Source/Core/Core/HW/WiiSave.cpp | 101 ++++++++++++++++++++++----- Source/Core/Core/HW/WiiSaveStructs.h | 3 +- 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/Source/Core/Core/HW/WiiSave.cpp b/Source/Core/Core/HW/WiiSave.cpp index dd4108a59b..98498a29cb 100644 --- a/Source/Core/Core/HW/WiiSave.cpp +++ b/Source/Core/Core/HW/WiiSave.cpp @@ -69,9 +69,35 @@ public: ScanForFiles(m_data_dir); } - bool SaveExists() override + bool SaveExists() const override { - return m_uid && m_gid && m_fs->GetMetadata(*m_uid, *m_gid, m_data_dir + "/banner.bin"); + return !m_files_list.empty() || + (m_uid && m_gid && m_fs->GetMetadata(*m_uid, *m_gid, m_data_dir + "/banner.bin")); + } + + bool EraseSave() override + { + // banner.bin is not in m_files_list, delete separately + const auto banner_delete_result = + m_fs->Delete(IOS::PID_KERNEL, IOS::PID_KERNEL, m_data_dir + "/banner.bin"); + if (banner_delete_result != FS::ResultCode::Success) + return false; + + for (const SaveFile& file : m_files_list) + { + // files in subdirs are deleted automatically when the subdir is deleted + if (file.path.find('/') != std::string::npos) + continue; + + const auto result = + m_fs->Delete(IOS::PID_KERNEL, IOS::PID_KERNEL, m_data_dir + "/" + file.path); + if (result != FS::ResultCode::Success) + return false; + } + + m_files_list.clear(); + m_files_size = 0; + return true; } std::optional
ReadHeader() override @@ -246,6 +272,10 @@ public: m_file = File::IOFile{path, mode}; } + bool SaveExists() const override { return m_file.GetSize() > 0; } + + bool EraseSave() override { return m_file.GetSize() == 0 || m_file.Resize(0); } + std::optional
ReadHeader() override { Header header; @@ -447,23 +477,60 @@ StoragePointer MakeDataBinStorage(IOS::HLE::IOSC* iosc, const std::string& path, return StoragePointer{new DataBinStorage{iosc, path, mode}}; } -template -static bool Copy(std::string_view description, Storage* source, - std::optional (Storage::*read_fn)(), Storage* dest, - bool (Storage::*write_fn)(const T&)) -{ - const std::optional data = (source->*read_fn)(); - if (data && (dest->*write_fn)(*data)) - return true; - ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to {} {}", !data ? "read" : "write", description); - return false; -} - bool Copy(Storage* source, Storage* dest) { - return Copy("header", source, &Storage::ReadHeader, dest, &Storage::WriteHeader) && - Copy("bk header", source, &Storage::ReadBkHeader, dest, &Storage::WriteBkHeader) && - Copy("files", source, &Storage::ReadFiles, dest, &Storage::WriteFiles); + // first make sure we can read all the data from the source + const auto header = source->ReadHeader(); + if (!header) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read header"); + return false; + } + + const auto bk_header = source->ReadBkHeader(); + if (!bk_header) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read bk header"); + return false; + } + + const auto files = source->ReadFiles(); + if (!files) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read files"); + return false; + } + + // once we have confirmed we can read the source, erase corresponding save in the destination + if (dest->SaveExists()) + { + if (!dest->EraseSave()) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to erase existing save"); + return false; + } + } + + // and then write it to the destination + if (!dest->WriteHeader(*header)) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write header"); + return false; + } + + if (!dest->WriteBkHeader(*bk_header)) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write bk header"); + return false; + } + + if (!dest->WriteFiles(*files)) + { + ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write files"); + return false; + } + + return true; } bool Import(const std::string& data_bin_path, std::function can_overwrite) diff --git a/Source/Core/Core/HW/WiiSaveStructs.h b/Source/Core/Core/HW/WiiSaveStructs.h index 6ef5a38feb..ed6b9aa948 100644 --- a/Source/Core/Core/HW/WiiSaveStructs.h +++ b/Source/Core/Core/HW/WiiSaveStructs.h @@ -101,7 +101,8 @@ public: }; virtual ~Storage() = default; - virtual bool SaveExists() { return true; } + virtual bool SaveExists() const = 0; + virtual bool EraseSave() = 0; virtual std::optional
ReadHeader() = 0; virtual std::optional ReadBkHeader() = 0; virtual std::optional> ReadFiles() = 0; From 2932b5f8cdaad063ccc869dfd7ce58891471b3a2 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 31 Dec 2020 23:26:16 +0100 Subject: [PATCH 5/5] Qt: Give better error messages when Wii save importing fails. --- Source/Core/Core/HW/WiiSave.cpp | 32 ++++++++-------- Source/Core/Core/HW/WiiSave.h | 15 ++++++-- Source/Core/DolphinQt/GameList/GameList.cpp | 5 ++- Source/Core/DolphinQt/MenuBar.cpp | 42 +++++++++++++++------ 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/Source/Core/Core/HW/WiiSave.cpp b/Source/Core/Core/HW/WiiSave.cpp index 98498a29cb..2bc87b8dad 100644 --- a/Source/Core/Core/HW/WiiSave.cpp +++ b/Source/Core/Core/HW/WiiSave.cpp @@ -477,28 +477,28 @@ StoragePointer MakeDataBinStorage(IOS::HLE::IOSC* iosc, const std::string& path, return StoragePointer{new DataBinStorage{iosc, path, mode}}; } -bool Copy(Storage* source, Storage* dest) +CopyResult Copy(Storage* source, Storage* dest) { // first make sure we can read all the data from the source const auto header = source->ReadHeader(); if (!header) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read header"); - return false; + return CopyResult::CorruptedSource; } const auto bk_header = source->ReadBkHeader(); if (!bk_header) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read bk header"); - return false; + return CopyResult::CorruptedSource; } const auto files = source->ReadFiles(); if (!files) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to read files"); - return false; + return CopyResult::CorruptedSource; } // once we have confirmed we can read the source, erase corresponding save in the destination @@ -507,7 +507,7 @@ bool Copy(Storage* source, Storage* dest) if (!dest->EraseSave()) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to erase existing save"); - return false; + return CopyResult::Error; } } @@ -515,25 +515,25 @@ bool Copy(Storage* source, Storage* dest) if (!dest->WriteHeader(*header)) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write header"); - return false; + return CopyResult::Error; } if (!dest->WriteBkHeader(*bk_header)) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write bk header"); - return false; + return CopyResult::Error; } if (!dest->WriteFiles(*files)) { ERROR_LOG_FMT(CORE, "WiiSave::Copy: Failed to write files"); - return false; + return CopyResult::Error; } - return true; + return CopyResult::Success; } -bool Import(const std::string& data_bin_path, std::function can_overwrite) +CopyResult Import(const std::string& data_bin_path, std::function can_overwrite) { IOS::HLE::Kernel ios; const auto data_bin = MakeDataBinStorage(&ios.GetIOSC(), data_bin_path, "rb"); @@ -541,23 +541,23 @@ bool Import(const std::string& data_bin_path, std::function can_overwrit if (!header) { ERROR_LOG_FMT(CORE, "WiiSave::Import: Failed to read header"); - return false; + return CopyResult::CorruptedSource; } if (!WiiUtils::EnsureTMDIsImported(*ios.GetFS(), *ios.GetES(), header->tid)) { ERROR_LOG_FMT(CORE, "WiiSave::Import: Failed to find or import TMD for title {:16x}", header->tid); - return false; + return CopyResult::TitleMissing; } const auto nand = MakeNandStorage(ios.GetFS().get(), header->tid); if (nand->SaveExists() && !can_overwrite()) - return false; + return CopyResult::Cancelled; return Copy(data_bin.get(), nand.get()); } -static bool Export(u64 tid, std::string_view export_path, IOS::HLE::Kernel* ios) +static CopyResult Export(u64 tid, std::string_view export_path, IOS::HLE::Kernel* ios) { const std::string path = fmt::format("{}/private/wii/title/{}{}{}{}/data.bin", export_path, static_cast(tid >> 24), static_cast(tid >> 16), @@ -566,7 +566,7 @@ static bool Export(u64 tid, std::string_view export_path, IOS::HLE::Kernel* ios) MakeDataBinStorage(&ios->GetIOSC(), path, "w+b").get()); } -bool Export(u64 tid, std::string_view export_path) +CopyResult Export(u64 tid, std::string_view export_path) { IOS::HLE::Kernel ios; return Export(tid, export_path, &ios); @@ -578,7 +578,7 @@ size_t ExportAll(std::string_view export_path) size_t exported_save_count = 0; for (const u64 title : ios.GetES()->GetInstalledTitles()) { - if (Export(title, export_path, &ios)) + if (Export(title, export_path, &ios) == CopyResult::Success) ++exported_save_count; } return exported_save_count; diff --git a/Source/Core/Core/HW/WiiSave.h b/Source/Core/Core/HW/WiiSave.h index d989498a94..f7d414586a 100644 --- a/Source/Core/Core/HW/WiiSave.h +++ b/Source/Core/Core/HW/WiiSave.h @@ -32,12 +32,21 @@ using StoragePointer = std::unique_ptr; StoragePointer MakeNandStorage(IOS::HLE::FS::FileSystem* fs, u64 tid); StoragePointer MakeDataBinStorage(IOS::HLE::IOSC* iosc, const std::string& path, const char* mode); -bool Copy(Storage* source, Storage* destination); +enum class CopyResult +{ + Success, + Error, + Cancelled, + CorruptedSource, + TitleMissing, +}; + +CopyResult Copy(Storage* source, Storage* destination); /// Import a save into the NAND from a .bin file. -bool Import(const std::string& data_bin_path, std::function can_overwrite); +CopyResult Import(const std::string& data_bin_path, std::function can_overwrite); /// Export a save to a .bin file. -bool Export(u64 tid, std::string_view export_path); +CopyResult Export(u64 tid, std::string_view export_path); /// Export all saves that are in the NAND. Returns the number of exported saves. size_t ExportAll(std::string_view export_path); } // namespace WiiSave diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index e3056b4ef4..1b458bdb35 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -452,8 +452,11 @@ void GameList::ExportWiiSave() QList failed; for (const auto& game : GetSelectedGames()) { - if (!WiiSave::Export(game->GetTitleID(), export_dir.toStdString())) + if (WiiSave::Export(game->GetTitleID(), export_dir.toStdString()) != + WiiSave::CopyResult::Success) + { failed.push_back(game->GetName(UICommon::GameFile::Variant::LongAndPossiblyCustom)); + } } if (!failed.isEmpty()) diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 3869fcf8e5..b1f6d40169 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -1066,19 +1066,39 @@ void MenuBar::ImportWiiSave() if (file.isEmpty()) return; - bool cancelled = false; auto can_overwrite = [&] { - bool yes = ModalMessageBox::question( - this, tr("Save Import"), - tr("Save data for this title already exists in the NAND. Consider backing up " - "the current data before overwriting.\nOverwrite now?")) == QMessageBox::Yes; - cancelled = !yes; - return yes; + return ModalMessageBox::question( + this, tr("Save Import"), + tr("Save data for this title already exists in the NAND. Consider backing up " + "the current data before overwriting.\nOverwrite now?")) == QMessageBox::Yes; }; - if (WiiSave::Import(file.toStdString(), can_overwrite)) - ModalMessageBox::information(this, tr("Save Import"), tr("Successfully imported save files.")); - else if (!cancelled) - ModalMessageBox::critical(this, tr("Save Import"), tr("Failed to import save files.")); + + const auto result = WiiSave::Import(file.toStdString(), can_overwrite); + switch (result) + { + case WiiSave::CopyResult::Success: + ModalMessageBox::information(this, tr("Save Import"), tr("Successfully imported save file.")); + break; + case WiiSave::CopyResult::CorruptedSource: + ModalMessageBox::critical(this, tr("Save Import"), + tr("Failed to import save file. The given file appears to be " + "corrupted or is not a valid Wii save.")); + break; + case WiiSave::CopyResult::TitleMissing: + ModalMessageBox::critical( + this, tr("Save Import"), + tr("Failed to import save file. Please launch the game once, then try again.")); + break; + case WiiSave::CopyResult::Cancelled: + break; + default: + ModalMessageBox::critical( + this, tr("Save Import"), + tr("Failed to import save file. Your NAND may be corrupt, or something is preventing " + "access to files within it. Try repairing your NAND (Tools -> Manage NAND -> Check " + "NAND...), then import the save again.")); + break; + } } void MenuBar::ExportWiiSaves()