lots of important fixes

- DPFS container is fixed
- SD Savegame is fixed
- added slot0x25KeyX load
- added regex for titile ID
- added aes_keys.txt import
- sd savegame listing is fixed (uninitialized won't be listed any more)
- error handling is improved (removed asserts and replaced with return values)
- UI is now functional
- config is now checked in main
This commit is contained in:
zhupengfei
2019-08-30 15:33:47 +08:00
parent 8acfe9f304
commit 67e6b05e87
22 changed files with 490 additions and 78 deletions
+24 -5
View File
@@ -5,6 +5,9 @@ include_directories(.)
set_property(DIRECTORY APPEND PROPERTY
COMPILE_DEFINITIONS $<$<CONFIG:Debug>:_DEBUG> $<$<NOT:$<CONFIG:Debug>>:NDEBUG>)
# Ensure that projects build with Unicode support.
add_definitions(-DUNICODE -D_UNICODE)
# Set compilation flags
if (MSVC)
set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE)
@@ -18,10 +21,15 @@ if (MSVC)
# Avoid windows.h from including some usually unused libs like winsocks.h, since this might cause some redefinition errors.
add_definitions(-DWIN32_LEAN_AND_MEAN)
# Ensure that projects build with Unicode support.
add_definitions(-DUNICODE -D_UNICODE)
# /W3 - Level 3 warnings
# /W4 - Level 4 warnings
# /w34263 - Non-virtual member function hides base class virtual function
# /w44265 - Class has virtual functions, but destructor is not virtual
# /w34456 - Declaration of 'var' hides previous local declaration
# /w34457 - Declaration of 'var' hides function parameter
# /w34458 - Declaration of 'var' hides class member
# /w34459 - Declaration of 'var' hides global definition
# /w34946 - Reinterpret-cast between related types
# /wd4592 - Symbol will be dynamically initialized (implementation limitation)
# /MP - Multi-threaded compilation
# /Zi - Output debugging information
# /Zo - Enhanced debug info for optimized builds
@@ -32,7 +40,15 @@ if (MSVC)
# /Zc:inline - Let codegen omit inline functions in object files
# /Zc:throwingNew - Let codegen assume `operator new` (without std::nothrow) will never return null
add_compile_options(
/W3
/W4
/w34263
/w44265
/w34456
/w34457
/w34458
/w34459
/w34946
/wd4592
/MP
/Zi
/Zo
@@ -58,6 +74,9 @@ else()
add_compile_options(
-Wall
-Wno-attributes
-pedantic
-pedantic-errors
-Wno-missing-braces
)
if (WARNINGS_AS_ERRORS)
+1
View File
@@ -38,3 +38,4 @@
#define SECRET_SECTOR "sector0x96.bin"
#define MOVABLE_SED "movable.sed"
#define SEED_DB "seeddb.bin"
#define AES_KEYS "aes_keys.txt"
+2 -1
View File
@@ -36,7 +36,8 @@ void PrintLog(std::FILE* f, const std::string& log_class, const std::string& lev
#ifdef _DEBUG
#define LOG_TRACE(log_class, ...) \
PrintLog(stderr, #log_class, "Trace", "[1;30m", __FILE__, __LINE__, __func__, __VA_ARGS__)
(void(0)) // PrintLog(stderr, #log_class, "Trace", "[1;30m", __FILE__, __LINE__, __func__,
// __VA_ARGS__)
#else
#define LOG_TRACE(log_class, fmt, ...) (void(0))
#endif
+33 -14
View File
@@ -25,7 +25,7 @@ u8 DPFSContainer::GetBit(u8 level, u8 selector, u64 index) const {
return (data[(descriptor.levels[level].offset + selector * descriptor.levels[level].size) / 4 +
index / 32] >>
(31 - (index % 32))) &
1;
static_cast<u32_le>(1);
}
u8 DPFSContainer::GetByte(u8 level, u8 selector, u64 index) const {
@@ -38,9 +38,9 @@ u8 DPFSContainer::GetByte(u8 level, u8 selector, u64 index) const {
std::vector<u8> DPFSContainer::GetLevel3Data() const {
std::vector<u8> level3_data(descriptor.levels[2].size);
for (std::size_t i = 0; i < level3_data.size(); i++) {
auto level2_bit_index = i / std::pow(2, descriptor.levels[1].block_size);
auto level2_bit_index = i / std::pow(2, descriptor.levels[2].block_size);
auto level1_bit_index =
(level2_bit_index / 8) / std::pow(2, descriptor.levels[0].block_size);
(level2_bit_index / 8) / std::pow(2, descriptor.levels[1].block_size);
auto level2_selector = GetBit(0, level1_selector, level1_bit_index);
auto level3_selector = GetBit(1, level2_selector, level2_bit_index);
level3_data[i] = GetByte(2, level3_selector, i);
@@ -49,27 +49,38 @@ std::vector<u8> DPFSContainer::GetLevel3Data() const {
}
DataContainer::DataContainer(std::vector<u8> data_) : data(std::move(data_)) {
ASSERT_MSG(data.size() >= 0x200, "Data size is too small");
if (data.size() < 0x200) {
LOG_ERROR(Core, "Data size {:X} is too small", data.size());
is_good = false;
return;
}
u32_le magic;
std::memcpy(&magic, data.data() + 0x100, sizeof(u32_le));
if (magic == MakeMagic('D', 'I', 'S', 'A')) {
InitAsDISA();
is_good = InitAsDISA();
} else if (magic == MakeMagic('D', 'I', 'F', 'F')) {
InitAsDIFF();
is_good = InitAsDIFF();
} else {
// TODO: Add error handling
UNREACHABLE_MSG("Unknown magic");
LOG_ERROR(Core, "Unknown magic 0x{:08x}", magic);
is_good = false;
}
}
DataContainer::~DataContainer() = default;
void DataContainer::InitAsDISA() {
bool DataContainer::IsGood() const {
return is_good;
}
bool DataContainer::InitAsDISA() {
DISAHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x40000, "DISA Version is not correct");
if (header.version != 0x40000) {
LOG_ERROR(Core, "DISA Version {:x} is not correct", header.version);
return false;
}
if (header.active_partition_table == 0) { // primary
partition_table_offset = header.primary_partition_table_offset;
@@ -86,13 +97,18 @@ void DataContainer::InitAsDISA() {
partition_descriptors = {header.partition_descriptors[0]};
partitions = {header.partitions[0]};
}
return true;
}
void DataContainer::InitAsDIFF() {
bool DataContainer::InitAsDIFF() {
DIFFHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x30000, "DIFF Version is not correct");
if (header.version != 0x30000) {
LOG_ERROR(Core, "DIFF Version {:x} is not correct", header.version);
return false;
}
if (header.active_partition_table == 0) { // primary
partition_table_offset = header.primary_partition_table_offset;
@@ -103,6 +119,8 @@ void DataContainer::InitAsDIFF() {
partition_count = 1;
partition_descriptors = {{/* offset */ 0, /* size */ header.partition_table_size}};
partitions = {header.partition_A};
return true;
}
std::vector<u8> DataContainer::GetPartitionData(u8 index) const {
@@ -132,11 +150,12 @@ std::vector<u8> DataContainer::GetPartitionData(u8 index) const {
std::memcpy(&dpfs_descriptor, data.data() + partition_descriptor_offset + difi.dpfs.offset,
sizeof(dpfs_descriptor));
std::vector<u32> partition_data(partitions[index].size / 4);
std::vector<u32_le> partition_data(partitions[index].size / 4);
std::memcpy(partition_data.data(), data.data() + partitions[index].offset,
partitions[index].size);
DPFSContainer dpfs_container(dpfs_descriptor, difi.dpfs_level1_selector, partition_data);
DPFSContainer dpfs_container(dpfs_descriptor, difi.dpfs_level1_selector,
std::move(partition_data));
auto ivfc_data = dpfs_container.GetLevel3Data();
std::vector<u8> result(ivfc_data.data() + ivfc_descriptor.levels[3].offset,
+5 -2
View File
@@ -116,13 +116,16 @@ public:
/// Unwraps the whole container, returning the data in IVFC Level 4 of all partitions.
std::vector<std::vector<u8>> GetIVFCLevel4Data() const;
bool IsGood() const;
private:
void InitAsDISA();
void InitAsDIFF();
bool InitAsDISA();
bool InitAsDIFF();
/// Unwraps the whole container, returning the data in IVFC Level 4 of a partition.
std::vector<u8> GetPartitionData(u8 index) const;
bool is_good = false;
std::vector<u8> data;
u32 partition_count;
u64_le partition_table_offset;
+4
View File
@@ -54,6 +54,10 @@ bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source,
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
if (!FileUtil::CreateFullPath(destination)) {
return false;
}
std::string absolute_source = root_folder + source;
try {
CryptoPP::FileSource(absolute_source.c_str(), true,
+70 -9
View File
@@ -6,6 +6,7 @@
#include "common/assert.h"
#include "common/common_paths.h"
#include "common/file_util.h"
#include "core/data_container.h"
#include "core/decryptor.h"
#include "core/importer.h"
#include "core/inner_fat.h"
@@ -89,7 +90,13 @@ bool SDMCImporter::ImportTitle(u64 id) {
bool SDMCImporter::ImportSavegame(u64 id) {
const auto path = fmt::format("title/{:08x}/{:08x}/data/", (id >> 32), (id & 0xFFFFFFFF));
SDSavegame save(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
DataContainer container(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
if (!container.IsGood()) {
return false;
}
SDSavegame save(std::move(container.GetIVFCLevel4Data()));
if (!save.IsGood()) {
return false;
}
@@ -116,6 +123,9 @@ bool SDMCImporter::ImportSysdata(u64 id) {
case 0: { // boot9.bin
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
LOG_INFO(Core, "Copying {} from {} to {}", BOOTROM9, config.bootrom_path, target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.bootrom_path, target_path);
}
case 1: { // safe mode firm
@@ -129,22 +139,31 @@ bool SDMCImporter::ImportSysdata(u64 id) {
real_path = config.safe_mode_firm_path + "old/";
}
return FileUtil::ForeachDirectoryEntry(
nullptr, config.safe_mode_firm_path,
nullptr, real_path,
[is_new_3ds](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
if (FileUtil::IsDirectory(directory + virtual_name)) {
return true;
}
return FileUtil::Copy(
directory + virtual_name,
const auto target_path =
fmt::format("{}00000000000000000000000000000000/title/00040138/{}/content/{}",
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir),
(is_new_3ds ? "20000003" : "00000003"), virtual_name));
(is_new_3ds ? "20000003" : "00000003"), virtual_name);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(directory + virtual_name, target_path);
});
}
case 2: { // seed db
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB;
LOG_INFO(Core, "Copying {} from {} to {}", SEED_DB, config.seed_db_path, target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.seed_db_path, target_path);
}
case 3: { // secret sector
@@ -152,8 +171,23 @@ bool SDMCImporter::ImportSysdata(u64 id) {
FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SECRET_SECTOR;
LOG_INFO(Core, "Copying {} from {} to {}", SECRET_SECTOR, config.secret_sector_path,
target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.secret_sector_path, target_path);
}
case 4: { // aes_keys.txt
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS;
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
FileUtil::IOFile file(target_path, "w");
if (!file) {
return false;
}
file.WriteString("slot0x25KeyX=" + Key::KeyToString(Key::GetKeyX(0x25)) + "\n");
return true;
}
default:
UNREACHABLE_MSG("Unexpected sysdata id {}", id);
}
@@ -167,17 +201,25 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
return content_list;
}
// Regex for half Title IDs
static const std::regex title_regex{"[0-9a-f]{8}"};
void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
const auto ProcessDirectory = [&out, &sdmc_path = config.sdmc_path](ContentType type,
u64 high_id) {
const auto ProcessDirectory = [& decryptor = this->decryptor, &out,
&sdmc_path = config.sdmc_path](ContentType type, u64 high_id) {
FileUtil::ForeachDirectoryEntry(
nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id),
[type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
[&decryptor, type, high_id, &out](u64* /*num_entries_out*/,
const std::string& directory,
const std::string& virtual_name) {
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
return true;
}
if (!std::regex_match(virtual_name, title_regex)) {
return true;
}
const u64 id = (high_id << 32) + std::stoull(virtual_name, nullptr, 16);
const auto citra_path = fmt::format(
"{}Nintendo "
@@ -194,6 +236,15 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
return true;
}
if (FileUtil::Exists(directory + virtual_name + "/data/")) {
// Savegames can be uninitialized.
// TODO: Is there a better way of checking this other than performing the
// decryption? (Very costy)
DataContainer container(decryptor->DecryptFile(
fmt::format("/title/{:08x}/{}/data/00000001.sav", high_id, virtual_name)));
if (!container.IsGood()) {
return true;
}
out.push_back(
{ContentType::Savegame, id, FileUtil::Exists(citra_path + "data/"),
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/data/")});
@@ -216,6 +267,10 @@ void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
return true;
}
if (!std::regex_match(virtual_name, title_regex)) {
return true;
}
const u64 id = std::stoull(virtual_name, nullptr, 16);
const auto citra_path =
fmt::format("{}Nintendo "
@@ -240,6 +295,12 @@ void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
CHECK_CONTENT(0, config.bootrom_path, sysdata_path + BOOTROM9, BOOTROM9);
CHECK_CONTENT(2, config.seed_db_path, sysdata_path + SEED_DB, SEED_DB);
CHECK_CONTENT(3, config.secret_sector_path, sysdata_path + SECRET_SECTOR, SECRET_SECTOR);
if (!config.bootrom_path.empty()) {
// 47 bytes = "slot0x26KeyX=<32>\r\n" is only for Windows,
// but it's maximum_size so probably okay
out.push_back(
{ContentType::Sysdata, 4, FileUtil::Exists(sysdata_path + AES_KEYS), 47, AES_KEYS});
}
}
#undef CHECK_CONTENT
+2
View File
@@ -55,6 +55,8 @@ struct Config {
std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1)
std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2)
std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3)
// Sysdata 4 is aes_keys.db (slot0x25KeyX)
};
class SDMCImporter {
+31 -15
View File
@@ -74,11 +74,17 @@ bool InnerFAT::WriteMetadata(const std::string& path) const {
return true;
}
SDSavegame::SDSavegame(std::vector<u8> data_) : duplicate_data(true), data(std::move(data_)) {}
SDSavegame::SDSavegame(std::vector<u8> partitionA_, std::vector<u8> partitionB_)
: duplicate_data(false), partitionA(std::move(partitionA_)),
partitionB(std::move(partitionB_)) {
SDSavegame::SDSavegame(std::vector<std::vector<u8>> 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();
}
@@ -151,9 +157,9 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
std::array<char, 17> 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 name = name_data.data();
FileUtil::IOFile file(path + name, "wb");
if (!file.IsOpen()) {
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path + name);
return false;
}
@@ -163,6 +169,7 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
return true;
}
u64 file_size = entry.file_size;
while (true) {
// Entry index is block index + 1
auto block_data = fat[block + 1];
@@ -172,14 +179,16 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
last_block = fat[block + 2].v.index - 1;
}
std::size_t size = fs_info.data_region_block_size * (last_block - block + 1);
if (file.WriteBytes(data_region.data() + fs_info.data_region_block_size * block, size) !=
size) {
const std::size_t size = fs_info.data_region_block_size * (last_block - block + 1);
const std::size_t to_write = std::min(file_size, size);
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) // last node
if (block_data.v.index == 0 || file_size == 0) // last node
break;
block = block_data.v.index - 1;
@@ -209,7 +218,7 @@ 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 */ 134328448,
ArchiveFormatInfo format_info = {/* total_size */ 0x40000,
/* number_directories */ fs_info.maximum_directory_count,
/* number_files */ fs_info.maximum_file_count,
/* duplicate_data */ duplicate_data};
@@ -236,7 +245,10 @@ bool SDExtdata::Init() {
LOG_ERROR(Core, "Failed to load or decrypt VSXE");
return false;
}
DataContainer vsxe_container(vsxe_raw);
DataContainer vsxe_container(std::move(vsxe_raw));
if (!vsxe_container.IsGood()) {
return false;
}
auto vsxe = vsxe_container.GetIVFCLevel4Data()[0];
// Read header
@@ -298,7 +310,7 @@ bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const {
std::array<char, 17> 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 name = name_data.data();
FileUtil::IOFile file(path + name, "wb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path + name);
@@ -317,7 +329,11 @@ bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const {
return true;
}
DataContainer container(container_data);
DataContainer container(std::move(container_data));
if (!container.IsGood()) {
return false;
}
auto data = container.GetIVFCLevel4Data()[0];
if (file.WriteBytes(data.data(), data.size()) != data.size()) {
LOG_ERROR(Core, "Write data failed (file: {})", path + name);
+1 -2
View File
@@ -156,8 +156,7 @@ protected:
class SDSavegame : public InnerFAT {
public:
explicit SDSavegame(std::vector<u8> data);
explicit SDSavegame(std::vector<u8> partitionA, std::vector<u8> partitionB);
explicit SDSavegame(std::vector<std::vector<u8>> partitions);
~SDSavegame() override;
bool Extract(std::string path) const override;
+22 -4
View File
@@ -82,14 +82,15 @@ struct KeySlot {
std::array<KeySlot, KeySlotID::MaxKeySlotID> key_slots;
std::array<std::optional<AESKey>, 6> common_key_y_slots;
std::string KeyToString(AESKey& key) {
} // namespace
std::string KeyToString(const AESKey& key) {
std::string s;
for (auto pos : key) {
s += fmt::format("{:02X}", pos);
}
return s;
}
} // namespace
void LoadBootromKeys(const std::string& path) {
constexpr std::array<KeyDesc, 80> keys = {
@@ -160,6 +161,18 @@ void LoadBootromKeys(const std::string& path) {
break;
}
}
// HACK: "Dump" 0x25 KeyX
// TODO: Is this legal?
constexpr std::array<u64, 16> offsets{{0x138A, 0xCAB, 0xD07, 0x3004, 0x2C, 0x49, 0xE6, 0x146E,
0x1126, 0xD0, 0x85C, 0x47, 0x70A, 0x112C, 0x808, 0x89}};
for (std::size_t i = 0; i < offsets.size(); ++i) {
file.Seek(offsets[i], SEEK_SET);
file.ReadBytes(&new_key[i], 1);
}
LOG_DEBUG(Key, "Loaded Slot0x25 KeyX: {}", KeyToString(new_key));
SetKeyX(0x25, new_key);
}
void LoadMovableSedKeys(const std::string& path) {
@@ -174,7 +187,7 @@ void LoadMovableSedKeys(const std::string& path) {
return;
}
constexpr std::size_t KEY_SECTION_START = 0x118;
constexpr std::size_t KEY_SECTION_START = 0x110;
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
AESKey key;
@@ -184,7 +197,8 @@ void LoadMovableSedKeys(const std::string& path) {
return;
}
SetKeyY(0x26, key);
LOG_DEBUG(Key, "Loaded Slot0x34KeyY: {}", KeyToString(key));
SetKeyY(0x34, key);
}
void ClearKeys() {
@@ -212,6 +226,10 @@ AESKey GetNormalKey(std::size_t slot_id) {
return key_slots.at(slot_id).normal.value_or(AESKey{});
}
AESKey GetKeyX(std::size_t slot_id) {
return key_slots.at(slot_id).x.value_or(AESKey{});
}
void SelectCommonKeyIndex(u8 index) {
key_slots[KeySlotID::TicketCommonKey].SetKeyY(common_key_y_slots.at(index));
}
+5
View File
@@ -56,6 +56,8 @@ constexpr std::size_t AES_BLOCK_SIZE = 16;
using AESKey = std::array<u8, AES_BLOCK_SIZE>;
std::string KeyToString(const AESKey& key);
void LoadBootromKeys(const std::string& path);
void LoadMovableSedKeys(const std::string& path);
void ClearKeys();
@@ -67,6 +69,9 @@ void SetNormalKey(std::size_t slot_id, const AESKey& key);
bool IsNormalKeyAvailable(std::size_t slot_id);
AESKey GetNormalKey(std::size_t slot_id);
// For importing aes_keys.txt
AESKey GetKeyX(std::size_t slot_id);
void SelectCommonKeyIndex(u8 index);
} // namespace Core::Key
+2
View File
@@ -10,6 +10,8 @@ add_executable(threeSD
import_dialog.cpp
import_dialog.h
import_dialog.ui
import_job.cpp
import_job.h
main.cpp
main.h
main.ui
+148 -18
View File
@@ -4,12 +4,16 @@
#include <cmath>
#include <QCheckBox>
#include <QFutureWatcher>
#include <QMessageBox>
#include <QProgressDialog>
#include <QPushButton>
#include <QStorageInfo>
#include <QtConcurrent/QtConcurrentRun>
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "frontend/import_dialog.h"
#include "frontend/import_job.h"
#include "ui_import_dialog.h"
QString ReadableByteSize(qulonglong size) {
@@ -25,10 +29,30 @@ QString ReadableByteSize(qulonglong size) {
.arg(QObject::tr(units[digit_groups], "ImportDialog"));
}
static const std::map<Core::ContentType, const char*> ContentTypeMap{
{Core::ContentType::Application, QT_TR_NOOP("Application")},
{Core::ContentType::Update, QT_TR_NOOP("Update")},
{Core::ContentType::DLC, QT_TR_NOOP("DLC")},
{Core::ContentType::Savegame, QT_TR_NOOP("Save Data")},
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data")},
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data")},
};
QString GetContentName(const Core::ContentSpecifier& specifier) {
return specifier.name.empty()
? QStringLiteral("0x%1 (%2)")
.arg(specifier.id, 16, 16, QLatin1Char('0'))
.arg(QObject::tr(ContentTypeMap.at(specifier.type), "ImportDialog"))
: QString::fromStdString(specifier.name);
}
ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
: QDialog(parent), ui(std::make_unique<Ui::ImportDialog>()), user_path(config.user_path),
importer(config) {
qRegisterMetaType<u64>("u64");
qRegisterMetaType<Core::ContentSpecifier>();
ui->setupUi(this);
if (!importer.IsGood()) {
QMessageBox::critical(
@@ -37,29 +61,57 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
reject();
}
PopulateContent();
ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh"));
connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) {
StartImporting();
} else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Cancel)) {
reject();
} else {
RelistContent();
}
});
RelistContent();
UpdateSizeDisplay();
// Set up column widths
ui->main->setColumnWidth(0, width() / 8);
ui->main->setColumnWidth(1, width() / 2);
ui->main->setColumnWidth(2, width() / 6);
ui->main->setColumnWidth(3, width() / 10);
}
ImportDialog::~ImportDialog() = default;
void ImportDialog::PopulateContent() {
void ImportDialog::RelistContent() {
auto* dialog = new QProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setCancelButton(nullptr);
dialog->setMinimumDuration(0);
dialog->setValue(0);
using FutureWatcher = QFutureWatcher<std::vector<Core::ContentSpecifier>>;
auto* future_watcher = new FutureWatcher(this);
connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] {
dialog->hide();
RepopulateContent();
});
auto future =
QtConcurrent::run([& importer = this->importer] { return importer.ListContent(); });
future_watcher->setFuture(future);
}
void ImportDialog::RepopulateContent() {
total_size = 0;
contents = importer.ListContent();
ui->main->clear();
ui->main->setSortingEnabled(false);
const std::map<Core::ContentType, QString> content_type_map{
{Core::ContentType::Application, QStringLiteral("Application")},
{Core::ContentType::Update, QStringLiteral("Update")},
{Core::ContentType::DLC, QStringLiteral("DLC (Add-on Content)")},
{Core::ContentType::Savegame, QStringLiteral("Save Data")},
{Core::ContentType::Extdata, QStringLiteral("Extra Data")},
{Core::ContentType::Sysdata, QStringLiteral("System Data")},
};
for (const auto& [type, name] : content_type_map) {
for (const auto& [type, name] : ContentTypeMap) {
auto* checkBox = new QCheckBox();
checkBox->setText(name);
checkBox->setText(tr(name));
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
checkBox->setTristate(true);
checkBox->setProperty("previousState", static_cast<int>(Qt::Unchecked));
@@ -96,15 +148,16 @@ void ImportDialog::PopulateContent() {
ui->main->setItemWidget(item, 0, checkBox);
}
for (const auto& content : contents) {
for (std::size_t i = 0; i < contents.size(); ++i) {
const auto& content = contents[i];
auto* checkBox = new QCheckBox();
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
// HACK: The checkbox is used to record ID. Is there a better way?
checkBox->setProperty("id", i);
auto* item = new QTreeWidgetItem{
{QString{},
content.name.empty() ? QStringLiteral("0x%1").arg(content.id, 16, 16, QLatin1Char('0'))
: QString::fromStdString(content.name),
ReadableByteSize(content.maximum_size),
{QString{}, GetContentName(content), ReadableByteSize(content.maximum_size),
content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}};
ui->main->invisibleRootItem()->child(static_cast<int>(content.type))->addChild(item);
@@ -123,6 +176,10 @@ void ImportDialog::PopulateContent() {
UpdateItemCheckState(item->parent());
}
});
if (!content.already_exists) {
checkBox->setChecked(true);
}
}
ui->main->setSortingEnabled(true);
@@ -172,3 +229,76 @@ void ImportDialog::UpdateItemCheckState(QTreeWidgetItem* item) {
}
program_trigger = false;
}
std::vector<Core::ContentSpecifier> ImportDialog::GetSelectedContentList() {
std::vector<Core::ContentSpecifier> to_import;
for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) {
const auto* item = ui->main->invisibleRootItem()->child(i);
for (int j = 0; j < item->childCount(); ++j) {
const auto* checkBox = static_cast<QCheckBox*>(ui->main->itemWidget(item->child(j), 0));
if (checkBox->isChecked()) {
to_import.emplace_back(contents[checkBox->property("id").toInt()]);
}
}
}
return to_import;
}
void ImportDialog::StartImporting() {
UpdateSizeDisplay();
if (!ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->isEnabled()) {
// Space is no longer enough
QMessageBox::warning(this, tr("Not Enough Space"),
tr("Your disk does not have enough space to hold imported data."));
return;
}
const auto& to_import = GetSelectedContentList();
const std::size_t total_count = to_import.size();
// Try to map total_size to int range
// This is equal to ceil(total_size / INT_MAX)
const u64 multiplier =
(total_size + std::numeric_limits<int>::max() - 1) / std::numeric_limits<int>::max();
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
static_cast<int>(total_size / multiplier), this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setValue(0);
auto* job = new ImportJob(this, importer, std::move(to_import));
connect(job, &ImportJob::ProgressUpdated, this,
[dialog, multiplier, total_count](u64 size_imported, u64 count,
Core::ContentSpecifier next_content) {
dialog->setValue(static_cast<int>(size_imported / multiplier));
dialog->setLabelText(tr("Importing %1 (%2/%3)...")
.arg(GetContentName(next_content))
.arg(count)
.arg(total_count));
});
connect(job, &ImportJob::ErrorOccured, this,
[this, dialog](Core::ContentSpecifier current_content) {
QMessageBox::critical(
this, tr("Error"),
tr("Failed to import content %1!").arg(GetContentName(current_content)));
dialog->hide();
});
connect(job, &ImportJob::Completed, this, [this, dialog] {
dialog->setValue(dialog->maximum());
RepopulateContent();
});
connect(dialog, &QProgressDialog::canceled, this, [this, dialog, job] {
// Add yet-another-ProgressDialog to indicate cancel progress
auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this);
cancel_dialog->setWindowModality(Qt::WindowModal);
cancel_dialog->setCancelButton(nullptr);
cancel_dialog->setMinimumDuration(0);
cancel_dialog->setValue(0);
connect(job, &ImportJob::Completed, cancel_dialog, &QProgressDialog::hide);
job->Cancel();
});
job->start();
}
+5 -2
View File
@@ -17,16 +17,19 @@ class ImportDialog;
}
class ImportDialog : public QDialog {
Q_OBJECT;
Q_OBJECT
public:
explicit ImportDialog(QWidget* parent, const Core::Config& config);
~ImportDialog() override;
private:
void PopulateContent();
void RelistContent();
void RepopulateContent();
void UpdateSizeDisplay();
void UpdateItemCheckState(QTreeWidgetItem* item);
std::vector<Core::ContentSpecifier> GetSelectedContentList();
void StartImporting();
std::unique_ptr<Ui::ImportDialog> ui;
+2 -2
View File
@@ -33,7 +33,7 @@
</column>
<column>
<property name="text">
<string>Imported</string>
<string>Exists</string>
</property>
</column>
</widget>
@@ -66,7 +66,7 @@
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Ok|QDialogButtonBox::Cancel</set>
<set>QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Reset</set>
</property>
</widget>
</item>
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "frontend/import_job.h"
#include "common/assert.h"
ImportJob::ImportJob(QObject* parent, Core::SDMCImporter& importer_,
std::vector<Core::ContentSpecifier> contents_)
: QThread(parent), importer(importer_), contents(std::move(contents_)) {}
ImportJob::~ImportJob() = default;
void ImportJob::run() {
u64 size_imported = 0, count = 0;
for (const auto& content : contents) {
emit ProgressUpdated(size_imported, count + 1, content);
if (!importer.ImportContent(content)) {
emit ErrorOccured(content);
return;
}
count++;
size_imported += content.maximum_size;
if (cancelled) {
break;
}
}
emit Completed();
}
void ImportJob::Cancel() {
cancelled.store(true);
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <atomic>
#include <QThread>
#include "core/importer.h"
class ImportJob : public QThread {
Q_OBJECT
public:
explicit ImportJob(QObject* parent, Core::SDMCImporter& importer,
std::vector<Core::ContentSpecifier> contents);
~ImportJob() override;
void run() override;
void Cancel();
signals:
void ProgressUpdated(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
void Completed();
void ErrorOccured(Core::ContentSpecifier current_content);
private:
std::atomic_bool cancelled{false};
Core::SDMCImporter& importer;
std::vector<Core::ContentSpecifier> contents;
};
Q_DECLARE_METATYPE(Core::ContentSpecifier)
+19 -2
View File
@@ -4,6 +4,7 @@
#include <string>
#include <QApplication>
#include <QMessageBox>
#include <QStorageInfo>
#include <qdevicewatcher.h>
#include "common/file_util.h"
@@ -16,6 +17,11 @@
#include "common/common_paths.h"
#endif
bool IsConfigGood(const Core::Config& config) {
return !config.sdmc_path.empty() && !config.user_path.empty() &&
!config.movable_sed_path.empty() && !config.bootrom_path.empty();
}
MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique<Ui::MainDialog>()) {
ui->setupUi(this);
@@ -36,8 +42,7 @@ MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique<U
if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) {
LoadPresetConfig();
} else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) {
ImportDialog dialog(this, GetCurrentConfig());
dialog.exec();
LaunchImportDialog();
}
});
@@ -145,6 +150,18 @@ Core::Config MainDialog::GetCurrentConfig() {
}
}
void MainDialog::LaunchImportDialog() {
const auto& config = GetCurrentConfig();
if (!IsConfigGood(config)) {
QMessageBox::critical(this, tr("Incomplete Config"),
tr("Your config is missing some of the required fields."));
return;
}
ImportDialog dialog(this, config);
dialog.exec();
}
int main(int argc, char* argv[]) {
// Init settings params
QCoreApplication::setOrganizationName(QStringLiteral("zhaowenlan1779"));
+2 -1
View File
@@ -13,7 +13,7 @@ class MainDialog;
}
class MainDialog : public QDialog {
Q_OBJECT;
Q_OBJECT
public:
explicit MainDialog(QWidget* parent = nullptr);
@@ -24,6 +24,7 @@ private:
void ShowAdvanced();
void HideAdvanced();
Core::Config GetCurrentConfig();
void LaunchImportDialog();
std::vector<Core::Config> preset_config_list;
std::unique_ptr<Ui::MainDialog> ui;