mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-02 16:49:04 +00:00
Refactor InnerFAT into Savegame and Extdata
And derive TitleDB from it too.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#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 <typename T>
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<u8> 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<std::vector<u8>> data;
|
||||
if (!vsxe_container.GetIVFCLevel4Data(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Archive<Extdata>::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<std::vector<u8>> 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
|
||||
@@ -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<Extdata> {
|
||||
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<u8> 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<Extdata>;
|
||||
friend class InnerFAT<Extdata>;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
// Copyright 2019 threeSD Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <fmt/format.h>
|
||||
#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<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 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<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();
|
||||
}
|
||||
|
||||
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<std::size_t>(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<std::size_t>(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<std::size_t>(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<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();
|
||||
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<std::size_t>(fs_info.data_region_block_size) * (last_block - block + 1);
|
||||
const std::size_t to_write = std::min<std::size_t>(file_size, size);
|
||||
|
||||
if (data_region.size() <
|
||||
static_cast<std::size_t>(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<u8> 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<std::vector<u8>> 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<std::size_t>(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<std::size_t>(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<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();
|
||||
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<std::vector<u8>> 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
|
||||
@@ -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 <array>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#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<ArchiveFormatInfo> && std::is_trivial_v<ArchiveFormatInfo>,
|
||||
"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<char, 16> 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<char, 16> 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<DirectoryEntryTableEntry> directory_entry_table;
|
||||
std::vector<FileEntryTableEntry> file_entry_table;
|
||||
std::vector<u8> data_region;
|
||||
};
|
||||
|
||||
class SDSavegame : public InnerFAT {
|
||||
public:
|
||||
explicit SDSavegame(std::vector<std::vector<u8>> 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<FATNode> fat;
|
||||
bool duplicate_data; // Layout variant
|
||||
|
||||
// Temporary storage for construction data
|
||||
std::vector<u8> data;
|
||||
std::vector<u8> partitionA;
|
||||
std::vector<u8> 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<u8> 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
|
||||
@@ -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 <array>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#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<char, 16> 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<char, 16> 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 <typename Preheader>
|
||||
struct FullHeaderInternal {
|
||||
static constexpr std::size_t PreheaderSize = sizeof(Preheader);
|
||||
Preheader pre_header;
|
||||
FATHeader fat_header;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct FullHeaderInternal<void> {
|
||||
static constexpr std::size_t PreheaderSize = 0;
|
||||
FATHeader fat_header;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
template <typename Preheader>
|
||||
struct FullHeaderInternal2 {
|
||||
using Type = FullHeaderInternal<Preheader>;
|
||||
|
||||
static_assert(sizeof(Type) == sizeof(Preheader) + sizeof(FATHeader));
|
||||
static_assert(std::is_standard_layout_v<Type>);
|
||||
};
|
||||
|
||||
template <>
|
||||
struct FullHeaderInternal2<void> {
|
||||
using Type = FullHeaderInternal<void>;
|
||||
|
||||
static_assert(sizeof(Type) == sizeof(FATHeader));
|
||||
static_assert(std::is_standard_layout_v<Type>);
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <typename Preheader = void>
|
||||
using FullHeader = detail::FullHeaderInternal2<Preheader>::Type;
|
||||
|
||||
template <typename T, typename Preheader = void,
|
||||
typename DirectoryEntryType = DirectoryEntryTableEntry,
|
||||
typename FileEntryType = FileEntryTableEntry>
|
||||
class InnerFAT {
|
||||
protected:
|
||||
bool Init(std::vector<std::vector<u8>> 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<const T*>(this)->CheckMagic()) {
|
||||
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr std::size_t PreheaderSize = FullHeader<Preheader>::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<std::size_t>(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<std::size_t>(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<std::size_t>(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<u8>& 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<std::size_t>(fs_info.data_region_block_size) * (last_block - block + 1);
|
||||
const std::size_t to_write = std::min<std::size_t>(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<Preheader> header;
|
||||
FileSystemInformation fs_info;
|
||||
std::vector<DirectoryEntryType> directory_entry_table;
|
||||
std::vector<FileEntryType> file_entry_table;
|
||||
std::vector<FATNode> fat;
|
||||
std::vector<u8> 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<ArchiveFormatInfo> && std::is_trivial_v<ArchiveFormatInfo>,
|
||||
"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 <typename T>
|
||||
class Archive : protected InnerFAT<T> {
|
||||
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<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 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<char, 17> name_data = {}; // Append a null terminator
|
||||
std::memcpy(name_data.data(), file_entry.name.data(), file_entry.name.size());
|
||||
|
||||
if (!static_cast<const T*>(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
|
||||
@@ -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<std::vector<u8>> partitions) {
|
||||
is_good = Archive<Savegame>::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<u8> 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
|
||||
@@ -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<Savegame> {
|
||||
public:
|
||||
explicit Savegame(std::vector<std::vector<u8>> 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<Savegame>;
|
||||
friend class InnerFAT<Savegame>;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
+24
-62
@@ -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<u8> 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<u8> 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<std::size_t>(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<std::size_t>(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<u32_le*>(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<u8>& 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<u8> 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;
|
||||
}
|
||||
|
||||
|
||||
+23
-12
@@ -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<TitleDB, TitleDBPreheader, TitleDBDirectoryEntryTableEntry,
|
||||
TitleDBFileEntryTableEntry>;
|
||||
|
||||
class TitleDB final : public InnerFAT_TitleDB {
|
||||
public:
|
||||
explicit TitleDB(std::vector<u8> data);
|
||||
bool IsGood() const;
|
||||
@@ -65,11 +75,12 @@ public:
|
||||
|
||||
private:
|
||||
bool Init(std::vector<u8> data);
|
||||
bool LoadTitleInfo(const std::vector<u8>& data, u32 index);
|
||||
bool CheckMagic() const;
|
||||
bool LoadTitleInfo(u32 index);
|
||||
|
||||
bool is_good = false;
|
||||
TitleDBHeader header;
|
||||
std::vector<TitleDBFileEntryTableEntry> file_entry_table;
|
||||
|
||||
friend InnerFAT_TitleDB;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user