mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-04 00:38:47 +00:00
Add system archives import
This commit is contained in:
@@ -72,6 +72,8 @@ bool SDMCImporter::ImportContent(const ContentSpecifier& specifier,
|
||||
return ImportSavegame(specifier.id, callback);
|
||||
case ContentType::Extdata:
|
||||
return ImportExtdata(specifier.id, callback);
|
||||
case ContentType::SystemArchive:
|
||||
return ImportSystemArchive(specifier.id, callback);
|
||||
case ContentType::Sysdata:
|
||||
return ImportSysdata(specifier.id, callback);
|
||||
default:
|
||||
@@ -128,6 +130,45 @@ bool SDMCImporter::ImportExtdata(u64 id, [[maybe_unused]] const ProgressCallback
|
||||
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSystemArchive(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
const auto path = fmt::format("{}{:08x}/{:08x}.app", config.system_archives_path, (id >> 32),
|
||||
(id & 0xFFFFFFFF));
|
||||
FileUtil::IOFile file(path, "rb");
|
||||
if (!file) {
|
||||
LOG_ERROR(Core, "Could not open {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u8> data(file.GetSize());
|
||||
if (file.ReadBytes(data.data(), data.size()) != data.size()) {
|
||||
LOG_ERROR(Core, "Failed to read from {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& romfs = LoadSharedRomFS(data);
|
||||
|
||||
const auto target_path = fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/00000000.app.romfs",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF));
|
||||
if (!FileUtil::CreateFullPath(target_path)) {
|
||||
LOG_ERROR(Core, "Could not create path {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
FileUtil::IOFile target(target_path, "wb");
|
||||
if (!target) {
|
||||
LOG_ERROR(Core, "Could not open {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.WriteBytes(romfs.data(), romfs.size()) != romfs.size()) {
|
||||
LOG_ERROR(Core, "Failed to write to {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
@@ -207,6 +248,7 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
|
||||
std::vector<ContentSpecifier> content_list;
|
||||
ListTitle(content_list);
|
||||
ListExtdata(content_list);
|
||||
ListSystemArchive(content_list);
|
||||
ListSysdata(content_list);
|
||||
return content_list;
|
||||
}
|
||||
@@ -361,6 +403,30 @@ void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
|
||||
});
|
||||
}
|
||||
|
||||
void SDMCImporter::ListSystemArchive(std::vector<ContentSpecifier>& out) const {
|
||||
constexpr std::array<std::pair<u64, const char*>, 8> SystemArchives{{
|
||||
{0x0004009b'00010202, "Mii Data"},
|
||||
{0x0004009b'00010402, "Region Manifest"},
|
||||
{0x0004009b'00014002, "Shared Font (JPN/EUR/USA)"},
|
||||
{0x0004009b'00014102, "Shared Font (CHN)"},
|
||||
{0x0004009b'00014202, "Shared Font (KOR)"},
|
||||
{0x0004009b'00014302, "Shared Font (TWN)"},
|
||||
{0x000400db'00010302, "Bad word list"},
|
||||
}};
|
||||
|
||||
for (const auto& [id, name] : SystemArchives) {
|
||||
const auto path = fmt::format("{}{:08x}/{:08x}.app", config.system_archives_path,
|
||||
(id >> 32), (id & 0xFFFFFFFF));
|
||||
if (FileUtil::Exists(path)) {
|
||||
const auto target_path = fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF));
|
||||
out.push_back({ContentType::SystemArchive, id, FileUtil::Exists(target_path),
|
||||
FileUtil::GetSize(path), name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
|
||||
#define CHECK_CONTENT(id, var_path, citra_path, display_name) \
|
||||
if (!var_path.empty()) { \
|
||||
@@ -418,6 +484,8 @@ void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) {
|
||||
return DeleteSavegame(specifier.id);
|
||||
case ContentType::Extdata:
|
||||
return DeleteExtdata(specifier.id);
|
||||
case ContentType::SystemArchive:
|
||||
return DeleteSystemArchive(specifier.id);
|
||||
case ContentType::Sysdata:
|
||||
return DeleteSysdata(specifier.id);
|
||||
default:
|
||||
@@ -449,6 +517,12 @@ void SDMCImporter::DeleteExtdata(u64 id) const {
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSystemArchive(u64 id) const {
|
||||
FileUtil::DeleteDirRecursively(fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSysdata(u64 id) const {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
@@ -501,6 +575,7 @@ std::vector<Config> LoadPresetConfig(std::string mount_point) {
|
||||
LOAD_DATA(safe_mode_firm_path, "firm/");
|
||||
LOAD_DATA(seed_db_path, SEED_DB);
|
||||
LOAD_DATA(secret_sector_path, SECRET_SECTOR);
|
||||
LOAD_DATA(system_archives_path, "sysarchives/");
|
||||
#undef LOAD_DATA
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -23,6 +23,7 @@ enum class ContentType {
|
||||
DLC,
|
||||
Savegame,
|
||||
Extdata,
|
||||
SystemArchive,
|
||||
Sysdata,
|
||||
};
|
||||
|
||||
@@ -53,9 +54,10 @@ struct Config {
|
||||
// The following system files are optional for importing and are only copied so that Citra
|
||||
// will be able to decrypt imported encrypted ROMs.
|
||||
|
||||
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)
|
||||
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)
|
||||
std::string system_archives_path; ///< Path to system archives.
|
||||
|
||||
// Sysdata 4 is aes_keys.db (slot0x25KeyX)
|
||||
};
|
||||
@@ -108,15 +110,18 @@ private:
|
||||
bool ImportTitle(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSavegame(u64 id, const ProgressCallback& callback);
|
||||
bool ImportExtdata(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSystemArchive(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSysdata(u64 id, const ProgressCallback& callback);
|
||||
|
||||
void ListTitle(std::vector<ContentSpecifier>& out) const;
|
||||
void ListExtdata(std::vector<ContentSpecifier>& out) const;
|
||||
void ListSystemArchive(std::vector<ContentSpecifier>& out) const;
|
||||
void ListSysdata(std::vector<ContentSpecifier>& out) const;
|
||||
|
||||
void DeleteTitle(u64 id) const;
|
||||
void DeleteSavegame(u64 id) const;
|
||||
void DeleteExtdata(u64 id) const;
|
||||
void DeleteSystemArchive(u64 id) const;
|
||||
void DeleteSysdata(u64 id) const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <cryptopp/aes.h>
|
||||
#include <cryptopp/modes.h>
|
||||
#include <cryptopp/sha.h>
|
||||
#include "common/alignment.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/data_container.h"
|
||||
#include "core/key/key.h"
|
||||
#include "core/ncch/ncch_container.h"
|
||||
|
||||
@@ -304,4 +309,40 @@ bool NCCHContainer::HasExHeader() {
|
||||
return has_exheader;
|
||||
}
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct RomFSIVFCHeader {
|
||||
u32_le magic;
|
||||
u32_le version;
|
||||
u32_le master_hash_size;
|
||||
std::array<LevelDescriptor, 3> levels;
|
||||
INSERT_PADDING_BYTES(0xC);
|
||||
};
|
||||
static_assert(sizeof(RomFSIVFCHeader) == 0x60, "Size of RomFSIVFCHeader is incorrect");
|
||||
#pragma pack(pop)
|
||||
|
||||
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data) {
|
||||
NCCH_Header header;
|
||||
ASSERT_MSG(data.size() >= sizeof(header), "NCCH size is too small");
|
||||
std::memcpy(&header, data.data(), sizeof(header));
|
||||
|
||||
const std::size_t offset = header.romfs_offset * 0x200; // 0x200: Media unit
|
||||
RomFSIVFCHeader ivfc;
|
||||
ASSERT_MSG(data.size() >= offset + sizeof(ivfc), "NCCH size is too small");
|
||||
std::memcpy(&ivfc, data.data() + offset, sizeof(ivfc));
|
||||
|
||||
ASSERT_MSG(ivfc.magic == MakeMagic('I', 'V', 'F', 'C'), "IVFC magic is incorrect");
|
||||
ASSERT_MSG(ivfc.version == 0x10000, "IVFC version is incorrect");
|
||||
|
||||
std::vector<u8> result(ivfc.levels[2].size);
|
||||
|
||||
// Calculation from ctrtool
|
||||
const std::size_t data_offset =
|
||||
offset + Common::AlignUp(sizeof(ivfc) + ivfc.master_hash_size,
|
||||
std::pow(2, ivfc.levels[2].block_size));
|
||||
ASSERT_MSG(data.size() >= data_offset + ivfc.levels[2].size);
|
||||
std::memcpy(result.data(), data.data() + data_offset, ivfc.levels[2].size);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -277,4 +277,10 @@ private:
|
||||
SDMCFile exefs_file;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the shared RomFS from a NCCH image.
|
||||
* Used for handling system archives.
|
||||
*/
|
||||
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data);
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -31,12 +31,13 @@ QString ReadableByteSize(qulonglong size) {
|
||||
.arg(QObject::tr(units[digit_groups], "ImportDialog"));
|
||||
}
|
||||
|
||||
static constexpr std::array<std::pair<Core::ContentType, const char*>, 6> ContentTypeMap{{
|
||||
static constexpr std::array<std::pair<Core::ContentType, const char*>, 7> 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::SystemArchive, QT_TR_NOOP("System Archive")},
|
||||
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data")},
|
||||
}};
|
||||
|
||||
@@ -158,10 +159,20 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
||||
// HACK: The checkbox is used to record ID. Is there a better way?
|
||||
checkBox->setProperty("id", id);
|
||||
|
||||
const QString name = (row == 0 ? QStringLiteral("%1 (%2)")
|
||||
.arg(GetContentName(content))
|
||||
.arg(GetContentTypeName(content.type))
|
||||
: GetContentTypeName(content.type));
|
||||
QString name;
|
||||
if (ui->title_view_button->isChecked()) {
|
||||
if (row == 0) {
|
||||
name = QStringLiteral("%1 (%2)")
|
||||
.arg(GetContentName(content))
|
||||
.arg(GetContentTypeName(content.type));
|
||||
} else if (row <= 2) {
|
||||
name = GetContentName(content);
|
||||
} else {
|
||||
name = GetContentTypeName(content.type);
|
||||
}
|
||||
} else {
|
||||
name = GetContentName(content);
|
||||
}
|
||||
|
||||
auto* item = new QTreeWidgetItem{
|
||||
{QString{}, name, ReadableByteSize(content.maximum_size),
|
||||
@@ -205,6 +216,8 @@ void ImportDialog::RepopulateContent() {
|
||||
}
|
||||
}
|
||||
title_name_map.insert_or_assign(0, tr("Ungrouped"));
|
||||
title_name_map.insert_or_assign(1, tr("System Archive"));
|
||||
title_name_map.insert_or_assign(2, tr("System Data"));
|
||||
|
||||
std::unordered_map<u64, u64> title_row_map;
|
||||
for (const auto& [id, name] : title_name_map) {
|
||||
@@ -233,8 +246,12 @@ void ImportDialog::RepopulateContent() {
|
||||
row = title_row_map.at(real_id);
|
||||
break;
|
||||
}
|
||||
case Core::ContentType::SystemArchive: {
|
||||
row = title_row_map.at(1); // System archive
|
||||
break;
|
||||
}
|
||||
case Core::ContentType::Sysdata: {
|
||||
row = title_row_map.at(0);
|
||||
row = title_row_map.at(2); // System data
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user