From 7abe1085e3834e26dfcda71ab4d46674f2c1f0b3 Mon Sep 17 00:00:00 2001
From: "Admiral H. Curtiss" <pikachu025@gmail.com>
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<u8>& ticket_bytes, const std::vector<u8>& cert_chain,
                           TicketImportType type = TicketImportType::PossiblyPersonalised,
                           VerifySignature verify_signature = VerifySignature::Yes);
-  ReturnCode ImportTmd(Context& context, const std::vector<u8>& tmd_bytes);
+  ReturnCode ImportTmd(Context& context, const std::vector<u8>& tmd_bytes, u64 caller_title_id,
+                       u32 caller_title_flags);
   ReturnCode ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes,
                              const std::vector<u8>& 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<u8, 16> 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<u8>& tmd_bytes)
+ReturnCode ES::ImportTmd(Context& context, const std::vector<u8>& 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<u8>& 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<u8> 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<u8>& 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" <pikachu025@gmail.com>
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 <fmt/format.h>
 #include <pugixml.hpp>
 
+#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<u8, 32> 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<u8> 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 <cstddef>
 #include <functional>
+#include <optional>
 #include <string>
 #include <unordered_set>
 
 #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" <pikachu025@gmail.com>
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<bool()> 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" <pikachu025@gmail.com>
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<Header> 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<Header> 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 <typename T>
-static bool Copy(std::string_view description, Storage* source,
-                 std::optional<T> (Storage::*read_fn)(), Storage* dest,
-                 bool (Storage::*write_fn)(const T&))
-{
-  const std::optional<T> 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<bool()> 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<Header> ReadHeader() = 0;
   virtual std::optional<BkHeader> ReadBkHeader() = 0;
   virtual std::optional<std::vector<SaveFile>> ReadFiles() = 0;

From 2932b5f8cdaad063ccc869dfd7ce58891471b3a2 Mon Sep 17 00:00:00 2001
From: "Admiral H. Curtiss" <pikachu025@gmail.com>
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<bool()> can_overwrite)
+CopyResult Import(const std::string& data_bin_path, std::function<bool()> 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<bool()> 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<char>(tid >> 24), static_cast<char>(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<Storage, StorageDeleter>;
 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<bool()> can_overwrite);
+CopyResult Import(const std::string& data_bin_path, std::function<bool()> 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<std::string> 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()