summaryrefslogtreecommitdiff
path: root/Source/Core/Common/FatFsUtil.cpp
diff options
context:
space:
mode:
authorJMC47 <JMC4789@gmail.com>2022-07-11 17:38:34 -0400
committerGitHub <noreply@github.com>2022-07-11 17:38:34 -0400
commitaa294337547c067452954c8d1dd002130a23cb14 (patch)
tree15a14959e925e4fb88b9194ba7a68ff7d24af912 /Source/Core/Common/FatFsUtil.cpp
parent322f19e9e9b558592d83aa25b015d4d9f73687d8 (diff)
parent2d8d3358c4b86032263afe0d5d9b27c53e097cd2 (diff)
Merge pull request #10590 from AdmiralCurtiss/sdcard-folder-sync
Wii SD Card Folder Syncing
Diffstat (limited to 'Source/Core/Common/FatFsUtil.cpp')
-rw-r--r--Source/Core/Common/FatFsUtil.cpp743
1 files changed, 743 insertions, 0 deletions
diff --git a/Source/Core/Common/FatFsUtil.cpp b/Source/Core/Common/FatFsUtil.cpp
new file mode 100644
index 0000000000..f247ae3f73
--- /dev/null
+++ b/Source/Core/Common/FatFsUtil.cpp
@@ -0,0 +1,743 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Common/FatFsUtil.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdlib>
+#include <mutex>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+
+// Does not compile if diskio.h is included first.
+// clang-format off
+#include "ff.h"
+#include "diskio.h"
+// clang-format on
+
+#include "Common/Align.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
+#include "Common/Logging/Log.h"
+#include "Common/ScopeGuard.h"
+#include "Common/StringUtil.h"
+
+enum : u32
+{
+ SECTOR_SIZE = 512,
+ MAX_CLUSTER_SIZE = 64 * SECTOR_SIZE,
+};
+
+static std::mutex s_fatfs_mutex;
+static File::IOFile* s_image;
+static bool s_deterministic;
+
+extern "C" DSTATUS disk_status(BYTE pdrv)
+{
+ return 0;
+}
+
+extern "C" DSTATUS disk_initialize(BYTE pdrv)
+{
+ return 0;
+}
+
+extern "C" DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count)
+{
+ const u64 offset = static_cast<u64>(sector) * SECTOR_SIZE;
+ if (!s_image->Seek(offset, File::SeekOrigin::Begin))
+ {
+ ERROR_LOG_FMT(COMMON, "SD image seek failed (offset={})", offset);
+ return RES_ERROR;
+ }
+
+ const size_t size = static_cast<size_t>(count) * SECTOR_SIZE;
+ if (!s_image->ReadBytes(buff, size))
+ {
+ ERROR_LOG_FMT(COMMON, "SD image read failed (offset={}, size={})", offset, size);
+ return RES_ERROR;
+ }
+
+ return RES_OK;
+}
+
+extern "C" DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count)
+{
+ const u64 offset = static_cast<u64>(sector) * SECTOR_SIZE;
+ if (!s_image->Seek(offset, File::SeekOrigin::Begin))
+ {
+ ERROR_LOG_FMT(COMMON, "SD image seek failed (offset={})", offset);
+ return RES_ERROR;
+ }
+
+ const size_t size = static_cast<size_t>(count) * SECTOR_SIZE;
+ if (!s_image->WriteBytes(buff, size))
+ {
+ ERROR_LOG_FMT(COMMON, "SD image write failed (offset={}, size={})", offset, size);
+ return RES_ERROR;
+ }
+
+ return RES_OK;
+}
+
+extern "C" DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff)
+{
+ switch (cmd)
+ {
+ case CTRL_SYNC:
+ return RES_OK;
+ case GET_SECTOR_COUNT:
+ *reinterpret_cast<LBA_t*>(buff) = s_image->GetSize() / SECTOR_SIZE;
+ return RES_OK;
+ default:
+ WARN_LOG_FMT(COMMON, "Unexpected SD image ioctl {}", cmd);
+ return RES_OK;
+ }
+}
+
+extern "C" DWORD get_fattime(void)
+{
+ if (s_deterministic)
+ return 0;
+
+ const std::time_t time = std::time(nullptr);
+ std::tm tm;
+#ifdef _WIN32
+ localtime_s(&tm, &time);
+#else
+ localtime_r(&time, &tm);
+#endif
+
+ DWORD fattime = 0;
+ fattime |= (tm.tm_year - 80) << 25;
+ fattime |= (tm.tm_mon + 1) << 21;
+ fattime |= tm.tm_mday << 16;
+ fattime |= tm.tm_hour << 11;
+ fattime |= tm.tm_min << 5;
+ fattime |= std::min(tm.tm_sec, 59) >> 1;
+ return fattime;
+}
+
+extern "C" void* ff_memalloc(UINT msize)
+{
+ return std::malloc(msize);
+}
+
+extern "C" void ff_memfree(void* mblock)
+{
+ return std::free(mblock);
+}
+
+extern "C" int ff_cre_syncobj(BYTE vol, FF_SYNC_t* sobj)
+{
+ *sobj = new std::recursive_mutex();
+ return *sobj != nullptr;
+}
+
+extern "C" int ff_req_grant(FF_SYNC_t sobj)
+{
+ std::recursive_mutex* m = reinterpret_cast<std::recursive_mutex*>(sobj);
+ m->lock();
+ return 1;
+}
+
+extern "C" void ff_rel_grant(FF_SYNC_t sobj)
+{
+ std::recursive_mutex* m = reinterpret_cast<std::recursive_mutex*>(sobj);
+ m->unlock();
+}
+
+extern "C" int ff_del_syncobj(FF_SYNC_t sobj)
+{
+ delete reinterpret_cast<std::recursive_mutex*>(sobj);
+ return 1;
+}
+
+static const char* FatFsErrorToString(FRESULT error_code)
+{
+ // These are taken from the comment next to each value in ff.h
+ switch (error_code)
+ {
+ case FR_OK:
+ return "Succeeded";
+ case FR_DISK_ERR:
+ return "A hard error occurred in the low level disk I/O layer";
+ case FR_INT_ERR:
+ return "Assertion failed";
+ case FR_NOT_READY:
+ return "The physical drive cannot work";
+ case FR_NO_FILE:
+ return "Could not find the file";
+ case FR_NO_PATH:
+ return "Could not find the path";
+ case FR_INVALID_NAME:
+ return "The path name format is invalid";
+ case FR_DENIED:
+ return "Access denied due to prohibited access or directory full";
+ case FR_EXIST:
+ return "Access denied due to prohibited access";
+ case FR_INVALID_OBJECT:
+ return "The file/directory object is invalid";
+ case FR_WRITE_PROTECTED:
+ return "The physical drive is write protected";
+ case FR_INVALID_DRIVE:
+ return "The logical drive number is invalid";
+ case FR_NOT_ENABLED:
+ return "The volume has no work area";
+ case FR_NO_FILESYSTEM:
+ return "There is no valid FAT volume";
+ case FR_MKFS_ABORTED:
+ return "The f_mkfs() aborted due to any problem";
+ case FR_TIMEOUT:
+ return "Could not get a grant to access the volume within defined period";
+ case FR_LOCKED:
+ return "The operation is rejected according to the file sharing policy";
+ case FR_NOT_ENOUGH_CORE:
+ return "LFN working buffer could not be allocated";
+ case FR_TOO_MANY_OPEN_FILES:
+ return "Number of open files > FF_FS_LOCK";
+ case FR_INVALID_PARAMETER:
+ return "Given parameter is invalid";
+ default:
+ return "Unknown error";
+ }
+}
+
+namespace Common
+{
+static constexpr u64 MebibytesToBytes(u64 mebibytes)
+{
+ return mebibytes * 1024 * 1024;
+}
+
+static constexpr u64 GibibytesToBytes(u64 gibibytes)
+{
+ return gibibytes * 1024 * 1024 * 1024;
+}
+
+static bool CheckIfFATCompatible(const File::FSTEntry& entry)
+{
+ if (!entry.isDirectory)
+ return true;
+
+ if (entry.children.size() > 65536)
+ {
+ ERROR_LOG_FMT(COMMON, "Directory {} has too many entries ({})", entry.physicalName,
+ entry.children.size());
+ return false;
+ }
+
+ for (const File::FSTEntry& child : entry.children)
+ {
+ const size_t size = UTF8ToUTF16(child.virtualName).size();
+ if (size > 255)
+ {
+ ERROR_LOG_FMT(COMMON, "Filename {0} (in directory {1}) is too long ({2})", child.virtualName,
+ entry.physicalName, size);
+ return false;
+ }
+
+ if (child.size >= GibibytesToBytes(4))
+ {
+ ERROR_LOG_FMT(COMMON, "File {0} (in directory {1}) is too large ({2})", child.virtualName,
+ entry.physicalName, child.size);
+ return false;
+ }
+
+ if (!CheckIfFATCompatible(child))
+ return false;
+ }
+
+ return true;
+}
+
+static u64 GetSize(const File::FSTEntry& entry)
+{
+ if (!entry.isDirectory)
+ return AlignUp(entry.size, MAX_CLUSTER_SIZE);
+
+ u64 size = 0;
+ for (const File::FSTEntry& child : entry.children)
+ {
+ size += 32;
+ // For simplicity, assume that all names are LFN.
+ const u64 num_lfn_entries = (UTF8ToUTF16(child.virtualName).size() + 13 - 1) / 13;
+ size += num_lfn_entries * 32;
+ }
+ size = AlignUp(size, MAX_CLUSTER_SIZE);
+
+ for (const File::FSTEntry& child : entry.children)
+ size += GetSize(child);
+
+ return size;
+}
+
+static bool Pack(const File::FSTEntry& entry, bool is_root, std::vector<u8>& tmp_buffer)
+{
+ if (!entry.isDirectory)
+ {
+ File::IOFile src(entry.physicalName, "rb");
+ if (!src)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open file {}", entry.physicalName);
+ return false;
+ }
+
+ FIL dst;
+ const auto open_error_code =
+ f_open(&dst, entry.virtualName.c_str(), FA_CREATE_ALWAYS | FA_WRITE);
+ if (open_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open file {} in SD image: {}", entry.physicalName,
+ FatFsErrorToString(open_error_code));
+ return false;
+ }
+
+ const size_t src_size = src.GetSize();
+ if (src.GetSize() != entry.size)
+ {
+ ERROR_LOG_FMT(COMMON, "File at {} does not match previously read filesize ({} != {})",
+ entry.physicalName, entry.size, src_size);
+ return false;
+ }
+
+ if (entry.size >= GibibytesToBytes(4))
+ {
+ ERROR_LOG_FMT(COMMON, "File at {} is too large to fit into FAT ({} >= 4GiB)",
+ entry.physicalName, entry.size);
+ return false;
+ }
+
+ u64 size = entry.size;
+ while (size > 0)
+ {
+ u32 chunk_size = static_cast<u32>(std::min(size, static_cast<u64>(tmp_buffer.size())));
+ if (!src.ReadBytes(tmp_buffer.data(), chunk_size))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to read data from file at {}", entry.physicalName);
+ return false;
+ }
+
+ u32 written_size;
+ const auto write_error_code = f_write(&dst, tmp_buffer.data(), chunk_size, &written_size);
+ if (write_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to write file {} to SD image: {}", entry.physicalName,
+ FatFsErrorToString(write_error_code));
+ return false;
+ }
+
+ if (written_size != chunk_size)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to write bytes of file {} to SD image ({} != {})",
+ entry.physicalName, written_size, chunk_size);
+ return false;
+ }
+
+ size -= chunk_size;
+ }
+
+ const auto close_error_code = f_close(&dst);
+ if (close_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close file {} in SD image: {}", entry.physicalName,
+ FatFsErrorToString(close_error_code));
+ return false;
+ }
+
+ if (!src.Close())
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close file {}", entry.physicalName);
+ return false;
+ }
+
+ return true;
+ }
+
+ if (!is_root)
+ {
+ const auto mkdir_error_code = f_mkdir(entry.virtualName.c_str());
+ if (mkdir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to make directory {} in SD image: {}", entry.physicalName,
+ FatFsErrorToString(mkdir_error_code));
+ return false;
+ }
+
+ const auto chdir_error_code = f_chdir(entry.virtualName.c_str());
+ if (chdir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to entry directory {} in SD image: {}", entry.physicalName,
+ FatFsErrorToString(chdir_error_code));
+ return false;
+ }
+ }
+
+ for (const File::FSTEntry& child : entry.children)
+ {
+ if (!Pack(child, false, tmp_buffer))
+ return false;
+ }
+
+ if (!is_root)
+ {
+ const auto chdir_up_error_code = f_chdir("..");
+ if (chdir_up_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to leave directory {} in SD image: {}", entry.physicalName,
+ FatFsErrorToString(chdir_up_error_code));
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void SortFST(File::FSTEntry* root)
+{
+ std::sort(root->children.begin(), root->children.end(),
+ [](const File::FSTEntry& lhs, const File::FSTEntry& rhs) {
+ return lhs.virtualName < rhs.virtualName;
+ });
+ for (auto& child : root->children)
+ SortFST(&child);
+}
+
+bool SyncSDFolderToSDImage(bool deterministic)
+{
+ const std::string source_dir = File::GetUserPath(D_WIISDCARDSYNCFOLDER_IDX);
+ const std::string image_path = File::GetUserPath(F_WIISDCARDIMAGE_IDX);
+ if (source_dir.empty() || image_path.empty())
+ return false;
+
+ INFO_LOG_FMT(COMMON, "Starting SD card conversion from folder {} to file {}", source_dir,
+ image_path);
+
+ if (!File::IsDirectory(source_dir))
+ {
+ ERROR_LOG_FMT(COMMON, "{} is not a directory, not converting", source_dir);
+ return false;
+ }
+
+ File::FSTEntry root = File::ScanDirectoryTree(source_dir, true);
+ if (deterministic)
+ SortFST(&root);
+ if (!CheckIfFATCompatible(root))
+ return false;
+
+ u64 size = GetSize(root);
+ // Allocate a reasonable amount of free space
+ size += std::clamp(size / 2, MebibytesToBytes(512), GibibytesToBytes(8));
+ size = AlignUp(size, MAX_CLUSTER_SIZE);
+
+ std::lock_guard lk(s_fatfs_mutex);
+
+ File::IOFile image;
+ s_image = &image;
+ Common::ScopeGuard image_guard{[] { s_image = nullptr; }};
+ s_deterministic = deterministic;
+
+ const std::string temp_image_path = File::GetTempFilenameForAtomicWrite(image_path);
+ if (!image.Open(temp_image_path, "w+b"))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to create or overwrite SD image at {}", image_path);
+ return false;
+ }
+
+ // delete temp file in failure case
+ Common::ScopeGuard image_delete_guard{[&] { File::Delete(temp_image_path); }};
+
+ if (!image.Resize(size))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to allocate {} bytes for SD image at {}", size, image_path);
+ return false;
+ }
+
+ MKFS_PARM options = {};
+ options.fmt = FM_FAT32;
+ options.n_fat = 0; // Number of FATs: automatic
+ options.align = 1; // Alignment of the data region (in sectors)
+ options.n_root = 0; // Number of root directory entries: automatic (and unused for FAT32)
+ options.au_size = 0; // Cluster size: automatic
+
+ std::vector<u8> tmp_buffer(MAX_CLUSTER_SIZE);
+ const auto mkfs_error_code =
+ f_mkfs("", &options, tmp_buffer.data(), static_cast<UINT>(tmp_buffer.size()));
+ if (mkfs_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to initialize SD image filesystem: {}",
+ FatFsErrorToString(mkfs_error_code));
+ return false;
+ }
+
+ FATFS fs;
+ const auto mount_error_code = f_mount(&fs, "", 0);
+ if (mount_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to mount SD image filesystem: {}",
+ FatFsErrorToString(mount_error_code));
+ return false;
+ }
+ Common::ScopeGuard unmount_guard{[] { f_unmount(""); }};
+
+ if (!Pack(root, true, tmp_buffer))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to pack folder {} to SD image at {}", source_dir,
+ temp_image_path);
+ return false;
+ }
+
+ unmount_guard.Exit(); // unmount before closing the image
+
+ if (!image.Close())
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close SD image at {}", temp_image_path);
+ return false;
+ }
+
+ if (!File::Rename(temp_image_path, image_path))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to rename SD image from {} to {}", temp_image_path, image_path);
+ return false;
+ }
+
+ image_delete_guard.Dismiss(); // no need to delete the temp file anymore after the rename
+
+ INFO_LOG_FMT(COMMON, "Successfully packed folder {} to SD image at {}", source_dir, image_path);
+ return true;
+}
+
+static bool Unpack(const std::string path, bool is_directory, const char* name,
+ std::vector<u8>& tmp_buffer)
+{
+ if (!is_directory)
+ {
+ FIL src;
+ const auto open_error_code = f_open(&src, name, FA_READ);
+ if (open_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open file {} in SD image: {}", path,
+ FatFsErrorToString(open_error_code));
+ return false;
+ }
+
+ File::IOFile dst(path, "wb");
+ if (!dst)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open file {}", path);
+ return false;
+ }
+
+ u32 size = f_size(&src);
+ while (size > 0)
+ {
+ u32 chunk_size = std::min(size, static_cast<u32>(tmp_buffer.size()));
+ u32 read_size;
+ const auto read_error_code = f_read(&src, tmp_buffer.data(), chunk_size, &read_size);
+ if (read_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to read from file {} in SD image: {}", path,
+ FatFsErrorToString(read_error_code));
+ return false;
+ }
+
+ if (read_size != chunk_size)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to read bytes of file {} in SD image ({} != {})", path,
+ read_size, chunk_size);
+ return false;
+ }
+
+ if (!dst.WriteBytes(tmp_buffer.data(), chunk_size))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to write to file {}", path);
+ return false;
+ }
+
+ size -= chunk_size;
+ }
+
+ if (!dst.Close())
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close file {}", path);
+ return false;
+ }
+
+ const auto close_error_code = f_close(&src);
+ if (close_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close file {} in SD image: {}", path,
+ FatFsErrorToString(close_error_code));
+ return false;
+ }
+
+ return true;
+ }
+
+ if (!File::CreateDir(path))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to create directory {}", path);
+ return false;
+ }
+
+ const auto chdir_error_code = f_chdir(name);
+ if (chdir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to enter directory {} in SD image: {}", path,
+ FatFsErrorToString(chdir_error_code));
+ return false;
+ }
+
+ DIR directory;
+ const auto opendir_error_code = f_opendir(&directory, ".");
+ if (opendir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open directory {} in SD image: {}", path,
+ FatFsErrorToString(opendir_error_code));
+ return false;
+ }
+
+ FILINFO entry;
+ while (true)
+ {
+ const auto readdir_error_code = f_readdir(&directory, &entry);
+ if (readdir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to read directory {} in SD image: {}", path,
+ FatFsErrorToString(readdir_error_code));
+ return false;
+ }
+
+ if (entry.fname[0] == '\0')
+ break;
+
+ if (entry.fname[0] == '?' && entry.fname[1] == '\0' && entry.altname[0] == '\0')
+ {
+ // FATFS indicates entries that have neither a short nor a long filename this way.
+ // These are likely corrupted file entries so just skip them.
+ continue;
+ }
+
+ const std::string_view childname = entry.fname;
+
+ // Check for path traversal attacks.
+ const bool is_path_traversal_attack =
+ (childname.find("\\") != std::string_view::npos) ||
+ (childname.find('/') != std::string_view::npos) ||
+ std::all_of(childname.begin(), childname.end(), [](char c) { return c == '.'; });
+ if (is_path_traversal_attack)
+ {
+ ERROR_LOG_FMT(
+ COMMON,
+ "Path traversal attack detected in directory {} in SD image, child filename is {}", path,
+ childname);
+ return false;
+ }
+
+ if (!Unpack(fmt::format("{}/{}", path, childname), entry.fattrib & AM_DIR, entry.fname,
+ tmp_buffer))
+ {
+ return false;
+ }
+ }
+
+ const auto closedir_error_code = f_closedir(&directory);
+ if (closedir_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to close directory {} in SD image: {}", path,
+ FatFsErrorToString(closedir_error_code));
+ return false;
+ }
+
+ const auto chdir_up_error_code = f_chdir("..");
+ if (chdir_up_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to leave directory {} in SD image: {}", path,
+ FatFsErrorToString(chdir_up_error_code));
+ return false;
+ }
+
+ return true;
+}
+
+bool SyncSDImageToSDFolder()
+{
+ const std::string image_path = File::GetUserPath(F_WIISDCARDIMAGE_IDX);
+ const std::string target_dir = File::GetUserPath(D_WIISDCARDSYNCFOLDER_IDX);
+ if (image_path.empty() || target_dir.empty())
+ return false;
+
+ std::lock_guard lk(s_fatfs_mutex);
+
+ INFO_LOG_FMT(COMMON, "Starting SD card conversion from file {} to folder {}", image_path,
+ target_dir);
+
+ File::IOFile image;
+ s_image = &image;
+ Common::ScopeGuard image_guard{[] { s_image = nullptr; }};
+
+ // this shouldn't matter since we're not modifying the SD image here, but initialize it to
+ // something consistent just in case
+ s_deterministic = true;
+
+ if (!image.Open(image_path, "r+b"))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to open SD image at {}", image_path);
+ return false;
+ }
+
+ FATFS fs;
+ const auto mount_error_code = f_mount(&fs, "", 0);
+ if (mount_error_code != FR_OK)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to mount SD image file system: {}",
+ FatFsErrorToString(mount_error_code));
+ return false;
+ }
+ Common::ScopeGuard unmount_guard{[] { f_unmount(""); }};
+
+ // Unpack() and GetTempFilenameForAtomicWrite() don't want the trailing separator.
+ const std::string target_dir_without_slash = target_dir.substr(0, target_dir.length() - 1);
+
+ // Most systems don't offer atomic directory renaming, so it's simpler to directly work on the
+ // actual one and rollback if needed.
+ const bool target_dir_exists = File::IsDirectory(target_dir);
+ const std::string backup_target_dir_without_slash =
+ File::GetTempFilenameForAtomicWrite(target_dir_without_slash);
+
+ if (target_dir_exists)
+ {
+ if (!File::Rename(target_dir_without_slash, backup_target_dir_without_slash))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to move old SD folder to {}", backup_target_dir_without_slash);
+ return false;
+ }
+ }
+
+ std::vector<u8> tmp_buffer(MAX_CLUSTER_SIZE);
+ if (!Unpack(target_dir_without_slash, true, "", tmp_buffer))
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to unpack SD image {} to {}", image_path, target_dir);
+ File::DeleteDirRecursively(target_dir_without_slash);
+ if (target_dir_exists)
+ File::Rename(backup_target_dir_without_slash, target_dir_without_slash);
+ return false;
+ }
+
+ unmount_guard.Exit(); // unmount before closing the image
+
+ if (target_dir_exists)
+ File::DeleteDirRecursively(backup_target_dir_without_slash);
+
+ // even if this fails the conversion has already succeeded, so we still return true
+ if (!image.Close())
+ ERROR_LOG_FMT(COMMON, "Failed to close SD image {}", image_path);
+
+ INFO_LOG_FMT(COMMON, "Successfully unpacked SD image {} to {}", image_path, target_dir);
+ return true;
+}
+} // namespace Common