diff --git a/src/common/file_util.h b/src/common/file_util.h index 0bdaeea..d698bc7 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,6 +15,7 @@ #include #include #include "common/common_types.h" +#include "common/logging/log.h" #ifdef _MSC_VER #include "common/string_util.h" #endif @@ -251,6 +252,25 @@ private: bool m_good = true; }; +template +bool WriteBytesToFile(const std::string& path, T* data, std::size_t length) { + if (!CreateFullPath(path)) { + LOG_ERROR(Core, "Could not create path {}", path); + return false; + } + + IOFile file(path, "wb"); + if (!file.IsOpen()) { + LOG_ERROR(Core, "Could not open file {}", path); + return false; + } + if (file.WriteBytes(data, length) != length) { + LOG_ERROR(Core, "Write data failed (file: {})", path); + return false; + } + return true; +} + } // namespace FileUtil // To deal with Windows being dumb at unicode: diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 41bda06..f1367fe 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -3,10 +3,11 @@ add_library(core STATIC data_container.h decryptor.cpp decryptor.h + extdata.cpp + extdata.h importer.cpp importer.h - inner_fat.cpp - inner_fat.h + inner_fat.hpp key/arithmetic128.cpp key/arithmetic128.h key/key.cpp @@ -26,6 +27,8 @@ add_library(core STATIC quick_decryptor.cpp quick_decryptor.h result_status.h + savegame.cpp + savegame.h title_db.cpp title_db.h ) diff --git a/src/core/extdata.cpp b/src/core/extdata.cpp new file mode 100644 index 0000000..08dddfb --- /dev/null +++ b/src/core/extdata.cpp @@ -0,0 +1,141 @@ +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/data_container.h" +#include "core/decryptor.h" +#include "core/extdata.h" + +namespace Core { + +Extdata::Extdata(std::string data_path_, const SDMCDecryptor& decryptor_) + : data_path(std::move(data_path_)), decryptor(&decryptor_) { + + if (data_path.back() != '/' && data_path.back() != '\\') { + data_path += '/'; + } + + use_decryptor = true; + is_good = Init(); +} + +Extdata::Extdata(std::string data_path_) : data_path(std::move(data_path_)) { + if (data_path.back() != '/' && data_path.back() != '\\') { + data_path += '/'; + } + + use_decryptor = false; + is_good = Init(); +} + +Extdata::~Extdata() = default; + +bool Extdata::CheckMagic() const { + if (header.fat_header.magic != MakeMagic('V', 'S', 'X', 'E') || + header.fat_header.version != 0x30000) { + + LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); + return false; + } + return true; +} + +bool Extdata::IsGood() const { + return is_good; +} + +bool Extdata::Extract(std::string path) const { + if (path.back() != '/' && path.back() != '\\') { + path += '/'; + } + + if (!ExtractDirectory(path, 1)) { + return false; + } + + // Write format info + const auto format_info = GetFormatInfo(); + return FileUtil::WriteBytesToFile(path + "metadata", &format_info, sizeof(format_info)); +} + +std::vector Extdata::ReadFile(const std::string& path) const { + if (use_decryptor) { + return decryptor->DecryptFile(path); + } else { + FileUtil::IOFile file(path, "rb"); + return file.GetData(); + } +} + +bool Extdata::Init() { + // Read VSXE file + auto vsxe_raw = ReadFile(data_path + "00000000/00000001"); + if (vsxe_raw.empty()) { + LOG_ERROR(Core, "Failed to load or decrypt VSXE"); + return false; + } + + DataContainer vsxe_container(std::move(vsxe_raw)); + if (!vsxe_container.IsGood()) { + return false; + } + + std::vector> data; + if (!vsxe_container.GetIVFCLevel4Data(data)) { + return false; + } + + return Archive::Init(std::move(data)); +} + +bool Extdata::ExtractFile(const std::string& path, std::size_t index) const { + /// Maximum amount of device files a device directory can hold. + constexpr u32 DeviceDirCapacity = 126; + + FileUtil::IOFile file(path, "wb"); + if (!file) { + LOG_ERROR(Core, "Could not open file {}", path); + return false; + } + + u32 file_index = index + 1; + u32 sub_directory_id = file_index / DeviceDirCapacity; + u32 sub_file_id = file_index % DeviceDirCapacity; + std::string device_file_path = + fmt::format("{}{:08x}/{:08x}", data_path, sub_directory_id, sub_file_id); + + auto container_data = ReadFile(device_file_path); + if (container_data.empty()) { // File does not exist? + LOG_WARNING(Core, "Ignoring file {}", device_file_path); + return true; + } + + DataContainer container(std::move(container_data)); + if (!container.IsGood()) { + return false; + } + + std::vector> data; + if (!container.GetIVFCLevel4Data(data)) { + return false; + } + + if (file.WriteBytes(data[0].data(), data[0].size()) != data[0].size()) { + LOG_ERROR(Core, "Write data failed (file: {})", path); + return false; + } + + return true; +} + +ArchiveFormatInfo Extdata::GetFormatInfo() const { + // This information is based on how Citra created the metadata in FS + ArchiveFormatInfo format_info = {/* total_size */ 0, + /* number_directories */ fs_info.maximum_directory_count, + /* number_files */ fs_info.maximum_file_count, + /* duplicate_data */ false}; + + return format_info; +} + +} // namespace Core diff --git a/src/core/extdata.h b/src/core/extdata.h new file mode 100644 index 0000000..573a01f --- /dev/null +++ b/src/core/extdata.h @@ -0,0 +1,49 @@ +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/inner_fat.hpp" + +namespace Core { + +class SDMCDecryptor; + +class Extdata final : public Archive { +public: + /** + * Loads an SD extdata folder. + * @param data_path Path to the ENCRYPTED SD extdata folder, relative to decryptor root + * @param decryptor Const reference to the SDMCDecryptor. + */ + explicit Extdata(std::string data_path, const SDMCDecryptor& decryptor); + + /** + * Loads an Extdata folder without encryption. + * @param data_path Path to the DECRYPTED extdata folder + */ + explicit Extdata(std::string data_path); + + ~Extdata(); + + bool IsGood() const; + bool Extract(std::string path) const; + +private: + bool Init(); + bool CheckMagic() const; + std::vector ReadFile(const std::string& path) const; + bool ExtractFile(const std::string& path, std::size_t index) const; + ArchiveFormatInfo GetFormatInfo() const; + + bool is_good = false; + std::string data_path; + const SDMCDecryptor* decryptor = nullptr; + bool use_decryptor = true; + + friend class Archive; + friend class InnerFAT; +}; + +} // namespace Core diff --git a/src/core/importer.cpp b/src/core/importer.cpp index e075225..28e8f77 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -9,14 +9,15 @@ #include "common/string_util.h" #include "core/data_container.h" #include "core/decryptor.h" +#include "core/extdata.h" #include "core/importer.h" -#include "core/inner_fat.h" #include "core/key/key.h" #include "core/ncch/cia_builder.h" #include "core/ncch/ncch_container.h" #include "core/ncch/seed_db.h" #include "core/ncch/smdh.h" #include "core/ncch/title_metadata.h" +#include "core/savegame.h" #include "core/title_db.h" namespace Core { @@ -209,7 +210,7 @@ bool SDMCImporter::ImportSavegame(u64 id, return false; } - SDSavegame save(std::move(container_data)); + Savegame save(std::move(container_data)); if (!save.IsGood()) { return false; } @@ -236,7 +237,7 @@ bool SDMCImporter::ImportNandSavegame(u64 id, return false; } - SDSavegame save(std::move(container_data)); + Savegame save(std::move(container_data)); if (!save.IsGood()) { return false; } @@ -249,7 +250,7 @@ bool SDMCImporter::ImportNandSavegame(u64 id, bool SDMCImporter::ImportExtdata(u64 id, [[maybe_unused]] const Common::ProgressCallback& callback) { const auto path = fmt::format("extdata/{:08x}/{:08x}/", (id >> 32), (id & 0xFFFFFFFF)); - SDExtdata extdata("/" + path, *decryptor); + Extdata extdata("/" + path, *decryptor); if (!extdata.IsGood()) { return false; } @@ -262,7 +263,7 @@ bool SDMCImporter::ImportExtdata(u64 id, bool SDMCImporter::ImportNandExtdata(u64 id, [[maybe_unused]] const Common::ProgressCallback& callback) { const auto path = fmt::format("extdata/{:08x}/{:08x}/", (id >> 32), (id & 0xFFFFFFFF)); - SDExtdata extdata(config.nand_data_path + path); + Extdata extdata(config.nand_data_path + path); if (!extdata.IsGood()) { return false; } @@ -412,7 +413,7 @@ bool SDMCImporter::ImportSysdata(u64 id, return false; } - SDSavegame save(std::move(container_data)); + Savegame save(std::move(container_data)); if (!save.IsGood()) { return false; } diff --git a/src/core/inner_fat.cpp b/src/core/inner_fat.cpp deleted file mode 100644 index 4ce148e..0000000 --- a/src/core/inner_fat.cpp +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "common/assert.h" -#include "common/common_funcs.h" -#include "common/file_util.h" -#include "core/data_container.h" -#include "core/decryptor.h" -#include "core/inner_fat.h" - -namespace Core { - -InnerFAT::~InnerFAT() = default; - -bool InnerFAT::IsGood() const { - return is_good; -} - -bool InnerFAT::ExtractDirectory(const std::string& path, std::size_t index) const { - if (index >= directory_entry_table.size()) { - LOG_ERROR(Core, "Index out of bound {}", index); - return false; - } - auto entry = directory_entry_table[index]; - - std::array name_data = {}; // Append a null terminator - std::memcpy(name_data.data(), entry.name.data(), entry.name.size()); - - std::string name = name_data.data(); - std::string new_path = name.empty() ? path : path + name + "/"; // Name is empty for root - - if (!FileUtil::CreateFullPath(new_path)) { - LOG_ERROR(Core, "Could not create path {}", new_path); - return false; - } - - // Files - u32 cur = entry.first_file_index; - while (cur != 0) { - if (!ExtractFile(new_path, cur)) - return false; - cur = file_entry_table[cur].next_sibling_index; - } - - // Subdirectories - cur = entry.first_subdirectory_index; - while (cur != 0) { - if (!ExtractDirectory(new_path, cur)) - return false; - cur = directory_entry_table[cur].next_sibling_index; - } - - return true; -} - -bool InnerFAT::WriteMetadata(const std::string& path) const { - if (!FileUtil::CreateFullPath(path)) { - LOG_ERROR(Core, "Could not create path {}", path); - return false; - } - - auto format_info = GetFormatInfo(); - - FileUtil::IOFile file(path, "wb"); - if (!file.IsOpen()) { - LOG_ERROR(Core, "Could not open file {}", path); - return false; - } - if (file.WriteBytes(&format_info, sizeof(format_info)) != sizeof(format_info)) { - LOG_ERROR(Core, "Write data failed (file: {})", path); - return false; - } - return true; -} - -SDSavegame::SDSavegame(std::vector> partitions) { - if (partitions.size() == 1) { - duplicate_data = true; - data = std::move(partitions[0]); - } else if (partitions.size() == 2) { - duplicate_data = false; - partitionA = std::move(partitions[0]); - partitionB = std::move(partitions[1]); - } else { - UNREACHABLE(); - } - is_good = Init(); -} - -SDSavegame::~SDSavegame() = default; - -bool SDSavegame::Init() { - const auto& header_vector = duplicate_data ? data : partitionA; - - // Read header - TRY(CheckedMemcpy(&header, header_vector, 0, sizeof(header)), - LOG_ERROR(Core, "File size is too small")); - - if (header.magic != MakeMagic('S', 'A', 'V', 'E') || header.version != 0x40000) { - LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); - return false; - } - - // Read filesystem information - TRY(CheckedMemcpy(&fs_info, header_vector, header.filesystem_information_offset, - sizeof(fs_info)), - LOG_ERROR(Core, "File size is too small")); - - // Read data region - if (duplicate_data) { - data_region.resize(fs_info.data_region_block_count * - static_cast(fs_info.data_region_block_size)); - TRY(CheckedMemcpy(data_region.data(), data, fs_info.data_region_offset, data_region.size()), - LOG_ERROR(Core, "File size is too small")); - } else { - data_region = std::move(partitionB); - } - - // Directory & file entry tables are allocated in the data region as if they were normal - // files. However, only continuous allocation has been observed so far according to 3DBrew, - // so it should be safe to directly read the bytes. - - // Read directory entry table - directory_entry_table.resize(fs_info.maximum_directory_count + 2); // including head and root - - auto directory_entry_table_pos = - duplicate_data ? fs_info.data_region_offset + - fs_info.directory_entry_table.duplicate.block_index * - static_cast(fs_info.data_region_block_size) - : fs_info.directory_entry_table.non_duplicate; - - TRY(CheckedMemcpy(directory_entry_table.data(), header_vector, directory_entry_table_pos, - directory_entry_table.size() * sizeof(DirectoryEntryTableEntry)), - LOG_ERROR(Core, "File is too small")); - - // Read file entry table - file_entry_table.resize(fs_info.maximum_file_count + 1); // including head - - auto file_entry_table_pos = - duplicate_data ? fs_info.data_region_offset + - fs_info.file_entry_table.duplicate.block_index * - static_cast(fs_info.data_region_block_size) - : fs_info.file_entry_table.non_duplicate; - - TRY(CheckedMemcpy(file_entry_table.data(), header_vector, file_entry_table_pos, - file_entry_table.size() * sizeof(FileEntryTableEntry)), - LOG_ERROR(Core, "File is too small")); - - // Read file allocation table - fat.resize(fs_info.file_allocation_table_entry_count); - TRY(CheckedMemcpy(fat.data(), header_vector, fs_info.file_allocation_table_offset, - fat.size() * sizeof(FATNode)), - LOG_ERROR(Core, "File size is too small")); - - return true; -} - -bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const { - if (!FileUtil::CreateFullPath(path)) { - LOG_ERROR(Core, "Could not create path {}", path); - return false; - } - - if (index >= file_entry_table.size()) { - LOG_ERROR(Core, "Index out of bound {}", index); - return false; - } - auto entry = file_entry_table[index]; - - std::array name_data = {}; // Append a null terminator - std::memcpy(name_data.data(), entry.name.data(), entry.name.size()); - - std::string name = name_data.data(); - FileUtil::IOFile file(path + name, "wb"); - if (!file) { - LOG_ERROR(Core, "Could not open file {}", path + name); - return false; - } - - u32 block = entry.data_block_index; - if (block == 0x80000000) { // empty file - return true; - } - - u64 file_size = entry.file_size; - while (true) { - // Entry index is block index + 1 - auto block_data = fat[block + 1]; - - u32 last_block = block; - if (block_data.v.flag) { // This node has multiple entries - last_block = fat[block + 2].v.index - 1; - } - - const std::size_t size = - static_cast(fs_info.data_region_block_size) * (last_block - block + 1); - const std::size_t to_write = std::min(file_size, size); - - if (data_region.size() < - static_cast(fs_info.data_region_block_size) * block + to_write) { - LOG_ERROR(Core, "Out of bound block: {} to_write: {}", block, to_write); - return false; - } - if (file.WriteBytes(data_region.data() + fs_info.data_region_block_size * block, - to_write) != to_write) { - LOG_ERROR(Core, "Write data failed (file: {})", path + name); - return false; - } - file_size -= to_write; - - if (block_data.v.index == 0 || file_size == 0) // last node - break; - - block = block_data.v.index - 1; - } - - return true; -} - -bool SDSavegame::Extract(std::string path) const { - if (path.back() != '/' && path.back() != '\\') { - path += '/'; - } - - // All saves on a physical 3DS are called 00000001.sav - if (!ExtractDirectory(path + "00000001/", 1)) { // Directory 1 = root - return false; - } - - if (!WriteMetadata(path + "00000001.metadata")) { - return false; - } - - return true; -} - -ArchiveFormatInfo SDSavegame::GetFormatInfo() const { - // Tests on a physical 3DS shows that the `total_size` field seems to always be 0 - // when requested with the UserSaveData archive, and 134328448 when requested with - // the SaveData archive. More investigation is required to tell whether this is a fixed value. - ArchiveFormatInfo format_info = {/* total_size */ 0x40000, - /* number_directories */ fs_info.maximum_directory_count, - /* number_files */ fs_info.maximum_file_count, - /* duplicate_data */ duplicate_data}; - - return format_info; -} - -SDExtdata::SDExtdata(std::string data_path_, const SDMCDecryptor& decryptor_) - : data_path(std::move(data_path_)), decryptor(&decryptor_) { - - if (data_path.back() != '/' && data_path.back() != '\\') { - data_path += '/'; - } - - use_decryptor = true; - is_good = Init(); -} - -SDExtdata::SDExtdata(std::string data_path_) : data_path(std::move(data_path_)) { - if (data_path.back() != '/' && data_path.back() != '\\') { - data_path += '/'; - } - - use_decryptor = false; - is_good = Init(); -} - -SDExtdata::~SDExtdata() = default; - -std::vector SDExtdata::ReadFile(const std::string& path) const { - if (use_decryptor) { - return decryptor->DecryptFile(path); - } else { - FileUtil::IOFile file(path, "rb"); - return file.GetData(); - } -} - -bool SDExtdata::Init() { - // Read VSXE file - auto vsxe_raw = ReadFile(data_path + "00000000/00000001"); - if (vsxe_raw.empty()) { - LOG_ERROR(Core, "Failed to load or decrypt VSXE"); - return false; - } - DataContainer vsxe_container(std::move(vsxe_raw)); - if (!vsxe_container.IsGood()) { - return false; - } - - std::vector> container_data; - if (!vsxe_container.GetIVFCLevel4Data(container_data)) { - return false; - } - const auto& vsxe = container_data[0]; - - // Read header - TRY(CheckedMemcpy(&header, vsxe, 0, sizeof(header)), LOG_ERROR(Core, "File size is too small")); - - if (header.magic != MakeMagic('V', 'S', 'X', 'E') || header.version != 0x30000) { - LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); - return false; - } - - // Read filesystem information - TRY(CheckedMemcpy(&fs_info, vsxe, header.filesystem_information_offset, sizeof(fs_info)), - LOG_ERROR(Core, "File size is too small")); - - // Read data region - TRY(CheckedMemcpy(data_region.data(), vsxe, fs_info.data_region_offset, data_region.size()), - LOG_ERROR(Core, "File size is too small")); - - // Read directory entry table - directory_entry_table.resize(fs_info.maximum_directory_count + 2); // including head and root - - const auto directory_entry_table_pos = - fs_info.data_region_offset + fs_info.directory_entry_table.duplicate.block_index * - static_cast(fs_info.data_region_block_size); - - TRY(CheckedMemcpy(directory_entry_table.data(), vsxe, directory_entry_table_pos, - directory_entry_table.size() * sizeof(DirectoryEntryTableEntry)), - LOG_ERROR(Core, "File size is too small")); - - // Read file entry table - file_entry_table.resize(fs_info.maximum_file_count + 1); // including head - - const auto file_entry_table_pos = - fs_info.data_region_offset + fs_info.file_entry_table.duplicate.block_index * - static_cast(fs_info.data_region_block_size); - - TRY(CheckedMemcpy(file_entry_table.data(), vsxe, file_entry_table_pos, - file_entry_table.size() * sizeof(FileEntryTableEntry)), - LOG_ERROR(Core, "File size is too small")); - - // File allocation table isn't needed here, as the only files allocated by them are - // directory/file entry tables which we already read above. - return true; -} - -bool SDExtdata::Extract(std::string path) const { - if (path.back() != '/' && path.back() != '\\') { - path += '/'; - } - - if (!ExtractDirectory(path, 1)) { - return false; - } - - if (!WriteMetadata(path + "metadata")) { - return false; - } - - return true; -} - -bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const { - /// Maximum amount of device files a device directory can hold. - constexpr u32 DeviceDirCapacity = 126; - - if (index >= file_entry_table.size()) { - LOG_ERROR(Core, "Index out of bound {}", index); - return false; - } - auto entry = file_entry_table[index]; - - std::array name_data = {}; // Append a null terminator - std::memcpy(name_data.data(), entry.name.data(), entry.name.size()); - - std::string name = name_data.data(); - FileUtil::IOFile file(path + name, "wb"); - if (!file) { - LOG_ERROR(Core, "Could not open file {}", path + name); - return false; - } - - u32 file_index = index + 1; - u32 sub_directory_id = file_index / DeviceDirCapacity; - u32 sub_file_id = file_index % DeviceDirCapacity; - std::string device_file_path = - fmt::format("{}{:08x}/{:08x}", data_path, sub_directory_id, sub_file_id); - - auto container_data = ReadFile(device_file_path); - if (container_data.empty()) { // File does not exist? - LOG_WARNING(Core, "Ignoring file {}", device_file_path); - return true; - } - - DataContainer container(std::move(container_data)); - if (!container.IsGood()) { - return false; - } - - std::vector> data; - if (!container.GetIVFCLevel4Data(data)) { - return false; - } - - if (file.WriteBytes(data[0].data(), data[0].size()) != data[0].size()) { - LOG_ERROR(Core, "Write data failed (file: {})", path + name); - return false; - } - - return true; -} - -ArchiveFormatInfo SDExtdata::GetFormatInfo() const { - // This information is based on how Citra created the metadata in FS - ArchiveFormatInfo format_info = {/* total_size */ 0, - /* number_directories */ fs_info.maximum_directory_count, - /* number_files */ fs_info.maximum_file_count, - /* duplicate_data */ false}; - - return format_info; -} - -} // namespace Core diff --git a/src/core/inner_fat.h b/src/core/inner_fat.h deleted file mode 100644 index becd666..0000000 --- a/src/core/inner_fat.h +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "common/bit_field.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace Core { - -class SDMCDecryptor; - -/// Parameters of the archive, as specified in the Create or Format call. -struct ArchiveFormatInfo { - u32_le total_size; ///< The pre-defined size of the archive. - u32_le number_directories; ///< The pre-defined number of directories in the archive. - u32_le number_files; ///< The pre-defined number of files in the archive. - u8 duplicate_data; ///< Whether the archive should duplicate the data. -}; -static_assert(std::is_standard_layout_v && std::is_trivial_v, - "ArchiveFormatInfo is not POD"); - -union TableOffset { - // This has different meanings for different savegame layouts - struct { // duplicate data = true - u32_le block_index; - u32_le block_count; - } duplicate; - - u64_le non_duplicate; // duplicate data = false -}; - -struct FATHeader { - u32_le magic; - u32_le version; - u64_le filesystem_information_offset; - u64_le image_size; - u32_le image_block_size; - INSERT_PADDING_BYTES(4); -}; -static_assert(sizeof(FATHeader) == 0x20, "FATHeader has incorrect size"); - -struct FileSystemInformation { - INSERT_PADDING_BYTES(4); - u32_le data_region_block_size; - u64_le directory_hash_table_offset; - u32_le directory_hash_table_bucket_count; - INSERT_PADDING_BYTES(4); - u64_le file_hash_table_offset; - u32_le file_hash_table_bucket_count; - INSERT_PADDING_BYTES(4); - u64_le file_allocation_table_offset; - u32_le file_allocation_table_entry_count; - INSERT_PADDING_BYTES(4); - u64_le data_region_offset; - u32_le data_region_block_count; - INSERT_PADDING_BYTES(4); - TableOffset directory_entry_table; - u32_le maximum_directory_count; - INSERT_PADDING_BYTES(4); - TableOffset file_entry_table; - u32_le maximum_file_count; - INSERT_PADDING_BYTES(4); -}; -static_assert(sizeof(FileSystemInformation) == 0x68, "FileSystemInformation has incorrect size"); - -struct DirectoryEntryTableEntry { - u32_le parent_directory_index; - std::array name; - u32_le next_sibling_index; - u32_le first_subdirectory_index; - u32_le first_file_index; - INSERT_PADDING_BYTES(4); - u32_le next_hash_bucket_entry; -}; -static_assert(sizeof(DirectoryEntryTableEntry) == 0x28, - "DirectoryEntryTableEntry has incorrect size"); - -struct FileEntryTableEntry { - u32_le parent_directory_index; - std::array name; - u32_le next_sibling_index; - INSERT_PADDING_BYTES(4); - u32_le data_block_index; - u64_le file_size; - INSERT_PADDING_BYTES(4); - u32_le next_hash_bucket_entry; -}; -static_assert(sizeof(FileEntryTableEntry) == 0x30, "FileEntryTableEntry has incorrect size"); - -struct FATNode { - union { - BitField<0, 31, u32> index; - BitField<31, 1, u32> flag; - - u32_le raw; - } u, v; -}; - -/** - * Virtual interface for the inner FAT filesystem of SD Savegames/Extdata/TitleDB. - */ -class InnerFAT { -public: - virtual ~InnerFAT(); - - /** - * Returns whether the filesystem is in "good" state, i.e. successfully initialized. - */ - bool IsGood() const; - - /** - * Completely extracts everything from this filesystem, including files, directories - * and metadata used by Citra. - * @return true on success, false otherwise - */ - virtual bool Extract(std::string path) const = 0; - - /** - * Extracts the index-th file in the file entry table to a certain path. (The path does not - * contain the file name). - * @return true on success, false otherwise - */ - virtual bool ExtractFile(const std::string& path, std::size_t index) const = 0; - - /** - * Recursively extracts the index-th directory in the directory entry table. - * @return true on success, false otherwise - */ - bool ExtractDirectory(const std::string& path, std::size_t index) const; - - /** - * Writes the corresponding archive metadata to a certain path. - * @return true on success, false otherwise - */ - bool WriteMetadata(const std::string& path) const; - -protected: - /** - * Gets the ArchiveFormatInfo of this archive, used for writing the archive metadata. - */ - virtual ArchiveFormatInfo GetFormatInfo() const = 0; - - bool is_good = false; - FATHeader header; - FileSystemInformation fs_info; - std::vector directory_entry_table; - std::vector file_entry_table; - std::vector data_region; -}; - -class SDSavegame : public InnerFAT { -public: - explicit SDSavegame(std::vector> partitions); - ~SDSavegame() override; - - bool Extract(std::string path) const override; - -private: - bool Init(); - bool ExtractFile(const std::string& path, std::size_t index) const override; - ArchiveFormatInfo GetFormatInfo() const override; - - std::vector fat; - bool duplicate_data; // Layout variant - - // Temporary storage for construction data - std::vector data; - std::vector partitionA; - std::vector partitionB; -}; - -class SDExtdata : public InnerFAT { -public: - /** - * Loads an SD extdata folder. - * @param data_path Path to the ENCRYPTED SD extdata folder, relative to decryptor root - * @param decryptor Const reference to the SDMCDecryptor. - */ - explicit SDExtdata(std::string data_path, const SDMCDecryptor& decryptor); - - /** - * Loads an Extdata folder without encryption. - * @param data_path Path to the DECRYPTED extdata folder - */ - explicit SDExtdata(std::string data_path); - - ~SDExtdata() override; - - bool Extract(std::string path) const override; - -private: - std::vector ReadFile(const std::string& path) const; - bool Init(); - bool ExtractFile(const std::string& path, std::size_t index) const override; - ArchiveFormatInfo GetFormatInfo() const override; - - std::string data_path; - const SDMCDecryptor* decryptor = nullptr; - bool use_decryptor = true; -}; - -} // namespace Core diff --git a/src/core/inner_fat.hpp b/src/core/inner_fat.hpp new file mode 100644 index 0000000..1722aec --- /dev/null +++ b/src/core/inner_fat.hpp @@ -0,0 +1,341 @@ +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/assert.h" +#include "common/bit_field.h" +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/swap.h" + +namespace Core { + +union TableOffset { + // This has different meanings for different savegame layouts + struct { // duplicate data = true + u32_le block_index; + u32_le block_count; + } duplicate; + + u64_le non_duplicate; // duplicate data = false +}; + +struct FATHeader { + u32_le magic; + u32_le version; + u64_le filesystem_information_offset; + u64_le image_size; + u32_le image_block_size; + INSERT_PADDING_BYTES(4); +}; +static_assert(sizeof(FATHeader) == 0x20, "FATHeader has incorrect size"); + +struct FileSystemInformation { + INSERT_PADDING_BYTES(4); + u32_le data_region_block_size; + u64_le directory_hash_table_offset; + u32_le directory_hash_table_bucket_count; + INSERT_PADDING_BYTES(4); + u64_le file_hash_table_offset; + u32_le file_hash_table_bucket_count; + INSERT_PADDING_BYTES(4); + u64_le file_allocation_table_offset; + u32_le file_allocation_table_entry_count; + INSERT_PADDING_BYTES(4); + u64_le data_region_offset; + u32_le data_region_block_count; + INSERT_PADDING_BYTES(4); + TableOffset directory_entry_table; + u32_le maximum_directory_count; + INSERT_PADDING_BYTES(4); + TableOffset file_entry_table; + u32_le maximum_file_count; + INSERT_PADDING_BYTES(4); +}; +static_assert(sizeof(FileSystemInformation) == 0x68, "FileSystemInformation has incorrect size"); + +struct DirectoryEntryTableEntry { + u32_le parent_directory_index; + std::array name; + u32_le next_sibling_index; + u32_le first_subdirectory_index; + u32_le first_file_index; + INSERT_PADDING_BYTES(4); + u32_le next_hash_bucket_entry; +}; +static_assert(sizeof(DirectoryEntryTableEntry) == 0x28, + "DirectoryEntryTableEntry has incorrect size"); + +struct FileEntryTableEntry { + u32_le parent_directory_index; + std::array name; + u32_le next_sibling_index; + INSERT_PADDING_BYTES(4); + u32_le data_block_index; + u64_le file_size; + INSERT_PADDING_BYTES(4); + u32_le next_hash_bucket_entry; +}; +static_assert(sizeof(FileEntryTableEntry) == 0x30, "FileEntryTableEntry has incorrect size"); + +struct FATNode { + union { + BitField<0, 31, u32> index; + BitField<31, 1, u32> flag; + + u32_le raw; + } u, v; +}; + +namespace detail { + +#pragma pack(push, 1) +template +struct FullHeaderInternal { + static constexpr std::size_t PreheaderSize = sizeof(Preheader); + Preheader pre_header; + FATHeader fat_header; +}; + +template <> +struct FullHeaderInternal { + static constexpr std::size_t PreheaderSize = 0; + FATHeader fat_header; +}; +#pragma pack(pop) + +template +struct FullHeaderInternal2 { + using Type = FullHeaderInternal; + + static_assert(sizeof(Type) == sizeof(Preheader) + sizeof(FATHeader)); + static_assert(std::is_standard_layout_v); +}; + +template <> +struct FullHeaderInternal2 { + using Type = FullHeaderInternal; + + static_assert(sizeof(Type) == sizeof(FATHeader)); + static_assert(std::is_standard_layout_v); +}; + +} // namespace detail + +template +using FullHeader = detail::FullHeaderInternal2::Type; + +template +class InnerFAT { +protected: + bool Init(std::vector> partitions) { + duplicate_data = partitions.size() == 1; + const auto& header_vector = partitions[0]; + + // Read header + TRY(CheckedMemcpy(&header, header_vector, 0, sizeof(header)), + LOG_ERROR(Core, "File size is too small")); + + if (!static_cast(this)->CheckMagic()) { + LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); + return false; + } + + static constexpr std::size_t PreheaderSize = FullHeader::PreheaderSize; + + // Read filesystem information + TRY(CheckedMemcpy(&fs_info, header_vector, + PreheaderSize + header.fat_header.filesystem_information_offset, + sizeof(fs_info)), + LOG_ERROR(Core, "File size is too small")); + + // Read data region + if (duplicate_data) { + data_region.resize(fs_info.data_region_block_count * + static_cast(fs_info.data_region_block_size)); + // This check is relaxed (not counting in PreheaderSize) for title.db + if (partitions[0].size() < fs_info.data_region_offset + data_region.size()) { + LOG_ERROR(Core, "File size is too small"); + return false; + } + const auto offset = PreheaderSize + fs_info.data_region_offset; + ASSERT(partitions[0].size() > offset); + const auto to_copy = std::min(data_region.size(), partitions[0].size() - offset); + std::memcpy(data_region.data(), partitions[0].data() + offset, to_copy); + } else { + data_region = std::move(partitions[1]); + } + + // Directory & file entry tables are allocated in the data region as if they were normal + // files. However, only continuous allocation has been observed so far according to 3DBrew, + // so it should be safe to directly read the bytes. + + // Read directory entry table, +2 to include head and root + directory_entry_table.resize(fs_info.maximum_directory_count + 2); + + auto directory_entry_table_pos = + duplicate_data ? PreheaderSize + fs_info.data_region_offset + + fs_info.directory_entry_table.duplicate.block_index * + static_cast(fs_info.data_region_block_size) + : PreheaderSize + fs_info.directory_entry_table.non_duplicate; + + TRY(CheckedMemcpy(directory_entry_table.data(), header_vector, directory_entry_table_pos, + directory_entry_table.size() * sizeof(DirectoryEntryType)), + LOG_ERROR(Core, "File is too small")); + + // Read file entry table + file_entry_table.resize(fs_info.maximum_file_count + 1); // including head + + auto file_entry_table_pos = + duplicate_data ? PreheaderSize + fs_info.data_region_offset + + fs_info.file_entry_table.duplicate.block_index * + static_cast(fs_info.data_region_block_size) + : PreheaderSize + fs_info.file_entry_table.non_duplicate; + + TRY(CheckedMemcpy(file_entry_table.data(), header_vector, file_entry_table_pos, + file_entry_table.size() * sizeof(FileEntryType)), + LOG_ERROR(Core, "File is too small")); + + // Read file allocation table + fat.resize(fs_info.file_allocation_table_entry_count); + TRY(CheckedMemcpy(fat.data(), header_vector, + PreheaderSize + fs_info.file_allocation_table_offset, + fat.size() * sizeof(FATNode)), + LOG_ERROR(Core, "File size is too small")); + + return true; + } + + bool GetFileData(std::vector& out, std::size_t index) const { + if (index >= file_entry_table.size()) { + LOG_ERROR(Core, "Index out of bound {}", index); + return false; + } + auto entry = file_entry_table[index]; + + u32 block = entry.data_block_index; + if (block == 0x80000000) { // empty file + return true; + } + + u64 file_size = entry.file_size; + if (file_size >= 64 * 1024 * 1024) { + LOG_ERROR(Core, "File size too large"); + return false; + } + + out.resize(file_size); + std::size_t written = 0; + while (true) { + // Entry index is block index + 1 + auto block_data = fat[block + 1]; + + u32 last_block = block; + if (block_data.v.flag) { // This node has multiple entries + last_block = fat[block + 2].v.index - 1; + } + + const std::size_t size = + static_cast(fs_info.data_region_block_size) * (last_block - block + 1); + const std::size_t to_write = std::min(file_size, size); + + TRY(CheckedMemcpy(out.data() + written, data_region, + fs_info.data_region_block_size * block, to_write), + LOG_ERROR(Core, "File data out of bound")); + file_size -= to_write; + written += to_write; + + if (block_data.v.index == 0 || file_size == 0) // last node + break; + + block = block_data.v.index - 1; + } + + return true; + } + + bool duplicate_data; + FullHeader header; + FileSystemInformation fs_info; + std::vector directory_entry_table; + std::vector file_entry_table; + std::vector fat; + std::vector data_region; +}; + +/// Parameters of the archive, as specified in the Create or Format call. +struct ArchiveFormatInfo { + u32_le total_size; ///< The pre-defined size of the archive. + u32_le number_directories; ///< The pre-defined number of directories in the archive. + u32_le number_files; ///< The pre-defined number of files in the archive. + u8 duplicate_data; ///< Whether the archive should duplicate the data. +}; +static_assert(std::is_standard_layout_v && std::is_trivial_v, + "ArchiveFormatInfo is not POD"); + +/** + * Represents an Archive-like pack where there are directory structures. + * Has an ExtractDirectory function that recursively extracts directories. + * Expects implementor to have ExtractFile. + */ +template +class Archive : protected InnerFAT { +public: + bool ExtractDirectory(const std::string& path, std::size_t index) const { + if (index >= this->directory_entry_table.size()) { + LOG_ERROR(Core, "Index out of bound {}", index); + return false; + } + const auto& entry = this->directory_entry_table[index]; + + std::array name_data = {}; // Append a null terminator + std::memcpy(name_data.data(), entry.name.data(), entry.name.size()); + + std::string name = name_data.data(); + std::string new_path = name.empty() ? path : path + name + "/"; // Name is empty for root + + if (!FileUtil::CreateFullPath(new_path)) { + LOG_ERROR(Core, "Could not create path {}", new_path); + return false; + } + + // Files + u32 cur = entry.first_file_index; + while (cur != 0) { + if (cur >= this->file_entry_table.size()) { + LOG_ERROR(Core, "Index out of bound {}", cur); + return false; + } + const auto& file_entry = this->file_entry_table[cur]; + + std::array name_data = {}; // Append a null terminator + std::memcpy(name_data.data(), file_entry.name.data(), file_entry.name.size()); + + if (!static_cast(this)->ExtractFile(new_path + std::string{name_data.data()}, + cur)) + return false; + cur = this->file_entry_table[cur].next_sibling_index; + } + + // Subdirectories + cur = entry.first_subdirectory_index; + while (cur != 0) { + if (!ExtractDirectory(new_path, cur)) + return false; + cur = this->directory_entry_table[cur].next_sibling_index; + } + + return true; + } +}; + +} // namespace Core diff --git a/src/core/savegame.cpp b/src/core/savegame.cpp new file mode 100644 index 0000000..d022a7f --- /dev/null +++ b/src/core/savegame.cpp @@ -0,0 +1,66 @@ +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/savegame.h" + +namespace Core { + +Savegame::Savegame(std::vector> partitions) { + is_good = Archive::Init(std::move(partitions)); +} + +Savegame::~Savegame() = default; + +bool Savegame::CheckMagic() const { + if (header.fat_header.magic != MakeMagic('S', 'A', 'V', 'E') || + header.fat_header.version != 0x40000) { + + LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); + return false; + } + return true; +} + +bool Savegame::IsGood() const { + return is_good; +} + +bool Savegame::ExtractFile(const std::string& path, std::size_t index) const { + std::vector data; + if (!GetFileData(data, index)) { + LOG_ERROR(Core, "Could not get file data for index {}", index); + return false; + } + return FileUtil::WriteBytesToFile(path, data.data(), data.size()); +} + +bool Savegame::Extract(std::string path) const { + if (path.back() != '/' && path.back() != '\\') { + path += '/'; + } + + // All saves on a physical 3DS are called 00000001.sav + if (!ExtractDirectory(path + "00000001/", 1)) { // Directory 1 = root + return false; + } + + // Write format info + const auto format_info = GetFormatInfo(); + return FileUtil::WriteBytesToFile(path + "00000001.metadata", &format_info, + sizeof(format_info)); +} + +ArchiveFormatInfo Savegame::GetFormatInfo() const { + // Tests on a physical 3DS shows that the `total_size` field seems to always be 0 + // when requested with the UserSaveData archive, and 134328448 when requested with + // the SaveData archive. More investigation is required to tell whether this is a fixed value. + ArchiveFormatInfo format_info = {/* total_size */ 0x40000, + /* number_directories */ fs_info.maximum_directory_count, + /* number_files */ fs_info.maximum_file_count, + /* duplicate_data */ duplicate_data}; + + return format_info; +} + +} // namespace Core diff --git a/src/core/savegame.h b/src/core/savegame.h new file mode 100644 index 0000000..d7b1ce7 --- /dev/null +++ b/src/core/savegame.h @@ -0,0 +1,30 @@ +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/inner_fat.hpp" + +namespace Core { + +class Savegame final : public Archive { +public: + explicit Savegame(std::vector> partitions); + ~Savegame(); + + bool IsGood() const; + bool Extract(std::string path) const; + +private: + bool CheckMagic() const; + bool ExtractFile(const std::string& path, std::size_t index) const; + ArchiveFormatInfo GetFormatInfo() const; + + bool is_good = false; + + friend class Archive; + friend class InnerFAT; +}; + +} // namespace Core diff --git a/src/core/title_db.cpp b/src/core/title_db.cpp index a49a01f..b98127b 100644 --- a/src/core/title_db.cpp +++ b/src/core/title_db.cpp @@ -15,14 +15,24 @@ bool TitleDB::IsGood() const { return is_good; } -// Note: Title DB is actually a degenerate version of the Inner FAT. -// We are simplifying things as much as possible and not actually dealing with FAT nodes. bool TitleDB::Init(std::vector data) { - // Read header, FAT header and filesystem information - TRY(CheckedMemcpy(&header, data, 0, sizeof(header)), LOG_ERROR(Core, "File size is too small")); + if (!InnerFAT_TitleDB::Init({std::move(data)})) { + return false; + } - if (header.db_magic != MakeMagic('N', 'A', 'N', 'D', 'T', 'D', 'B', 0) && - header.db_magic != MakeMagic('T', 'E', 'M', 'P', 'T', 'D', 'B', 0)) { + u32 cur = directory_entry_table[1].first_file_index; + while (cur != 0) { + if (!LoadTitleInfo(cur)) { + return false; + } + cur = file_entry_table[cur].next_sibling_index; + } + return true; +} + +bool TitleDB::CheckMagic() const { + if (header.pre_header.db_magic != MakeMagic('N', 'A', 'N', 'D', 'T', 'D', 'B', 0) && + header.pre_header.db_magic != MakeMagic('T', 'E', 'M', 'P', 'T', 'D', 'B', 0)) { LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); return false; @@ -34,70 +44,22 @@ bool TitleDB::Init(std::vector data) { LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); return false; } - - if (header.fat_header.image_block_size != 0x80 || - header.fs_info.data_region_block_size != 0x80) { // This simplifies things - LOG_ERROR(Core, "Unexpected block size (this may be a bug)"); - return false; - } - - // Read file entry table - file_entry_table.resize(header.fs_info.maximum_file_count + 1); // including head - - auto file_entry_table_pos = TitleDBPreheaderSize + header.fs_info.data_region_offset + - header.fs_info.file_entry_table.duplicate.block_index * - static_cast(header.fs_info.data_region_block_size); - - TRY(CheckedMemcpy(file_entry_table.data(), data, file_entry_table_pos, - file_entry_table.size() * sizeof(TitleDBFileEntryTableEntry)), - LOG_ERROR(Core, "File is too small")); - - // Read directory entry table for first file index - auto first_file_index_pos = - TitleDBPreheaderSize + header.fs_info.data_region_offset + - header.fs_info.directory_entry_table.duplicate.block_index * - static_cast(header.fs_info.data_region_block_size) + - 0x20 /* sizeof TitleDB's directory entry (to skip head) */ + - 0x0C /* offset of first_file_index in directory entry of Title DB */; - - if (data.size() < first_file_index_pos + 4) { - LOG_ERROR(Core, "File size is too small"); - return false; - } - - const u32 first_file_index = *reinterpret_cast(data.data() + first_file_index_pos); - LOG_INFO(Core, "First file index is {}", first_file_index); - u32 cur = first_file_index; - while (cur != 0) { - if (!LoadTitleInfo(data, cur)) { - return false; - } - cur = file_entry_table[cur].next_sibling_index; - } return true; } -bool TitleDB::LoadTitleInfo(const std::vector& data, u32 index) { - auto entry = file_entry_table[index]; - u32 block = entry.data_block_index; - if (block == 0x80000000) { // empty file - LOG_ERROR(Core, "Entry is an empty file"); - return false; - } - - u64 file_size = entry.file_size; - if (file_size != sizeof(TitleInfoEntry)) { - LOG_ERROR(Core, "Entry has incorrect size {}", file_size); +bool TitleDB::LoadTitleInfo(u32 index) { + std::vector data; + if (!GetFileData(data, index)) { return false; } TitleInfoEntry title; - const auto offset = TitleDBPreheaderSize + header.fs_info.data_region_offset + - block * header.fs_info.data_region_block_size; - TRY(CheckedMemcpy(&title, data, offset, sizeof(TitleInfoEntry)), - LOG_ERROR(Core, "File size is too small")); + if (data.size() != sizeof(TitleInfoEntry)) { + LOG_ERROR(Core, "Entry {} has incorrect size", index); + } + std::memcpy(&title, data.data(), data.size()); - titles.emplace(entry.title_id, title); + titles.emplace(file_entry_table[index].title_id, title); return true; } diff --git a/src/core/title_db.h b/src/core/title_db.h index 47eb01e..924ae66 100644 --- a/src/core/title_db.h +++ b/src/core/title_db.h @@ -10,22 +10,28 @@ #include "common/common_funcs.h" #include "common/common_types.h" #include "common/swap.h" -#include "core/inner_fat.h" +#include "core/inner_fat.hpp" namespace Core { -struct TitleDBHeader { +struct TitleDBPreheader { u64_le db_magic; INSERT_PADDING_BYTES(0x78); - FATHeader fat_header; - FileSystemInformation fs_info; }; -constexpr std::size_t TitleDBPreheaderSize = 0x80; -static_assert(sizeof(TitleDBHeader) == - TitleDBPreheaderSize + sizeof(FATHeader) + sizeof(FileSystemInformation), - "TitleDB preheader has incorrect size"); +static_assert(sizeof(TitleDBPreheader) == 0x80, "TitleDB pre-header has incorrect size"); #pragma pack(push, 1) +struct TitleDBDirectoryEntryTableEntry { + u32_le parent_directory_index; + u32_le next_sibling_index; + u32_le first_subdirectory_index; + u32_le first_file_index; + INSERT_PADDING_BYTES(12); + u32_le next_hash_bucket_entry; +}; +static_assert(sizeof(TitleDBDirectoryEntryTableEntry) == 0x20, + "TitleDBDirectoryEntryTableEntry has incorrect size"); + struct TitleDBFileEntryTableEntry { u32_le parent_directory_index; u64_le title_id; @@ -56,7 +62,11 @@ struct TitleInfoEntry { }; static_assert(sizeof(TitleInfoEntry) == 0x80, "TitleInfoEntry has incorrect size"); -class TitleDB { +class TitleDB; +using InnerFAT_TitleDB = InnerFAT; + +class TitleDB final : public InnerFAT_TitleDB { public: explicit TitleDB(std::vector data); bool IsGood() const; @@ -65,11 +75,12 @@ public: private: bool Init(std::vector data); - bool LoadTitleInfo(const std::vector& data, u32 index); + bool CheckMagic() const; + bool LoadTitleInfo(u32 index); bool is_good = false; - TitleDBHeader header; - std::vector file_entry_table; + + friend InnerFAT_TitleDB; }; } // namespace Core diff --git a/src/frontend/utilities.cpp b/src/frontend/utilities.cpp index d3bb661..2ce3ab5 100644 --- a/src/frontend/utilities.cpp +++ b/src/frontend/utilities.cpp @@ -10,9 +10,10 @@ #include #include "core/data_container.h" #include "core/decryptor.h" -#include "core/inner_fat.h" +#include "core/extdata.h" #include "core/key/key.h" #include "core/ncch/ncch_container.h" +#include "core/savegame.h" #include "frontend/select_files_dialog.h" #include "frontend/utilities.h" #include "ui_utilities.h" @@ -211,7 +212,7 @@ void UtilitiesDialog::SaveDataExtractionTool() { return false; } - Core::SDSavegame save(std::move(container_data)); + Core::Savegame save(std::move(container_data)); if (!save.IsGood()) { return false; } @@ -237,7 +238,7 @@ void UtilitiesDialog::SaveDataExtractionTool() { return false; } - Core::SDSavegame save(std::move(container_data)); + Core::Savegame save(std::move(container_data)); if (!save.IsGood()) { return false; } @@ -264,7 +265,7 @@ void UtilitiesDialog::ExtdataExtractionTool() { ShowProgressDialog( [sdmc_root = sdmc_root, relative_source = relative_source, destination = destination] { Core::SDMCDecryptor decryptor(sdmc_root); - Core::SDExtdata extdata(relative_source, decryptor); + Core::Extdata extdata(relative_source, decryptor); if (!extdata.IsGood()) { return false; }