mirror of
https://github.com/pound-emu/pound.git
synced 2025-12-12 10:37:00 +00:00
Add Yuzu's file system implementation, will make it compilable later
This commit is contained in:
parent
e08782b582
commit
fc057169a0
137 changed files with 94111 additions and 0 deletions
8
core/README.md
Normal file
8
core/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Assets folder
|
||||||
|
|
||||||
|
### !! The file system implementation is copied from Yuzu as the filesystem is believed to be the same betweeen both consoles
|
||||||
|
|
||||||
|
dir: /core/
|
||||||
|
|
||||||
|
This is where all of the main universal code for emulating the Switch/Switch2 goes.
|
||||||
|
|
||||||
144
core/fs/bis_factory.cpp
Normal file
144
core/fs/bis_factory.cpp
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
|
#include "core/file_sys/bis_factory.h"
|
||||||
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u64 NAND_USER_SIZE = 0x680000000; // 26624 MiB
|
||||||
|
constexpr u64 NAND_SYSTEM_SIZE = 0xA0000000; // 2560 MiB
|
||||||
|
constexpr u64 NAND_TOTAL_SIZE = 0x747C00000; // 29820 MiB
|
||||||
|
|
||||||
|
BISFactory::BISFactory(VirtualDir nand_root_, VirtualDir load_root_, VirtualDir dump_root_)
|
||||||
|
: nand_root(std::move(nand_root_)), load_root(std::move(load_root_)),
|
||||||
|
dump_root(std::move(dump_root_)),
|
||||||
|
sysnand_cache(std::make_unique<RegisteredCache>(
|
||||||
|
GetOrCreateDirectoryRelative(nand_root, "/system/Contents/registered"))),
|
||||||
|
usrnand_cache(std::make_unique<RegisteredCache>(
|
||||||
|
GetOrCreateDirectoryRelative(nand_root, "/user/Contents/registered"))),
|
||||||
|
sysnand_placeholder(std::make_unique<PlaceholderCache>(
|
||||||
|
GetOrCreateDirectoryRelative(nand_root, "/system/Contents/placehld"))),
|
||||||
|
usrnand_placeholder(std::make_unique<PlaceholderCache>(
|
||||||
|
GetOrCreateDirectoryRelative(nand_root, "/user/Contents/placehld"))) {}
|
||||||
|
|
||||||
|
BISFactory::~BISFactory() = default;
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetSystemNANDContentDirectory() const {
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/system/Contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetUserNANDContentDirectory() const {
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/user/Contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredCache* BISFactory::GetSystemNANDContents() const {
|
||||||
|
return sysnand_cache.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredCache* BISFactory::GetUserNANDContents() const {
|
||||||
|
return usrnand_cache.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceholderCache* BISFactory::GetSystemNANDPlaceholder() const {
|
||||||
|
return sysnand_placeholder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceholderCache* BISFactory::GetUserNANDPlaceholder() const {
|
||||||
|
return usrnand_placeholder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetModificationLoadRoot(u64 title_id) const {
|
||||||
|
// LayeredFS doesn't work on updates and title id-less homebrew
|
||||||
|
if (title_id == 0 || (title_id & 0xFFF) == 0x800)
|
||||||
|
return nullptr;
|
||||||
|
return GetOrCreateDirectoryRelative(load_root, fmt::format("/{:016X}", title_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetModificationDumpRoot(u64 title_id) const {
|
||||||
|
if (title_id == 0)
|
||||||
|
return nullptr;
|
||||||
|
return GetOrCreateDirectoryRelative(dump_root, fmt::format("/{:016X}", title_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::OpenPartition(BisPartitionId id) const {
|
||||||
|
switch (id) {
|
||||||
|
case BisPartitionId::CalibrationFile:
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/prodinfof");
|
||||||
|
case BisPartitionId::SafeMode:
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/safe");
|
||||||
|
case BisPartitionId::System:
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/system");
|
||||||
|
case BisPartitionId::User:
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/user");
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile BISFactory::OpenPartitionStorage(BisPartitionId id,
|
||||||
|
VirtualFilesystem file_system) const {
|
||||||
|
auto& keys = Core::Crypto::KeyManager::Instance();
|
||||||
|
Core::Crypto::PartitionDataManager pdm{file_system->OpenDirectory(
|
||||||
|
Common::FS::GetYuzuPathString(Common::FS::YuzuPath::NANDDir), OpenMode::Read)};
|
||||||
|
keys.PopulateFromPartitionData(pdm);
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case BisPartitionId::CalibrationBinary:
|
||||||
|
return pdm.GetDecryptedProdInfo();
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part1:
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part2:
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part3:
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part4:
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part5:
|
||||||
|
case BisPartitionId::BootConfigAndPackage2Part6: {
|
||||||
|
const auto new_id = static_cast<u8>(id) -
|
||||||
|
static_cast<u8>(BisPartitionId::BootConfigAndPackage2Part1) +
|
||||||
|
static_cast<u8>(Core::Crypto::Package2Type::NormalMain);
|
||||||
|
return pdm.GetPackage2Raw(static_cast<Core::Crypto::Package2Type>(new_id));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetImageDirectory() const {
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root, "/user/Album");
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BISFactory::GetSystemNANDFreeSpace() const {
|
||||||
|
const auto sys_dir = GetOrCreateDirectoryRelative(nand_root, "/system");
|
||||||
|
if (sys_dir == nullptr) {
|
||||||
|
return GetSystemNANDTotalSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetSystemNANDTotalSpace() - sys_dir->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BISFactory::GetSystemNANDTotalSpace() const {
|
||||||
|
return NAND_SYSTEM_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BISFactory::GetUserNANDFreeSpace() const {
|
||||||
|
// For some reason games such as BioShock 1 checks whether this is exactly 0x680000000 bytes.
|
||||||
|
// Set the free space to be 1 MiB less than the total as a workaround to this issue.
|
||||||
|
return GetUserNANDTotalSpace() - 0x100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BISFactory::GetUserNANDTotalSpace() const {
|
||||||
|
return NAND_USER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 BISFactory::GetFullNANDTotalSpace() const {
|
||||||
|
return NAND_TOTAL_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir BISFactory::GetBCATDirectory(u64 title_id) const {
|
||||||
|
return GetOrCreateDirectoryRelative(nand_root,
|
||||||
|
fmt::format("/system/save/bcat/{:016X}", title_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
78
core/fs/bis_factory.h
Normal file
78
core/fs/bis_factory.h
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class BisPartitionId : u32 {
|
||||||
|
UserDataRoot = 20,
|
||||||
|
CalibrationBinary = 27,
|
||||||
|
CalibrationFile = 28,
|
||||||
|
BootConfigAndPackage2Part1 = 21,
|
||||||
|
BootConfigAndPackage2Part2 = 22,
|
||||||
|
BootConfigAndPackage2Part3 = 23,
|
||||||
|
BootConfigAndPackage2Part4 = 24,
|
||||||
|
BootConfigAndPackage2Part5 = 25,
|
||||||
|
BootConfigAndPackage2Part6 = 26,
|
||||||
|
SafeMode = 29,
|
||||||
|
System = 31,
|
||||||
|
SystemProperEncryption = 32,
|
||||||
|
SystemProperPartition = 33,
|
||||||
|
User = 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
class RegisteredCache;
|
||||||
|
class PlaceholderCache;
|
||||||
|
|
||||||
|
/// File system interface to the Built-In Storage
|
||||||
|
/// This is currently missing accessors to BIS partitions, but seemed like a good place for the NAND
|
||||||
|
/// registered caches.
|
||||||
|
class BISFactory {
|
||||||
|
public:
|
||||||
|
explicit BISFactory(VirtualDir nand_root, VirtualDir load_root, VirtualDir dump_root);
|
||||||
|
~BISFactory();
|
||||||
|
|
||||||
|
VirtualDir GetSystemNANDContentDirectory() const;
|
||||||
|
VirtualDir GetUserNANDContentDirectory() const;
|
||||||
|
|
||||||
|
RegisteredCache* GetSystemNANDContents() const;
|
||||||
|
RegisteredCache* GetUserNANDContents() const;
|
||||||
|
|
||||||
|
PlaceholderCache* GetSystemNANDPlaceholder() const;
|
||||||
|
PlaceholderCache* GetUserNANDPlaceholder() const;
|
||||||
|
|
||||||
|
VirtualDir GetModificationLoadRoot(u64 title_id) const;
|
||||||
|
VirtualDir GetModificationDumpRoot(u64 title_id) const;
|
||||||
|
|
||||||
|
VirtualDir OpenPartition(BisPartitionId id) const;
|
||||||
|
VirtualFile OpenPartitionStorage(BisPartitionId id, VirtualFilesystem file_system) const;
|
||||||
|
|
||||||
|
VirtualDir GetImageDirectory() const;
|
||||||
|
|
||||||
|
u64 GetSystemNANDFreeSpace() const;
|
||||||
|
u64 GetSystemNANDTotalSpace() const;
|
||||||
|
u64 GetUserNANDFreeSpace() const;
|
||||||
|
u64 GetUserNANDTotalSpace() const;
|
||||||
|
u64 GetFullNANDTotalSpace() const;
|
||||||
|
|
||||||
|
VirtualDir GetBCATDirectory(u64 title_id) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualDir nand_root;
|
||||||
|
VirtualDir load_root;
|
||||||
|
VirtualDir dump_root;
|
||||||
|
|
||||||
|
std::unique_ptr<RegisteredCache> sysnand_cache;
|
||||||
|
std::unique_ptr<RegisteredCache> usrnand_cache;
|
||||||
|
|
||||||
|
std::unique_ptr<PlaceholderCache> sysnand_placeholder;
|
||||||
|
std::unique_ptr<PlaceholderCache> usrnand_placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
356
core/fs/card_image.cpp
Normal file
356
core/fs/card_image.cpp
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <fmt/ostream.h>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/card_image.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/partition_filesystem.h"
|
||||||
|
#include "core/file_sys/submission_package.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u64 GAMECARD_CERTIFICATE_OFFSET = 0x7000;
|
||||||
|
constexpr std::array partition_names{
|
||||||
|
"update",
|
||||||
|
"normal",
|
||||||
|
"secure",
|
||||||
|
"logo",
|
||||||
|
};
|
||||||
|
|
||||||
|
XCI::XCI(VirtualFile file_, u64 program_id, size_t program_index)
|
||||||
|
: file(std::move(file_)), program_nca_status{Loader::ResultStatus::ErrorXCIMissingProgramNCA},
|
||||||
|
partitions(partition_names.size()),
|
||||||
|
partitions_raw(partition_names.size()), keys{Core::Crypto::KeyManager::Instance()} {
|
||||||
|
const auto header_status = TryReadHeader();
|
||||||
|
if (header_status != Loader::ResultStatus::Success) {
|
||||||
|
status = header_status;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartitionFilesystem main_hfs(std::make_shared<OffsetVfsFile>(
|
||||||
|
file, file->GetSize() - header.hfs_offset, header.hfs_offset));
|
||||||
|
|
||||||
|
update_normal_partition_end = main_hfs.GetFileOffsets()["secure"];
|
||||||
|
|
||||||
|
if (main_hfs.GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
status = main_hfs.GetStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (XCIPartition partition :
|
||||||
|
{XCIPartition::Update, XCIPartition::Normal, XCIPartition::Secure, XCIPartition::Logo}) {
|
||||||
|
const auto partition_idx = static_cast<std::size_t>(partition);
|
||||||
|
auto raw = main_hfs.GetFile(partition_names[partition_idx]);
|
||||||
|
|
||||||
|
partitions_raw[static_cast<std::size_t>(partition)] = std::move(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
secure_partition = std::make_shared<NSP>(
|
||||||
|
main_hfs.GetFile(partition_names[static_cast<std::size_t>(XCIPartition::Secure)]),
|
||||||
|
program_id, program_index);
|
||||||
|
|
||||||
|
ncas = secure_partition->GetNCAsCollapsed();
|
||||||
|
program =
|
||||||
|
secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program);
|
||||||
|
program_nca_status = secure_partition->GetProgramStatus();
|
||||||
|
if (program_nca_status == Loader::ResultStatus::ErrorNSPMissingProgramNCA) {
|
||||||
|
program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = AddNCAFromPartition(XCIPartition::Normal);
|
||||||
|
if (result != Loader::ResultStatus::Success) {
|
||||||
|
status = result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetFormatVersion() >= 0x2) {
|
||||||
|
result = AddNCAFromPartition(XCIPartition::Logo);
|
||||||
|
if (result != Loader::ResultStatus::Success) {
|
||||||
|
status = result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status = Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
XCI::~XCI() = default;
|
||||||
|
|
||||||
|
Loader::ResultStatus XCI::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus XCI::GetProgramNCAStatus() const {
|
||||||
|
return program_nca_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetPartition(XCIPartition partition) {
|
||||||
|
const auto id = static_cast<std::size_t>(partition);
|
||||||
|
if (partitions[id] == nullptr && partitions_raw[id] != nullptr) {
|
||||||
|
partitions[id] = std::make_shared<PartitionFilesystem>(partitions_raw[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return partitions[static_cast<std::size_t>(partition)];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualDir> XCI::GetPartitions() {
|
||||||
|
std::vector<VirtualDir> out;
|
||||||
|
for (const auto& id :
|
||||||
|
{XCIPartition::Update, XCIPartition::Normal, XCIPartition::Secure, XCIPartition::Logo}) {
|
||||||
|
const auto part = GetPartition(id);
|
||||||
|
if (part != nullptr) {
|
||||||
|
out.push_back(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<NSP> XCI::GetSecurePartitionNSP() const {
|
||||||
|
return secure_partition;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetSecurePartition() {
|
||||||
|
return GetPartition(XCIPartition::Secure);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetNormalPartition() {
|
||||||
|
return GetPartition(XCIPartition::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetUpdatePartition() {
|
||||||
|
return GetPartition(XCIPartition::Update);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetLogoPartition() {
|
||||||
|
return GetPartition(XCIPartition::Logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetPartitionRaw(XCIPartition partition) const {
|
||||||
|
return partitions_raw[static_cast<std::size_t>(partition)];
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetSecurePartitionRaw() const {
|
||||||
|
return GetPartitionRaw(XCIPartition::Secure);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetStoragePartition0() const {
|
||||||
|
return std::make_shared<OffsetVfsFile>(file, update_normal_partition_end, 0, "partition0");
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetStoragePartition1() const {
|
||||||
|
return std::make_shared<OffsetVfsFile>(file, file->GetSize() - update_normal_partition_end,
|
||||||
|
update_normal_partition_end, "partition1");
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetNormalPartitionRaw() const {
|
||||||
|
return GetPartitionRaw(XCIPartition::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetUpdatePartitionRaw() const {
|
||||||
|
return GetPartitionRaw(XCIPartition::Update);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetLogoPartitionRaw() const {
|
||||||
|
return GetPartitionRaw(XCIPartition::Logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 XCI::GetProgramTitleID() const {
|
||||||
|
return secure_partition->GetProgramTitleID();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u64> XCI::GetProgramTitleIDs() const {
|
||||||
|
return secure_partition->GetProgramTitleIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 XCI::GetSystemUpdateVersion() {
|
||||||
|
const auto update = GetPartition(XCIPartition::Update);
|
||||||
|
if (update == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& update_file : update->GetFiles()) {
|
||||||
|
NCA nca{update_file};
|
||||||
|
|
||||||
|
if (nca.GetStatus() != Loader::ResultStatus::Success || nca.GetSubdirectories().empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.GetType() == NCAContentType::Meta && nca.GetTitleId() == 0x0100000000000816) {
|
||||||
|
const auto dir = nca.GetSubdirectories()[0];
|
||||||
|
const auto cnmt = dir->GetFile("SystemUpdate_0100000000000816.cnmt");
|
||||||
|
if (cnmt == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CNMT cnmt_data{cnmt};
|
||||||
|
|
||||||
|
const auto metas = cnmt_data.GetMetaRecords();
|
||||||
|
if (metas.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metas[0].title_version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 XCI::GetSystemUpdateTitleID() const {
|
||||||
|
return 0x0100000000000816;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XCI::HasProgramNCA() const {
|
||||||
|
return program != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetProgramNCAFile() const {
|
||||||
|
if (!HasProgramNCA()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return program->GetBaseFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::shared_ptr<NCA>>& XCI::GetNCAs() const {
|
||||||
|
return ncas;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
|
||||||
|
const auto program_id = secure_partition->GetProgramTitleID();
|
||||||
|
const auto iter =
|
||||||
|
std::find_if(ncas.begin(), ncas.end(), [type, program_id](const std::shared_ptr<NCA>& nca) {
|
||||||
|
return nca->GetType() == type && nca->GetTitleId() == program_id;
|
||||||
|
});
|
||||||
|
return iter == ncas.end() ? nullptr : *iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile XCI::GetNCAFileByType(NCAContentType type) const {
|
||||||
|
auto nca = GetNCAByType(type);
|
||||||
|
if (nca != nullptr) {
|
||||||
|
return nca->GetBaseFile();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualFile> XCI::GetFiles() const {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualDir> XCI::GetSubdirectories() const {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string XCI::GetName() const {
|
||||||
|
return file->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::GetParentDirectory() const {
|
||||||
|
return file->GetContainingDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir XCI::ConcatenatedPseudoDirectory() {
|
||||||
|
const auto out = std::make_shared<VectorVfsDirectory>();
|
||||||
|
for (const auto& part_id : {XCIPartition::Normal, XCIPartition::Logo, XCIPartition::Secure}) {
|
||||||
|
const auto& part = GetPartition(part_id);
|
||||||
|
if (part == nullptr)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (const auto& part_file : part->GetFiles())
|
||||||
|
out->AddFile(part_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<u8, 0x200> XCI::GetCertificate() const {
|
||||||
|
std::array<u8, 0x200> out;
|
||||||
|
file->Read(out.data(), out.size(), GAMECARD_CERTIFICATE_OFFSET);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
|
||||||
|
const auto partition_index = static_cast<std::size_t>(part);
|
||||||
|
const auto partition = GetPartition(part);
|
||||||
|
|
||||||
|
if (partition == nullptr) {
|
||||||
|
return Loader::ResultStatus::ErrorXCIMissingPartition;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const VirtualFile& partition_file : partition->GetFiles()) {
|
||||||
|
if (partition_file->GetExtension() != "nca") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nca = std::make_shared<NCA>(partition_file);
|
||||||
|
if (nca->IsUpdate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nca->GetType() == NCAContentType::Program) {
|
||||||
|
program_nca_status = nca->GetStatus();
|
||||||
|
}
|
||||||
|
if (nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
ncas.push_back(std::move(nca));
|
||||||
|
} else {
|
||||||
|
const u16 error_id = static_cast<u16>(nca->GetStatus());
|
||||||
|
LOG_CRITICAL(Loader, "Could not load NCA {}/{}, failed with error code {:04X} ({})",
|
||||||
|
partition_names[partition_index], nca->GetName(), error_id,
|
||||||
|
nca->GetStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus XCI::TryReadHeader() {
|
||||||
|
constexpr size_t CardInitialDataRegionSize = 0x1000;
|
||||||
|
|
||||||
|
// Define the function we'll use to determine if we read a valid header.
|
||||||
|
const auto ReadCardHeader = [&]() {
|
||||||
|
// Ensure we can read the entire header. If we can't, we can't read the card image.
|
||||||
|
if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the header magic matches. If it doesn't, this isn't a card image header.
|
||||||
|
if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
|
||||||
|
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We read a card image header.
|
||||||
|
return Loader::ResultStatus::Success;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to read the header directly.
|
||||||
|
if (ReadCardHeader() == Loader::ResultStatus::Success) {
|
||||||
|
return Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the size of the file.
|
||||||
|
const size_t card_image_size = file->GetSize();
|
||||||
|
|
||||||
|
// If we are large enough to have a key area, offset past the key area and retry.
|
||||||
|
if (card_image_size >= CardInitialDataRegionSize) {
|
||||||
|
file = std::make_shared<OffsetVfsFile>(file, card_image_size - CardInitialDataRegionSize,
|
||||||
|
CardInitialDataRegionSize);
|
||||||
|
return ReadCardHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We had no header and aren't large enough to have a key area, so this can't be parsed.
|
||||||
|
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 XCI::GetFormatVersion() {
|
||||||
|
return GetLogoPartition() == nullptr ? 0x1 : 0x2;
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
149
core/fs/card_image.h
Normal file
149
core/fs/card_image.h
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace Core::Crypto {
|
||||||
|
class KeyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class NCA;
|
||||||
|
enum class NCAContentType : u8;
|
||||||
|
class NSP;
|
||||||
|
|
||||||
|
enum class GamecardSize : u8 {
|
||||||
|
S_1GB = 0xFA,
|
||||||
|
S_2GB = 0xF8,
|
||||||
|
S_4GB = 0xF0,
|
||||||
|
S_8GB = 0xE0,
|
||||||
|
S_16GB = 0xE1,
|
||||||
|
S_32GB = 0xE2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GamecardInfo {
|
||||||
|
u64_le firmware_version;
|
||||||
|
u32_le access_control_flags;
|
||||||
|
u32_le read_wait_time1;
|
||||||
|
u32_le read_wait_time2;
|
||||||
|
u32_le write_wait_time1;
|
||||||
|
u32_le write_wait_time2;
|
||||||
|
u32_le firmware_mode;
|
||||||
|
u32_le cup_version;
|
||||||
|
std::array<u8, 4> reserved1;
|
||||||
|
u64_le update_partition_hash;
|
||||||
|
u64_le cup_id;
|
||||||
|
std::array<u8, 0x38> reserved2;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GamecardInfo) == 0x70, "GamecardInfo has incorrect size.");
|
||||||
|
|
||||||
|
struct GamecardHeader {
|
||||||
|
std::array<u8, 0x100> signature;
|
||||||
|
u32_le magic;
|
||||||
|
u32_le secure_area_start;
|
||||||
|
u32_le backup_area_start;
|
||||||
|
u8 kek_index;
|
||||||
|
GamecardSize size;
|
||||||
|
u8 header_version;
|
||||||
|
u8 flags;
|
||||||
|
u64_le package_id;
|
||||||
|
u64_le valid_data_end;
|
||||||
|
u128 info_iv;
|
||||||
|
u64_le hfs_offset;
|
||||||
|
u64_le hfs_size;
|
||||||
|
std::array<u8, 0x20> hfs_header_hash;
|
||||||
|
std::array<u8, 0x20> initial_data_hash;
|
||||||
|
u32_le secure_mode_flag;
|
||||||
|
u32_le title_key_flag;
|
||||||
|
u32_le key_flag;
|
||||||
|
u32_le normal_area_end;
|
||||||
|
GamecardInfo info;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GamecardHeader) == 0x200, "GamecardHeader has incorrect size.");
|
||||||
|
|
||||||
|
enum class XCIPartition : u8 { Update, Normal, Secure, Logo };
|
||||||
|
|
||||||
|
class XCI : public ReadOnlyVfsDirectory {
|
||||||
|
public:
|
||||||
|
explicit XCI(VirtualFile file, u64 program_id = 0, size_t program_index = 0);
|
||||||
|
~XCI() override;
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
Loader::ResultStatus GetProgramNCAStatus() const;
|
||||||
|
|
||||||
|
u8 GetFormatVersion();
|
||||||
|
|
||||||
|
VirtualDir GetPartition(XCIPartition partition);
|
||||||
|
std::vector<VirtualDir> GetPartitions();
|
||||||
|
|
||||||
|
std::shared_ptr<NSP> GetSecurePartitionNSP() const;
|
||||||
|
VirtualDir GetSecurePartition();
|
||||||
|
VirtualDir GetNormalPartition();
|
||||||
|
VirtualDir GetUpdatePartition();
|
||||||
|
VirtualDir GetLogoPartition();
|
||||||
|
|
||||||
|
VirtualFile GetPartitionRaw(XCIPartition partition) const;
|
||||||
|
VirtualFile GetSecurePartitionRaw() const;
|
||||||
|
VirtualFile GetStoragePartition0() const;
|
||||||
|
VirtualFile GetStoragePartition1() const;
|
||||||
|
VirtualFile GetNormalPartitionRaw() const;
|
||||||
|
VirtualFile GetUpdatePartitionRaw() const;
|
||||||
|
VirtualFile GetLogoPartitionRaw() const;
|
||||||
|
|
||||||
|
u64 GetProgramTitleID() const;
|
||||||
|
std::vector<u64> GetProgramTitleIDs() const;
|
||||||
|
u32 GetSystemUpdateVersion();
|
||||||
|
u64 GetSystemUpdateTitleID() const;
|
||||||
|
|
||||||
|
bool HasProgramNCA() const;
|
||||||
|
VirtualFile GetProgramNCAFile() const;
|
||||||
|
const std::vector<std::shared_ptr<NCA>>& GetNCAs() const;
|
||||||
|
std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
|
||||||
|
VirtualFile GetNCAFileByType(NCAContentType type) const;
|
||||||
|
|
||||||
|
std::vector<VirtualFile> GetFiles() const override;
|
||||||
|
|
||||||
|
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||||
|
|
||||||
|
std::string GetName() const override;
|
||||||
|
|
||||||
|
VirtualDir GetParentDirectory() const override;
|
||||||
|
|
||||||
|
// Creates a directory that contains all the NCAs in the gamecard
|
||||||
|
VirtualDir ConcatenatedPseudoDirectory();
|
||||||
|
|
||||||
|
std::array<u8, 0x200> GetCertificate() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Loader::ResultStatus AddNCAFromPartition(XCIPartition part);
|
||||||
|
Loader::ResultStatus TryReadHeader();
|
||||||
|
|
||||||
|
VirtualFile file;
|
||||||
|
GamecardHeader header{};
|
||||||
|
|
||||||
|
Loader::ResultStatus status;
|
||||||
|
Loader::ResultStatus program_nca_status;
|
||||||
|
|
||||||
|
std::vector<VirtualDir> partitions;
|
||||||
|
std::vector<VirtualFile> partitions_raw;
|
||||||
|
std::shared_ptr<NSP> secure_partition;
|
||||||
|
std::shared_ptr<NCA> program;
|
||||||
|
std::vector<std::shared_ptr<NCA>> ncas;
|
||||||
|
|
||||||
|
u64 update_normal_partition_end;
|
||||||
|
|
||||||
|
Core::Crypto::KeyManager& keys;
|
||||||
|
};
|
||||||
|
} // namespace FileSys
|
||||||
55
core/fs/common_funcs.h
Normal file
55
core/fs/common_funcs.h
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u64 AOC_TITLE_ID_MASK = 0x7FF;
|
||||||
|
constexpr u64 AOC_TITLE_ID_OFFSET = 0x1000;
|
||||||
|
constexpr u64 BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base title ID from a given title ID.
|
||||||
|
*
|
||||||
|
* @param title_id The title ID.
|
||||||
|
* @returns The base title ID.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] constexpr u64 GetBaseTitleID(u64 title_id) {
|
||||||
|
return title_id & BASE_TITLE_ID_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base title ID with a program index offset from a given title ID.
|
||||||
|
*
|
||||||
|
* @param title_id The title ID.
|
||||||
|
* @param program_index The program index.
|
||||||
|
* @returns The base title ID with a program index offset.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] constexpr u64 GetBaseTitleIDWithProgramIndex(u64 title_id, u64 program_index) {
|
||||||
|
return GetBaseTitleID(title_id) + program_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the AOC (Add-On Content) base title ID from a given title ID.
|
||||||
|
*
|
||||||
|
* @param title_id The title ID.
|
||||||
|
* @returns The AOC base title ID.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] constexpr u64 GetAOCBaseTitleID(u64 title_id) {
|
||||||
|
return GetBaseTitleID(title_id) + AOC_TITLE_ID_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the AOC (Add-On Content) ID from a given AOC title ID.
|
||||||
|
*
|
||||||
|
* @param aoc_title_id The AOC title ID.
|
||||||
|
* @returns The AOC ID.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] constexpr u64 GetAOCID(u64 aoc_title_id) {
|
||||||
|
return aoc_title_id & AOC_TITLE_ID_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
199
core/fs/content_archive.cpp
Normal file
199
core/fs/content_archive.cpp
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/polyfill_ranges.h"
|
||||||
|
#include "core/crypto/aes_util.h"
|
||||||
|
#include "core/crypto/ctr_encryption_layer.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/partition_filesystem.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_compression_configuration.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_crypto_configuration.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
static u8 MasterKeyIdForKeyGeneration(u8 key_generation) {
|
||||||
|
return std::max<u8>(key_generation, 1) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
NCA::NCA(VirtualFile file_, const NCA* base_nca)
|
||||||
|
: file(std::move(file_)), keys{Core::Crypto::KeyManager::Instance()} {
|
||||||
|
if (file == nullptr) {
|
||||||
|
status = Loader::ResultStatus::ErrorNullFile;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = std::make_shared<NcaReader>();
|
||||||
|
if (Result rc =
|
||||||
|
reader->Initialize(file, GetCryptoConfiguration(), GetNcaCompressionConfiguration());
|
||||||
|
R_FAILED(rc)) {
|
||||||
|
if (rc != ResultInvalidNcaSignature) {
|
||||||
|
LOG_ERROR(Loader, "File reader errored out during header read: {:#x}",
|
||||||
|
rc.GetInnerValue());
|
||||||
|
}
|
||||||
|
status = Loader::ResultStatus::ErrorBadNCAHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the proper key area keys to continue.
|
||||||
|
const u8 master_key_id = MasterKeyIdForKeyGeneration(reader->GetKeyGeneration());
|
||||||
|
if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, reader->GetKeyIndex())) {
|
||||||
|
status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RightsId rights_id{};
|
||||||
|
reader->GetRightsId(rights_id.data(), rights_id.size());
|
||||||
|
if (rights_id != RightsId{}) {
|
||||||
|
// External decryption key required; provide it here.
|
||||||
|
u128 rights_id_u128;
|
||||||
|
std::memcpy(rights_id_u128.data(), rights_id.data(), sizeof(rights_id));
|
||||||
|
|
||||||
|
auto titlekey =
|
||||||
|
keys.GetKey(Core::Crypto::S128KeyType::Titlekey, rights_id_u128[1], rights_id_u128[0]);
|
||||||
|
if (titlekey == Core::Crypto::Key128{}) {
|
||||||
|
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keys.HasKey(Core::Crypto::S128KeyType::Titlekek, master_key_id)) {
|
||||||
|
status = Loader::ResultStatus::ErrorMissingTitlekek;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto titlekek = keys.GetKey(Core::Crypto::S128KeyType::Titlekek, master_key_id);
|
||||||
|
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(titlekek, Core::Crypto::Mode::ECB);
|
||||||
|
cipher.Transcode(titlekey.data(), titlekey.size(), titlekey.data(),
|
||||||
|
Core::Crypto::Op::Decrypt);
|
||||||
|
|
||||||
|
reader->SetExternalDecryptionKey(titlekey.data(), titlekey.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const s32 fs_count = reader->GetFsCount();
|
||||||
|
NcaFileSystemDriver fs(base_nca ? base_nca->reader : nullptr, reader);
|
||||||
|
std::vector<VirtualFile> filesystems(fs_count);
|
||||||
|
for (s32 i = 0; i < fs_count; i++) {
|
||||||
|
NcaFsHeaderReader header_reader;
|
||||||
|
const Result rc = fs.OpenStorage(&filesystems[i], &header_reader, i);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
LOG_ERROR(Loader, "File reader errored out during read of section {}: {:#x}", i,
|
||||||
|
rc.GetInnerValue());
|
||||||
|
status = Loader::ResultStatus::ErrorBadNCAHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_reader.GetFsType() == NcaFsHeader::FsType::RomFs) {
|
||||||
|
files.push_back(filesystems[i]);
|
||||||
|
romfs = files.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_reader.GetFsType() == NcaFsHeader::FsType::PartitionFs) {
|
||||||
|
auto npfs = std::make_shared<PartitionFilesystem>(filesystems[i]);
|
||||||
|
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
dirs.push_back(npfs);
|
||||||
|
if (IsDirectoryExeFS(npfs)) {
|
||||||
|
exefs = dirs.back();
|
||||||
|
} else if (IsDirectoryLogoPartition(npfs)) {
|
||||||
|
logo = dirs.back();
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_reader.GetEncryptionType() == NcaFsHeader::EncryptionType::AesCtrEx) {
|
||||||
|
is_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_update && base_nca == nullptr) {
|
||||||
|
status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS;
|
||||||
|
} else {
|
||||||
|
status = Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NCA::~NCA() = default;
|
||||||
|
|
||||||
|
Loader::ResultStatus NCA::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualFile> NCA::GetFiles() const {
|
||||||
|
if (status != Loader::ResultStatus::Success) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualDir> NCA::GetSubdirectories() const {
|
||||||
|
if (status != Loader::ResultStatus::Success) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NCA::GetName() const {
|
||||||
|
return file->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir NCA::GetParentDirectory() const {
|
||||||
|
return file->GetContainingDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
NCAContentType NCA::GetType() const {
|
||||||
|
return static_cast<NCAContentType>(reader->GetContentType());
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NCA::GetTitleId() const {
|
||||||
|
if (is_update) {
|
||||||
|
return reader->GetProgramId() | 0x800;
|
||||||
|
}
|
||||||
|
return reader->GetProgramId();
|
||||||
|
}
|
||||||
|
|
||||||
|
RightsId NCA::GetRightsId() const {
|
||||||
|
RightsId result;
|
||||||
|
reader->GetRightsId(result.data(), result.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NCA::GetSDKVersion() const {
|
||||||
|
return reader->GetSdkAddonVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 NCA::GetKeyGeneration() const {
|
||||||
|
return reader->GetKeyGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NCA::IsUpdate() const {
|
||||||
|
return is_update;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile NCA::GetRomFS() const {
|
||||||
|
return romfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir NCA::GetExeFS() const {
|
||||||
|
return exefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile NCA::GetBaseFile() const {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir NCA::GetLogoPartition() const {
|
||||||
|
return logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
108
core/fs/content_archive.h
Normal file
108
core/fs/content_archive.h
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class NcaReader;
|
||||||
|
|
||||||
|
/// Describes the type of content within an NCA archive.
|
||||||
|
enum class NCAContentType : u8 {
|
||||||
|
/// Executable-related data
|
||||||
|
Program = 0,
|
||||||
|
|
||||||
|
/// Metadata.
|
||||||
|
Meta = 1,
|
||||||
|
|
||||||
|
/// Access control data.
|
||||||
|
Control = 2,
|
||||||
|
|
||||||
|
/// Information related to the game manual
|
||||||
|
/// e.g. Legal information, etc.
|
||||||
|
Manual = 3,
|
||||||
|
|
||||||
|
/// System data.
|
||||||
|
Data = 4,
|
||||||
|
|
||||||
|
/// Data that can be accessed by applications.
|
||||||
|
PublicData = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
using RightsId = std::array<u8, 0x10>;
|
||||||
|
|
||||||
|
inline bool IsDirectoryExeFS(const VirtualDir& pfs) {
|
||||||
|
// According to switchbrew, an exefs must only contain these two files:
|
||||||
|
return pfs->GetFile("main") != nullptr && pfs->GetFile("main.npdm") != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsDirectoryLogoPartition(const VirtualDir& pfs) {
|
||||||
|
// NintendoLogo is the static image in the top left corner while StartupMovie is the animation
|
||||||
|
// in the bottom right corner.
|
||||||
|
return pfs->GetFile("NintendoLogo.png") != nullptr &&
|
||||||
|
pfs->GetFile("StartupMovie.gif") != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An implementation of VfsDirectory that represents a Nintendo Content Archive (NCA) container.
|
||||||
|
// After construction, use GetStatus to determine if the file is valid and ready to be used.
|
||||||
|
class NCA : public ReadOnlyVfsDirectory {
|
||||||
|
public:
|
||||||
|
explicit NCA(VirtualFile file, const NCA* base_nca = nullptr);
|
||||||
|
~NCA() override;
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
|
||||||
|
std::vector<VirtualFile> GetFiles() const override;
|
||||||
|
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||||
|
std::string GetName() const override;
|
||||||
|
VirtualDir GetParentDirectory() const override;
|
||||||
|
|
||||||
|
NCAContentType GetType() const;
|
||||||
|
u64 GetTitleId() const;
|
||||||
|
RightsId GetRightsId() const;
|
||||||
|
u32 GetSDKVersion() const;
|
||||||
|
u8 GetKeyGeneration() const;
|
||||||
|
bool IsUpdate() const;
|
||||||
|
|
||||||
|
VirtualFile GetRomFS() const;
|
||||||
|
VirtualDir GetExeFS() const;
|
||||||
|
|
||||||
|
VirtualFile GetBaseFile() const;
|
||||||
|
|
||||||
|
VirtualDir GetLogoPartition() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<VirtualDir> dirs;
|
||||||
|
std::vector<VirtualFile> files;
|
||||||
|
|
||||||
|
VirtualFile romfs = nullptr;
|
||||||
|
VirtualDir exefs = nullptr;
|
||||||
|
VirtualDir logo = nullptr;
|
||||||
|
VirtualFile file;
|
||||||
|
|
||||||
|
Loader::ResultStatus status{};
|
||||||
|
|
||||||
|
bool encrypted = false;
|
||||||
|
bool is_update = false;
|
||||||
|
|
||||||
|
Core::Crypto::KeyManager& keys;
|
||||||
|
std::shared_ptr<NcaReader> reader;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
142
core/fs/control_metadata.cpp
Normal file
142
core/fs/control_metadata.cpp
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/settings.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/control_metadata.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
const std::array<const char*, 16> LANGUAGE_NAMES{{
|
||||||
|
"AmericanEnglish",
|
||||||
|
"BritishEnglish",
|
||||||
|
"Japanese",
|
||||||
|
"French",
|
||||||
|
"German",
|
||||||
|
"LatinAmericanSpanish",
|
||||||
|
"Spanish",
|
||||||
|
"Italian",
|
||||||
|
"Dutch",
|
||||||
|
"CanadianFrench",
|
||||||
|
"Portuguese",
|
||||||
|
"Russian",
|
||||||
|
"Korean",
|
||||||
|
"TraditionalChinese",
|
||||||
|
"SimplifiedChinese",
|
||||||
|
"BrazilianPortuguese",
|
||||||
|
}};
|
||||||
|
|
||||||
|
std::string LanguageEntry::GetApplicationName() const {
|
||||||
|
return Common::StringFromFixedZeroTerminatedBuffer(application_name.data(),
|
||||||
|
application_name.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LanguageEntry::GetDeveloperName() const {
|
||||||
|
return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(),
|
||||||
|
developer_name.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::array<Language, 18> language_to_codes = {{
|
||||||
|
Language::Japanese,
|
||||||
|
Language::AmericanEnglish,
|
||||||
|
Language::French,
|
||||||
|
Language::German,
|
||||||
|
Language::Italian,
|
||||||
|
Language::Spanish,
|
||||||
|
Language::SimplifiedChinese,
|
||||||
|
Language::Korean,
|
||||||
|
Language::Dutch,
|
||||||
|
Language::Portuguese,
|
||||||
|
Language::Russian,
|
||||||
|
Language::TraditionalChinese,
|
||||||
|
Language::BritishEnglish,
|
||||||
|
Language::CanadianFrench,
|
||||||
|
Language::LatinAmericanSpanish,
|
||||||
|
Language::SimplifiedChinese,
|
||||||
|
Language::TraditionalChinese,
|
||||||
|
Language::BrazilianPortuguese,
|
||||||
|
}};
|
||||||
|
|
||||||
|
NACP::NACP() = default;
|
||||||
|
|
||||||
|
NACP::NACP(VirtualFile file) {
|
||||||
|
file->ReadObject(&raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
NACP::~NACP() = default;
|
||||||
|
|
||||||
|
const LanguageEntry& NACP::GetLanguageEntry() const {
|
||||||
|
Language language =
|
||||||
|
language_to_codes[static_cast<s32>(Settings::values.language_index.GetValue())];
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto& language_entry = raw.language_entries.at(static_cast<u8>(language));
|
||||||
|
if (!language_entry.GetApplicationName().empty())
|
||||||
|
return language_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& language_entry : raw.language_entries) {
|
||||||
|
if (!language_entry.GetApplicationName().empty())
|
||||||
|
return language_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.language_entries.at(static_cast<u8>(Language::AmericanEnglish));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NACP::GetApplicationName() const {
|
||||||
|
return GetLanguageEntry().GetApplicationName();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NACP::GetDeveloperName() const {
|
||||||
|
return GetLanguageEntry().GetDeveloperName();
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NACP::GetTitleId() const {
|
||||||
|
return raw.save_data_owner_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NACP::GetDLCBaseTitleId() const {
|
||||||
|
return raw.dlc_base_title_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NACP::GetVersionString() const {
|
||||||
|
return Common::StringFromFixedZeroTerminatedBuffer(raw.version_string.data(),
|
||||||
|
raw.version_string.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NACP::GetDefaultNormalSaveSize() const {
|
||||||
|
return raw.user_account_save_data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NACP::GetDefaultJournalSaveSize() const {
|
||||||
|
return raw.user_account_save_data_journal_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NACP::GetUserAccountSwitchLock() const {
|
||||||
|
return raw.user_account_switch_lock != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NACP::GetSupportedLanguages() const {
|
||||||
|
return raw.supported_languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NACP::GetDeviceSaveDataSize() const {
|
||||||
|
return raw.device_save_data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NACP::GetParentalControlFlag() const {
|
||||||
|
return raw.parental_control;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<u8, 0x20>& NACP::GetRatingAge() const {
|
||||||
|
return raw.rating_age;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> NACP::GetRawBytes() const {
|
||||||
|
std::vector<u8> out(sizeof(RawNACP));
|
||||||
|
std::memcpy(out.data(), &raw, sizeof(RawNACP));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
123
core/fs/control_metadata.h
Normal file
123
core/fs/control_metadata.h
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
// A localized entry containing strings within the NACP.
|
||||||
|
// One for each language of type Language.
|
||||||
|
struct LanguageEntry {
|
||||||
|
std::array<char, 0x200> application_name;
|
||||||
|
std::array<char, 0x100> developer_name;
|
||||||
|
|
||||||
|
std::string GetApplicationName() const;
|
||||||
|
std::string GetDeveloperName() const;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LanguageEntry) == 0x300, "LanguageEntry has incorrect size.");
|
||||||
|
|
||||||
|
// The raw file format of a NACP file.
|
||||||
|
struct RawNACP {
|
||||||
|
std::array<LanguageEntry, 16> language_entries;
|
||||||
|
std::array<u8, 0x25> isbn;
|
||||||
|
u8 startup_user_account;
|
||||||
|
u8 user_account_switch_lock;
|
||||||
|
u8 addon_content_registration_type;
|
||||||
|
u32_le application_attribute;
|
||||||
|
u32_le supported_languages;
|
||||||
|
u32_le parental_control;
|
||||||
|
bool screenshot_enabled;
|
||||||
|
u8 video_capture_mode;
|
||||||
|
bool data_loss_confirmation;
|
||||||
|
INSERT_PADDING_BYTES(1);
|
||||||
|
u64_le presence_group_id;
|
||||||
|
std::array<u8, 0x20> rating_age;
|
||||||
|
std::array<char, 0x10> version_string;
|
||||||
|
u64_le dlc_base_title_id;
|
||||||
|
u64_le save_data_owner_id;
|
||||||
|
u64_le user_account_save_data_size;
|
||||||
|
u64_le user_account_save_data_journal_size;
|
||||||
|
u64_le device_save_data_size;
|
||||||
|
u64_le device_save_data_journal_size;
|
||||||
|
u64_le bcat_delivery_cache_storage_size;
|
||||||
|
char application_error_code_category[8];
|
||||||
|
std::array<u64_le, 0x8> local_communication;
|
||||||
|
u8 logo_type;
|
||||||
|
u8 logo_handling;
|
||||||
|
bool runtime_add_on_content_install;
|
||||||
|
INSERT_PADDING_BYTES(5);
|
||||||
|
u64_le seed_for_pseudo_device_id;
|
||||||
|
std::array<u8, 0x41> bcat_passphrase;
|
||||||
|
INSERT_PADDING_BYTES(7);
|
||||||
|
u64_le user_account_save_data_max_size;
|
||||||
|
u64_le user_account_save_data_max_journal_size;
|
||||||
|
u64_le device_save_data_max_size;
|
||||||
|
u64_le device_save_data_max_journal_size;
|
||||||
|
u64_le temporary_storage_size;
|
||||||
|
u64_le cache_storage_size;
|
||||||
|
u64_le cache_storage_journal_size;
|
||||||
|
u64_le cache_storage_data_and_journal_max_size;
|
||||||
|
u16_le cache_storage_max_index;
|
||||||
|
INSERT_PADDING_BYTES(0xE76);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size.");
|
||||||
|
|
||||||
|
// A language on the NX. These are for names and icons.
|
||||||
|
enum class Language : u8 {
|
||||||
|
AmericanEnglish = 0,
|
||||||
|
BritishEnglish = 1,
|
||||||
|
Japanese = 2,
|
||||||
|
French = 3,
|
||||||
|
German = 4,
|
||||||
|
LatinAmericanSpanish = 5,
|
||||||
|
Spanish = 6,
|
||||||
|
Italian = 7,
|
||||||
|
Dutch = 8,
|
||||||
|
CanadianFrench = 9,
|
||||||
|
Portuguese = 10,
|
||||||
|
Russian = 11,
|
||||||
|
Korean = 12,
|
||||||
|
TraditionalChinese = 13,
|
||||||
|
SimplifiedChinese = 14,
|
||||||
|
BrazilianPortuguese = 15,
|
||||||
|
|
||||||
|
Default = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const std::array<const char*, 16> LANGUAGE_NAMES;
|
||||||
|
|
||||||
|
// A class representing the format used by NX metadata files, typically named Control.nacp.
|
||||||
|
// These store application name, dev name, title id, and other miscellaneous data.
|
||||||
|
class NACP {
|
||||||
|
public:
|
||||||
|
explicit NACP();
|
||||||
|
explicit NACP(VirtualFile file);
|
||||||
|
~NACP();
|
||||||
|
|
||||||
|
const LanguageEntry& GetLanguageEntry() const;
|
||||||
|
std::string GetApplicationName() const;
|
||||||
|
std::string GetDeveloperName() const;
|
||||||
|
u64 GetTitleId() const;
|
||||||
|
u64 GetDLCBaseTitleId() const;
|
||||||
|
std::string GetVersionString() const;
|
||||||
|
u64 GetDefaultNormalSaveSize() const;
|
||||||
|
u64 GetDefaultJournalSaveSize() const;
|
||||||
|
u32 GetSupportedLanguages() const;
|
||||||
|
std::vector<u8> GetRawBytes() const;
|
||||||
|
bool GetUserAccountSwitchLock() const;
|
||||||
|
u64 GetDeviceSaveDataSize() const;
|
||||||
|
u32 GetParentalControlFlag() const;
|
||||||
|
const std::array<u8, 0x20>& GetRatingAge() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RawNACP raw{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
97
core/fs/errors.h
Normal file
97
core/fs/errors.h
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr Result ResultPathNotFound{ErrorModule::FS, 1};
|
||||||
|
constexpr Result ResultPathAlreadyExists{ErrorModule::FS, 2};
|
||||||
|
constexpr Result ResultUnsupportedSdkVersion{ErrorModule::FS, 50};
|
||||||
|
constexpr Result ResultPartitionNotFound{ErrorModule::FS, 1001};
|
||||||
|
constexpr Result ResultTargetNotFound{ErrorModule::FS, 1002};
|
||||||
|
constexpr Result ResultPortSdCardNoDevice{ErrorModule::FS, 2001};
|
||||||
|
constexpr Result ResultNotImplemented{ErrorModule::FS, 3001};
|
||||||
|
constexpr Result ResultUnsupportedVersion{ErrorModule::FS, 3002};
|
||||||
|
constexpr Result ResultOutOfRange{ErrorModule::FS, 3005};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedInFileSystemBuddyHeapA{ErrorModule::FS, 3294};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedInNcaFileSystemDriverI{ErrorModule::FS, 3341};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedInNcaReaderA{ErrorModule::FS, 3363};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedInAesCtrCounterExtendedStorageA{ErrorModule::FS, 3399};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedInIntegrityRomFsStorageA{ErrorModule::FS, 3412};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedMakeUnique{ErrorModule::FS, 3422};
|
||||||
|
constexpr Result ResultAllocationMemoryFailedAllocateShared{ErrorModule::FS, 3423};
|
||||||
|
constexpr Result ResultInvalidAesCtrCounterExtendedEntryOffset{ErrorModule::FS, 4012};
|
||||||
|
constexpr Result ResultIndirectStorageCorrupted{ErrorModule::FS, 4021};
|
||||||
|
constexpr Result ResultInvalidIndirectEntryOffset{ErrorModule::FS, 4022};
|
||||||
|
constexpr Result ResultInvalidIndirectEntryStorageIndex{ErrorModule::FS, 4023};
|
||||||
|
constexpr Result ResultInvalidIndirectStorageSize{ErrorModule::FS, 4024};
|
||||||
|
constexpr Result ResultInvalidBucketTreeSignature{ErrorModule::FS, 4032};
|
||||||
|
constexpr Result ResultInvalidBucketTreeEntryCount{ErrorModule::FS, 4033};
|
||||||
|
constexpr Result ResultInvalidBucketTreeNodeEntryCount{ErrorModule::FS, 4034};
|
||||||
|
constexpr Result ResultInvalidBucketTreeNodeOffset{ErrorModule::FS, 4035};
|
||||||
|
constexpr Result ResultInvalidBucketTreeEntryOffset{ErrorModule::FS, 4036};
|
||||||
|
constexpr Result ResultInvalidBucketTreeEntrySetOffset{ErrorModule::FS, 4037};
|
||||||
|
constexpr Result ResultInvalidBucketTreeNodeIndex{ErrorModule::FS, 4038};
|
||||||
|
constexpr Result ResultInvalidBucketTreeVirtualOffset{ErrorModule::FS, 4039};
|
||||||
|
constexpr Result ResultRomNcaInvalidPatchMetaDataHashType{ErrorModule::FS, 4084};
|
||||||
|
constexpr Result ResultRomNcaInvalidIntegrityLayerInfoOffset{ErrorModule::FS, 4085};
|
||||||
|
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataSize{ErrorModule::FS, 4086};
|
||||||
|
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataOffset{ErrorModule::FS, 4087};
|
||||||
|
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataHash{ErrorModule::FS, 4088};
|
||||||
|
constexpr Result ResultRomNcaInvalidSparseMetaDataHashType{ErrorModule::FS, 4089};
|
||||||
|
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataSize{ErrorModule::FS, 4090};
|
||||||
|
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataOffset{ErrorModule::FS, 4091};
|
||||||
|
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataHash{ErrorModule::FS, 4091};
|
||||||
|
constexpr Result ResultNcaBaseStorageOutOfRangeB{ErrorModule::FS, 4509};
|
||||||
|
constexpr Result ResultNcaBaseStorageOutOfRangeC{ErrorModule::FS, 4510};
|
||||||
|
constexpr Result ResultNcaBaseStorageOutOfRangeD{ErrorModule::FS, 4511};
|
||||||
|
constexpr Result ResultInvalidNcaSignature{ErrorModule::FS, 4517};
|
||||||
|
constexpr Result ResultNcaFsHeaderHashVerificationFailed{ErrorModule::FS, 4520};
|
||||||
|
constexpr Result ResultInvalidNcaKeyIndex{ErrorModule::FS, 4521};
|
||||||
|
constexpr Result ResultInvalidNcaFsHeaderHashType{ErrorModule::FS, 4522};
|
||||||
|
constexpr Result ResultInvalidNcaFsHeaderEncryptionType{ErrorModule::FS, 4523};
|
||||||
|
constexpr Result ResultInvalidNcaPatchInfoIndirectSize{ErrorModule::FS, 4524};
|
||||||
|
constexpr Result ResultInvalidNcaPatchInfoAesCtrExSize{ErrorModule::FS, 4525};
|
||||||
|
constexpr Result ResultInvalidNcaPatchInfoAesCtrExOffset{ErrorModule::FS, 4526};
|
||||||
|
constexpr Result ResultInvalidNcaHeader{ErrorModule::FS, 4528};
|
||||||
|
constexpr Result ResultInvalidNcaFsHeader{ErrorModule::FS, 4529};
|
||||||
|
constexpr Result ResultNcaBaseStorageOutOfRangeE{ErrorModule::FS, 4530};
|
||||||
|
constexpr Result ResultInvalidHierarchicalSha256BlockSize{ErrorModule::FS, 4532};
|
||||||
|
constexpr Result ResultInvalidHierarchicalSha256LayerCount{ErrorModule::FS, 4533};
|
||||||
|
constexpr Result ResultHierarchicalSha256BaseStorageTooLarge{ErrorModule::FS, 4534};
|
||||||
|
constexpr Result ResultHierarchicalSha256HashVerificationFailed{ErrorModule::FS, 4535};
|
||||||
|
constexpr Result ResultInvalidNcaHierarchicalIntegrityVerificationLayerCount{ErrorModule::FS, 4541};
|
||||||
|
constexpr Result ResultInvalidNcaIndirectStorageOutOfRange{ErrorModule::FS, 4542};
|
||||||
|
constexpr Result ResultInvalidNcaHeader1SignatureKeyGeneration{ErrorModule::FS, 4543};
|
||||||
|
constexpr Result ResultInvalidCompressedStorageSize{ErrorModule::FS, 4547};
|
||||||
|
constexpr Result ResultInvalidNcaMetaDataHashDataSize{ErrorModule::FS, 4548};
|
||||||
|
constexpr Result ResultInvalidNcaMetaDataHashDataHash{ErrorModule::FS, 4549};
|
||||||
|
constexpr Result ResultUnexpectedInCompressedStorageA{ErrorModule::FS, 5324};
|
||||||
|
constexpr Result ResultUnexpectedInCompressedStorageB{ErrorModule::FS, 5325};
|
||||||
|
constexpr Result ResultUnexpectedInCompressedStorageC{ErrorModule::FS, 5326};
|
||||||
|
constexpr Result ResultUnexpectedInCompressedStorageD{ErrorModule::FS, 5327};
|
||||||
|
constexpr Result ResultUnexpectedInPathA{ErrorModule::FS, 5328};
|
||||||
|
constexpr Result ResultInvalidArgument{ErrorModule::FS, 6001};
|
||||||
|
constexpr Result ResultInvalidPath{ErrorModule::FS, 6002};
|
||||||
|
constexpr Result ResultTooLongPath{ErrorModule::FS, 6003};
|
||||||
|
constexpr Result ResultInvalidCharacter{ErrorModule::FS, 6004};
|
||||||
|
constexpr Result ResultInvalidPathFormat{ErrorModule::FS, 6005};
|
||||||
|
constexpr Result ResultDirectoryUnobtainable{ErrorModule::FS, 6006};
|
||||||
|
constexpr Result ResultNotNormalized{ErrorModule::FS, 6007};
|
||||||
|
constexpr Result ResultInvalidOffset{ErrorModule::FS, 6061};
|
||||||
|
constexpr Result ResultInvalidSize{ErrorModule::FS, 6062};
|
||||||
|
constexpr Result ResultNullptrArgument{ErrorModule::FS, 6063};
|
||||||
|
constexpr Result ResultInvalidOpenMode{ErrorModule::FS, 6072};
|
||||||
|
constexpr Result ResultFileExtensionWithoutOpenModeAllowAppend{ErrorModule::FS, 6201};
|
||||||
|
constexpr Result ResultReadNotPermitted{ErrorModule::FS, 6202};
|
||||||
|
constexpr Result ResultWriteNotPermitted{ErrorModule::FS, 6203};
|
||||||
|
constexpr Result ResultUnsupportedSetSizeForIndirectStorage{ErrorModule::FS, 6325};
|
||||||
|
constexpr Result ResultUnsupportedWriteForCompressedStorage{ErrorModule::FS, 6387};
|
||||||
|
constexpr Result ResultUnsupportedOperateRangeForCompressedStorage{ErrorModule::FS, 6388};
|
||||||
|
constexpr Result ResultPermissionDenied{ErrorModule::FS, 6400};
|
||||||
|
constexpr Result ResultBufferAllocationFailed{ErrorModule::FS, 6705};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
37
core/fs/fs_directory.h
Normal file
37
core/fs/fs_directory.h
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr inline size_t EntryNameLengthMax = 0x300;
|
||||||
|
|
||||||
|
struct DirectoryEntry {
|
||||||
|
DirectoryEntry(std::string_view view, s8 entry_type, u64 entry_size)
|
||||||
|
: type{entry_type}, file_size{static_cast<s64>(entry_size)} {
|
||||||
|
const std::size_t copy_size = view.copy(name, std::size(name) - 1);
|
||||||
|
name[copy_size] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
char name[EntryNameLengthMax + 1];
|
||||||
|
INSERT_PADDING_BYTES(3);
|
||||||
|
s8 type;
|
||||||
|
INSERT_PADDING_BYTES(3);
|
||||||
|
s64 file_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(DirectoryEntry) == 0x310,
|
||||||
|
"Directory Entry struct isn't exactly 0x310 bytes long!");
|
||||||
|
static_assert(offsetof(DirectoryEntry, type) == 0x304, "Wrong offset for type in Entry.");
|
||||||
|
static_assert(offsetof(DirectoryEntry, file_size) == 0x308, "Wrong offset for file_size in Entry.");
|
||||||
|
|
||||||
|
struct DirectoryHandle {
|
||||||
|
void* handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
65
core/fs/fs_file.h
Normal file
65
core/fs/fs_file.h
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
struct ReadOption {
|
||||||
|
u32 value;
|
||||||
|
|
||||||
|
static const ReadOption None;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ReadOptionFlag : u32 {
|
||||||
|
ReadOptionFlag_None = (0 << 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr const ReadOption ReadOption::None = {ReadOptionFlag_None};
|
||||||
|
|
||||||
|
inline constexpr bool operator==(const ReadOption& lhs, const ReadOption& rhs) {
|
||||||
|
return lhs.value == rhs.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr bool operator!=(const ReadOption& lhs, const ReadOption& rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(sizeof(ReadOption) == sizeof(u32));
|
||||||
|
|
||||||
|
enum WriteOptionFlag : u32 {
|
||||||
|
WriteOptionFlag_None = (0 << 0),
|
||||||
|
WriteOptionFlag_Flush = (1 << 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteOption {
|
||||||
|
u32 value;
|
||||||
|
|
||||||
|
constexpr inline bool HasFlushFlag() const {
|
||||||
|
return value & WriteOptionFlag_Flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const WriteOption None;
|
||||||
|
static const WriteOption Flush;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr const WriteOption WriteOption::None = {WriteOptionFlag_None};
|
||||||
|
inline constexpr const WriteOption WriteOption::Flush = {WriteOptionFlag_Flush};
|
||||||
|
|
||||||
|
inline constexpr bool operator==(const WriteOption& lhs, const WriteOption& rhs) {
|
||||||
|
return lhs.value == rhs.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr bool operator!=(const WriteOption& lhs, const WriteOption& rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(sizeof(WriteOption) == sizeof(u32));
|
||||||
|
|
||||||
|
struct FileHandle {
|
||||||
|
void* handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
66
core/fs/fs_filesystem.h
Normal file
66
core/fs/fs_filesystem.h
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class OpenMode : u32 {
|
||||||
|
Read = (1 << 0),
|
||||||
|
Write = (1 << 1),
|
||||||
|
AllowAppend = (1 << 2),
|
||||||
|
|
||||||
|
ReadWrite = (Read | Write),
|
||||||
|
All = (ReadWrite | AllowAppend),
|
||||||
|
};
|
||||||
|
DECLARE_ENUM_FLAG_OPERATORS(OpenMode)
|
||||||
|
|
||||||
|
enum class OpenDirectoryMode : u64 {
|
||||||
|
Directory = (1 << 0),
|
||||||
|
File = (1 << 1),
|
||||||
|
|
||||||
|
All = (Directory | File),
|
||||||
|
|
||||||
|
NotRequireFileSize = (1ULL << 31),
|
||||||
|
};
|
||||||
|
DECLARE_ENUM_FLAG_OPERATORS(OpenDirectoryMode)
|
||||||
|
|
||||||
|
enum class DirectoryEntryType : u8 {
|
||||||
|
Directory = 0,
|
||||||
|
File = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CreateOption : u8 {
|
||||||
|
None = (0 << 0),
|
||||||
|
BigFile = (1 << 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileSystemAttribute {
|
||||||
|
u8 dir_entry_name_length_max_defined;
|
||||||
|
u8 file_entry_name_length_max_defined;
|
||||||
|
u8 dir_path_name_length_max_defined;
|
||||||
|
u8 file_path_name_length_max_defined;
|
||||||
|
INSERT_PADDING_BYTES_NOINIT(0x5);
|
||||||
|
u8 utf16_dir_entry_name_length_max_defined;
|
||||||
|
u8 utf16_file_entry_name_length_max_defined;
|
||||||
|
u8 utf16_dir_path_name_length_max_defined;
|
||||||
|
u8 utf16_file_path_name_length_max_defined;
|
||||||
|
INSERT_PADDING_BYTES_NOINIT(0x18);
|
||||||
|
s32 dir_entry_name_length_max;
|
||||||
|
s32 file_entry_name_length_max;
|
||||||
|
s32 dir_path_name_length_max;
|
||||||
|
s32 file_path_name_length_max;
|
||||||
|
INSERT_PADDING_WORDS_NOINIT(0x5);
|
||||||
|
s32 utf16_dir_entry_name_length_max;
|
||||||
|
s32 utf16_file_entry_name_length_max;
|
||||||
|
s32 utf16_dir_path_name_length_max;
|
||||||
|
s32 utf16_file_path_name_length_max;
|
||||||
|
INSERT_PADDING_WORDS_NOINIT(0x18);
|
||||||
|
INSERT_PADDING_WORDS_NOINIT(0x1);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(FileSystemAttribute) == 0xC0, "FileSystemAttribute has incorrect size");
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
40
core/fs/fs_memory_management.h
Normal file
40
core/fs/fs_memory_management.h
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include "common/alignment.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr size_t RequiredAlignment = alignof(u64);
|
||||||
|
|
||||||
|
inline void* AllocateUnsafe(size_t size) {
|
||||||
|
// Allocate
|
||||||
|
void* const ptr = ::operator new(size, std::align_val_t{RequiredAlignment});
|
||||||
|
|
||||||
|
// Check alignment
|
||||||
|
ASSERT(Common::IsAligned(reinterpret_cast<uintptr_t>(ptr), RequiredAlignment));
|
||||||
|
|
||||||
|
// Return allocated pointer
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void DeallocateUnsafe(void* ptr, size_t size) {
|
||||||
|
// Deallocate the pointer
|
||||||
|
::operator delete(ptr, std::align_val_t{RequiredAlignment});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void* Allocate(size_t size) {
|
||||||
|
return AllocateUnsafe(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Deallocate(void* ptr, size_t size) {
|
||||||
|
// If the pointer is non-null, deallocate it
|
||||||
|
if (ptr != nullptr) {
|
||||||
|
DeallocateUnsafe(ptr, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
22
core/fs/fs_operate_range.h
Normal file
22
core/fs/fs_operate_range.h
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class OperationId : s64 {
|
||||||
|
FillZero = 0,
|
||||||
|
DestroySignature = 1,
|
||||||
|
Invalidate = 2,
|
||||||
|
QueryRange = 3,
|
||||||
|
QueryUnpreparedRange = 4,
|
||||||
|
QueryLazyLoadCompletionRate = 5,
|
||||||
|
SetLazyLoadPriority = 6,
|
||||||
|
|
||||||
|
ReadLazyLoadFileForciblyForDebug = 10001,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
566
core/fs/fs_path.h
Normal file
566
core/fs/fs_path.h
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fs_memory_management.h"
|
||||||
|
#include "core/file_sys/fs_path_utility.h"
|
||||||
|
#include "core/file_sys/fs_string_util.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
class DirectoryPathParser;
|
||||||
|
|
||||||
|
class Path {
|
||||||
|
YUZU_NON_COPYABLE(Path);
|
||||||
|
YUZU_NON_MOVEABLE(Path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr const char* EmptyPath = "";
|
||||||
|
static constexpr size_t WriteBufferAlignmentLength = 8;
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class DirectoryPathParser;
|
||||||
|
|
||||||
|
public:
|
||||||
|
class WriteBuffer {
|
||||||
|
YUZU_NON_COPYABLE(WriteBuffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
char* m_buffer;
|
||||||
|
size_t m_length_and_is_normalized;
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr WriteBuffer() : m_buffer(nullptr), m_length_and_is_normalized(0) {}
|
||||||
|
|
||||||
|
constexpr ~WriteBuffer() {
|
||||||
|
if (m_buffer != nullptr) {
|
||||||
|
Deallocate(m_buffer, this->GetLength());
|
||||||
|
this->ResetBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr WriteBuffer(WriteBuffer&& rhs)
|
||||||
|
: m_buffer(rhs.m_buffer), m_length_and_is_normalized(rhs.m_length_and_is_normalized) {
|
||||||
|
rhs.ResetBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr WriteBuffer& operator=(WriteBuffer&& rhs) {
|
||||||
|
if (m_buffer != nullptr) {
|
||||||
|
Deallocate(m_buffer, this->GetLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buffer = rhs.m_buffer;
|
||||||
|
m_length_and_is_normalized = rhs.m_length_and_is_normalized;
|
||||||
|
|
||||||
|
rhs.ResetBuffer();
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void ResetBuffer() {
|
||||||
|
m_buffer = nullptr;
|
||||||
|
this->SetLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr char* Get() const {
|
||||||
|
return m_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t GetLength() const {
|
||||||
|
return m_length_and_is_normalized >> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsNormalized() const {
|
||||||
|
return static_cast<bool>(m_length_and_is_normalized & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetNormalized() {
|
||||||
|
m_length_and_is_normalized |= static_cast<size_t>(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetNotNormalized() {
|
||||||
|
m_length_and_is_normalized &= ~static_cast<size_t>(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
constexpr WriteBuffer(char* buffer, size_t length)
|
||||||
|
: m_buffer(buffer), m_length_and_is_normalized(0) {
|
||||||
|
this->SetLength(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static WriteBuffer Make(size_t length) {
|
||||||
|
if (void* alloc = Allocate(length); alloc != nullptr) {
|
||||||
|
return WriteBuffer(static_cast<char*>(alloc), length);
|
||||||
|
} else {
|
||||||
|
return WriteBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
constexpr void SetLength(size_t size) {
|
||||||
|
m_length_and_is_normalized = (m_length_and_is_normalized & 1) | (size << 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
const char* m_str;
|
||||||
|
WriteBuffer m_write_buffer;
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr Path() : m_str(EmptyPath), m_write_buffer() {}
|
||||||
|
|
||||||
|
constexpr Path(const char* s) : m_str(s), m_write_buffer() {
|
||||||
|
m_write_buffer.SetNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ~Path() = default;
|
||||||
|
|
||||||
|
constexpr Result SetShallowBuffer(const char* buffer) {
|
||||||
|
// Check pre-conditions
|
||||||
|
ASSERT(m_write_buffer.GetLength() == 0);
|
||||||
|
|
||||||
|
// Check the buffer is valid
|
||||||
|
R_UNLESS(buffer != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Set buffer
|
||||||
|
this->SetReadOnlyBuffer(buffer);
|
||||||
|
|
||||||
|
// Note that we're normalized
|
||||||
|
this->SetNormalized();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr const char* GetString() const {
|
||||||
|
// Check pre-conditions
|
||||||
|
ASSERT(this->IsNormalized());
|
||||||
|
|
||||||
|
return m_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t GetLength() const {
|
||||||
|
if (std::is_constant_evaluated()) {
|
||||||
|
return Strlen(this->GetString());
|
||||||
|
} else {
|
||||||
|
return std::strlen(this->GetString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsEmpty() const {
|
||||||
|
return *m_str == '\x00';
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsMatchHead(const char* p, size_t len) const {
|
||||||
|
return Strncmp(this->GetString(), p, len) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const Path& rhs) {
|
||||||
|
// Check the other path is normalized
|
||||||
|
const bool normalized = rhs.IsNormalized();
|
||||||
|
R_UNLESS(normalized, ResultNotNormalized);
|
||||||
|
|
||||||
|
// Allocate buffer for our path
|
||||||
|
const auto len = rhs.GetLength();
|
||||||
|
R_TRY(this->Preallocate(len + 1));
|
||||||
|
|
||||||
|
// Copy the path
|
||||||
|
const size_t copied = Strlcpy<char>(m_write_buffer.Get(), rhs.GetString(), len + 1);
|
||||||
|
R_UNLESS(copied == len, ResultUnexpectedInPathA);
|
||||||
|
|
||||||
|
// Set normalized
|
||||||
|
this->SetNormalized();
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const char* path, size_t len) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
R_TRY(this->InitializeImpl(path, len));
|
||||||
|
|
||||||
|
// Set not normalized
|
||||||
|
this->SetNotNormalized();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const char* path) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
R_RETURN(this->Initialize(path, std::strlen(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeWithReplaceBackslash(const char* path) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
R_TRY(this->InitializeImpl(path, std::strlen(path)));
|
||||||
|
|
||||||
|
// Replace slashes as desired
|
||||||
|
if (const auto write_buffer_length = m_write_buffer.GetLength(); write_buffer_length > 1) {
|
||||||
|
Replace(m_write_buffer.Get(), write_buffer_length - 1, '\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set not normalized
|
||||||
|
this->SetNotNormalized();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeWithReplaceForwardSlashes(const char* path) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
R_TRY(this->InitializeImpl(path, std::strlen(path)));
|
||||||
|
|
||||||
|
// Replace slashes as desired
|
||||||
|
if (m_write_buffer.GetLength() > 1) {
|
||||||
|
if (auto* p = m_write_buffer.Get(); p[0] == '/' && p[1] == '/') {
|
||||||
|
p[0] = '\\';
|
||||||
|
p[1] = '\\';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set not normalized
|
||||||
|
this->SetNotNormalized();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeWithNormalization(const char* path, size_t size) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
R_TRY(this->InitializeImpl(path, size));
|
||||||
|
|
||||||
|
// Set not normalized
|
||||||
|
this->SetNotNormalized();
|
||||||
|
|
||||||
|
// Perform normalization
|
||||||
|
PathFlags path_flags;
|
||||||
|
if (IsPathRelative(m_str)) {
|
||||||
|
path_flags.AllowRelativePath();
|
||||||
|
} else if (IsWindowsPath(m_str, true)) {
|
||||||
|
path_flags.AllowWindowsPath();
|
||||||
|
} else {
|
||||||
|
/* NOTE: In this case, Nintendo checks is normalized, then sets is normalized, then
|
||||||
|
* returns success. */
|
||||||
|
/* This seems like a bug. */
|
||||||
|
size_t dummy;
|
||||||
|
bool normalized;
|
||||||
|
R_TRY(PathFormatter::IsNormalized(std::addressof(normalized), std::addressof(dummy),
|
||||||
|
m_str));
|
||||||
|
|
||||||
|
this->SetNormalized();
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
R_TRY(this->Normalize(path_flags));
|
||||||
|
|
||||||
|
this->SetNormalized();
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeWithNormalization(const char* path) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(path != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
R_RETURN(this->InitializeWithNormalization(path, std::strlen(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeAsEmpty() {
|
||||||
|
// Clear our buffer
|
||||||
|
this->ClearBuffer();
|
||||||
|
|
||||||
|
// Set normalized
|
||||||
|
this->SetNormalized();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result AppendChild(const char* child) {
|
||||||
|
// Check the path is valid
|
||||||
|
R_UNLESS(child != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Basic checks. If we have a path and the child is empty, we have nothing to do
|
||||||
|
const char* c = child;
|
||||||
|
if (m_str[0]) {
|
||||||
|
// Skip an early separator
|
||||||
|
if (*c == '/') {
|
||||||
|
++c;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED_IF(*c == '\x00');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a string, we can just initialize
|
||||||
|
auto cur_len = std::strlen(m_str);
|
||||||
|
if (cur_len == 0) {
|
||||||
|
R_RETURN(this->Initialize(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a trailing separator
|
||||||
|
if (m_str[cur_len - 1] == '/' || m_str[cur_len - 1] == '\\') {
|
||||||
|
--cur_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the child path's length
|
||||||
|
auto child_len = std::strlen(c);
|
||||||
|
|
||||||
|
// Reset our write buffer
|
||||||
|
WriteBuffer old_write_buffer;
|
||||||
|
if (m_write_buffer.Get() != nullptr) {
|
||||||
|
old_write_buffer = std::move(m_write_buffer);
|
||||||
|
this->ClearBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-allocate the new buffer
|
||||||
|
R_TRY(this->Preallocate(cur_len + 1 + child_len + 1));
|
||||||
|
|
||||||
|
// Get our write buffer
|
||||||
|
auto* dst = m_write_buffer.Get();
|
||||||
|
if (old_write_buffer.Get() != nullptr && cur_len > 0) {
|
||||||
|
Strlcpy<char>(dst, old_write_buffer.Get(), cur_len + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add separator
|
||||||
|
dst[cur_len] = '/';
|
||||||
|
|
||||||
|
// Copy the child path
|
||||||
|
const size_t copied = Strlcpy<char>(dst + cur_len + 1, c, child_len + 1);
|
||||||
|
R_UNLESS(copied == child_len, ResultUnexpectedInPathA);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result AppendChild(const Path& rhs) {
|
||||||
|
R_RETURN(this->AppendChild(rhs.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Combine(const Path& parent, const Path& child) {
|
||||||
|
// Get the lengths
|
||||||
|
const auto p_len = parent.GetLength();
|
||||||
|
const auto c_len = child.GetLength();
|
||||||
|
|
||||||
|
// Allocate our buffer
|
||||||
|
R_TRY(this->Preallocate(p_len + c_len + 1));
|
||||||
|
|
||||||
|
// Initialize as parent
|
||||||
|
R_TRY(this->Initialize(parent));
|
||||||
|
|
||||||
|
// If we're empty, we can just initialize as child
|
||||||
|
if (this->IsEmpty()) {
|
||||||
|
R_TRY(this->Initialize(child));
|
||||||
|
} else {
|
||||||
|
// Otherwise, we should append the child
|
||||||
|
R_TRY(this->AppendChild(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result RemoveChild() {
|
||||||
|
// If we don't have a write-buffer, ensure that we have one
|
||||||
|
if (m_write_buffer.Get() == nullptr) {
|
||||||
|
if (const auto len = std::strlen(m_str); len > 0) {
|
||||||
|
R_TRY(this->Preallocate(len));
|
||||||
|
Strlcpy<char>(m_write_buffer.Get(), m_str, len + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it's possible for us to remove a child
|
||||||
|
auto* p = m_write_buffer.Get();
|
||||||
|
s32 len = static_cast<s32>(std::strlen(p));
|
||||||
|
R_UNLESS(len != 1 || (p[0] != '/' && p[0] != '.'), ResultNotImplemented);
|
||||||
|
|
||||||
|
// Handle a trailing separator
|
||||||
|
if (len > 0 && (p[len - 1] == '\\' || p[len - 1] == '/')) {
|
||||||
|
--len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the child path segment
|
||||||
|
while ((--len) >= 0 && p[len]) {
|
||||||
|
if (p[len] == '/' || p[len] == '\\') {
|
||||||
|
if (len > 0) {
|
||||||
|
p[len] = 0;
|
||||||
|
} else {
|
||||||
|
p[1] = 0;
|
||||||
|
len = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that length remains > 0
|
||||||
|
R_UNLESS(len > 0, ResultNotImplemented);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Normalize(const PathFlags& flags) {
|
||||||
|
// If we're already normalized, nothing to do
|
||||||
|
R_SUCCEED_IF(this->IsNormalized());
|
||||||
|
|
||||||
|
// Check if we're normalized
|
||||||
|
bool normalized;
|
||||||
|
size_t dummy;
|
||||||
|
R_TRY(PathFormatter::IsNormalized(std::addressof(normalized), std::addressof(dummy), m_str,
|
||||||
|
flags));
|
||||||
|
|
||||||
|
// If we're not normalized, normalize
|
||||||
|
if (!normalized) {
|
||||||
|
// Determine necessary buffer length
|
||||||
|
auto len = m_write_buffer.GetLength();
|
||||||
|
if (flags.IsRelativePathAllowed() && IsPathRelative(m_str)) {
|
||||||
|
len += 2;
|
||||||
|
}
|
||||||
|
if (flags.IsWindowsPathAllowed() && IsWindowsPath(m_str, true)) {
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate a new buffer
|
||||||
|
const size_t size = Common::AlignUp(len, WriteBufferAlignmentLength);
|
||||||
|
auto buf = WriteBuffer::Make(size);
|
||||||
|
R_UNLESS(buf.Get() != nullptr, ResultAllocationMemoryFailedMakeUnique);
|
||||||
|
|
||||||
|
// Normalize into it
|
||||||
|
R_TRY(PathFormatter::Normalize(buf.Get(), size, m_write_buffer.Get(),
|
||||||
|
m_write_buffer.GetLength(), flags));
|
||||||
|
|
||||||
|
// Set the normalized buffer as our buffer
|
||||||
|
this->SetModifiableBuffer(std::move(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set normalized
|
||||||
|
this->SetNormalized();
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ClearBuffer() {
|
||||||
|
m_write_buffer.ResetBuffer();
|
||||||
|
m_str = EmptyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetModifiableBuffer(WriteBuffer&& buffer) {
|
||||||
|
// Check pre-conditions
|
||||||
|
ASSERT(buffer.Get() != nullptr);
|
||||||
|
ASSERT(buffer.GetLength() > 0);
|
||||||
|
ASSERT(Common::IsAligned(buffer.GetLength(), WriteBufferAlignmentLength));
|
||||||
|
|
||||||
|
// Get whether we're normalized
|
||||||
|
if (m_write_buffer.IsNormalized()) {
|
||||||
|
buffer.SetNormalized();
|
||||||
|
} else {
|
||||||
|
buffer.SetNotNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set write buffer
|
||||||
|
m_write_buffer = std::move(buffer);
|
||||||
|
m_str = m_write_buffer.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetReadOnlyBuffer(const char* buffer) {
|
||||||
|
m_str = buffer;
|
||||||
|
m_write_buffer.ResetBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Preallocate(size_t length) {
|
||||||
|
// Allocate additional space, if needed
|
||||||
|
if (length > m_write_buffer.GetLength()) {
|
||||||
|
// Allocate buffer
|
||||||
|
const size_t size = Common::AlignUp(length, WriteBufferAlignmentLength);
|
||||||
|
auto buf = WriteBuffer::Make(size);
|
||||||
|
R_UNLESS(buf.Get() != nullptr, ResultAllocationMemoryFailedMakeUnique);
|
||||||
|
|
||||||
|
// Set write buffer
|
||||||
|
this->SetModifiableBuffer(std::move(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InitializeImpl(const char* path, size_t size) {
|
||||||
|
if (size > 0 && path[0]) {
|
||||||
|
// Pre allocate a buffer for the path
|
||||||
|
R_TRY(this->Preallocate(size + 1));
|
||||||
|
|
||||||
|
// Copy the path
|
||||||
|
const size_t copied = Strlcpy<char>(m_write_buffer.Get(), path, size + 1);
|
||||||
|
R_UNLESS(copied >= size, ResultUnexpectedInPathA);
|
||||||
|
} else {
|
||||||
|
// We can just clear the buffer
|
||||||
|
this->ClearBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr char* GetWriteBuffer() {
|
||||||
|
ASSERT(m_write_buffer.Get() != nullptr);
|
||||||
|
return m_write_buffer.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t GetWriteBufferLength() const {
|
||||||
|
return m_write_buffer.GetLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsNormalized() const {
|
||||||
|
return m_write_buffer.IsNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetNormalized() {
|
||||||
|
m_write_buffer.SetNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetNotNormalized() {
|
||||||
|
m_write_buffer.SetNotNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool operator==(const FileSys::Path& rhs) const {
|
||||||
|
return std::strcmp(this->GetString(), rhs.GetString()) == 0;
|
||||||
|
}
|
||||||
|
bool operator!=(const FileSys::Path& rhs) const {
|
||||||
|
return !(*this == rhs);
|
||||||
|
}
|
||||||
|
bool operator==(const char* p) const {
|
||||||
|
return std::strcmp(this->GetString(), p) == 0;
|
||||||
|
}
|
||||||
|
bool operator!=(const char* p) const {
|
||||||
|
return !(*this == p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline Result SetUpFixedPath(FileSys::Path* out, const char* s) {
|
||||||
|
// Verify the path is normalized
|
||||||
|
bool normalized;
|
||||||
|
size_t dummy;
|
||||||
|
R_TRY(PathNormalizer::IsNormalized(std::addressof(normalized), std::addressof(dummy), s));
|
||||||
|
|
||||||
|
R_UNLESS(normalized, ResultInvalidPathFormat);
|
||||||
|
|
||||||
|
// Set the fixed path
|
||||||
|
R_RETURN(out->SetShallowBuffer(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline bool IsWindowsDriveRootPath(const FileSys::Path& path) {
|
||||||
|
const char* const str = path.GetString();
|
||||||
|
return IsWindowsDrive(str) &&
|
||||||
|
(str[2] == StringTraits::DirectorySeparator ||
|
||||||
|
str[2] == StringTraits::AlternateDirectorySeparator) &&
|
||||||
|
str[3] == StringTraits::NullTerminator;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
1240
core/fs/fs_path_utility.h
Normal file
1240
core/fs/fs_path_utility.h
Normal file
File diff suppressed because it is too large
Load diff
188
core/fs/fs_save_data_types.h
Normal file
188
core/fs/fs_save_data_types.h
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using SaveDataId = u64;
|
||||||
|
using SystemSaveDataId = u64;
|
||||||
|
using SystemBcatSaveDataId = SystemSaveDataId;
|
||||||
|
using ProgramId = u64;
|
||||||
|
|
||||||
|
enum class SaveDataSpaceId : u8 {
|
||||||
|
System = 0,
|
||||||
|
User = 1,
|
||||||
|
SdSystem = 2,
|
||||||
|
Temporary = 3,
|
||||||
|
SdUser = 4,
|
||||||
|
|
||||||
|
ProperSystem = 100,
|
||||||
|
SafeMode = 101,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SaveDataType : u8 {
|
||||||
|
System = 0,
|
||||||
|
Account = 1,
|
||||||
|
Bcat = 2,
|
||||||
|
Device = 3,
|
||||||
|
Temporary = 4,
|
||||||
|
Cache = 5,
|
||||||
|
SystemBcat = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SaveDataRank : u8 {
|
||||||
|
Primary = 0,
|
||||||
|
Secondary = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SaveDataSize {
|
||||||
|
u64 normal;
|
||||||
|
u64 journal;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SaveDataSize) == 0x10, "SaveDataSize has invalid size.");
|
||||||
|
|
||||||
|
using UserId = u128;
|
||||||
|
static_assert(std::is_trivially_copyable_v<UserId>, "Data type must be trivially copyable.");
|
||||||
|
static_assert(sizeof(UserId) == 0x10, "UserId has invalid size.");
|
||||||
|
|
||||||
|
constexpr inline SystemSaveDataId InvalidSystemSaveDataId = 0;
|
||||||
|
constexpr inline UserId InvalidUserId = {};
|
||||||
|
|
||||||
|
enum class SaveDataFlags : u32 {
|
||||||
|
None = (0 << 0),
|
||||||
|
KeepAfterResettingSystemSaveData = (1 << 0),
|
||||||
|
KeepAfterRefurbishment = (1 << 1),
|
||||||
|
KeepAfterResettingSystemSaveDataWithoutUserSaveData = (1 << 2),
|
||||||
|
NeedsSecureDelete = (1 << 3),
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SaveDataMetaType : u8 {
|
||||||
|
None = 0,
|
||||||
|
Thumbnail = 1,
|
||||||
|
ExtensionContext = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SaveDataMetaInfo {
|
||||||
|
u32 size;
|
||||||
|
SaveDataMetaType type;
|
||||||
|
INSERT_PADDING_BYTES(0xB);
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivially_copyable_v<SaveDataMetaInfo>,
|
||||||
|
"Data type must be trivially copyable.");
|
||||||
|
static_assert(sizeof(SaveDataMetaInfo) == 0x10, "SaveDataMetaInfo has invalid size.");
|
||||||
|
|
||||||
|
struct SaveDataCreationInfo {
|
||||||
|
s64 size;
|
||||||
|
s64 journal_size;
|
||||||
|
s64 block_size;
|
||||||
|
u64 owner_id;
|
||||||
|
u32 flags;
|
||||||
|
SaveDataSpaceId space_id;
|
||||||
|
bool pseudo;
|
||||||
|
INSERT_PADDING_BYTES(0x1A);
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivially_copyable_v<SaveDataCreationInfo>,
|
||||||
|
"Data type must be trivially copyable.");
|
||||||
|
static_assert(sizeof(SaveDataCreationInfo) == 0x40, "SaveDataCreationInfo has invalid size.");
|
||||||
|
|
||||||
|
struct SaveDataAttribute {
|
||||||
|
ProgramId program_id;
|
||||||
|
UserId user_id;
|
||||||
|
SystemSaveDataId system_save_data_id;
|
||||||
|
SaveDataType type;
|
||||||
|
SaveDataRank rank;
|
||||||
|
u16 index;
|
||||||
|
INSERT_PADDING_BYTES(0x1C);
|
||||||
|
|
||||||
|
static constexpr SaveDataAttribute Make(ProgramId program_id, SaveDataType type, UserId user_id,
|
||||||
|
SystemSaveDataId system_save_data_id, u16 index,
|
||||||
|
SaveDataRank rank) {
|
||||||
|
return {
|
||||||
|
.program_id = program_id,
|
||||||
|
.user_id = user_id,
|
||||||
|
.system_save_data_id = system_save_data_id,
|
||||||
|
.type = type,
|
||||||
|
.rank = rank,
|
||||||
|
.index = index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr SaveDataAttribute Make(ProgramId program_id, SaveDataType type, UserId user_id,
|
||||||
|
SystemSaveDataId system_save_data_id, u16 index) {
|
||||||
|
return Make(program_id, type, user_id, system_save_data_id, index, SaveDataRank::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr SaveDataAttribute Make(ProgramId program_id, SaveDataType type, UserId user_id,
|
||||||
|
SystemSaveDataId system_save_data_id) {
|
||||||
|
return Make(program_id, type, user_id, system_save_data_id, 0, SaveDataRank::Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DebugInfo() const {
|
||||||
|
return fmt::format(
|
||||||
|
"[title_id={:016X}, user_id={:016X}{:016X}, save_id={:016X}, type={:02X}, "
|
||||||
|
"rank={}, index={}]",
|
||||||
|
program_id, user_id[1], user_id[0], system_save_data_id, static_cast<u8>(type),
|
||||||
|
static_cast<u8>(rank), index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SaveDataAttribute) == 0x40);
|
||||||
|
static_assert(std::is_trivially_destructible<SaveDataAttribute>::value);
|
||||||
|
|
||||||
|
constexpr inline bool operator<(const SaveDataAttribute& lhs, const SaveDataAttribute& rhs) {
|
||||||
|
return std::tie(lhs.program_id, lhs.user_id, lhs.system_save_data_id, lhs.index, lhs.rank) <
|
||||||
|
std::tie(rhs.program_id, rhs.user_id, rhs.system_save_data_id, rhs.index, rhs.rank);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline bool operator==(const SaveDataAttribute& lhs, const SaveDataAttribute& rhs) {
|
||||||
|
return std::tie(lhs.program_id, lhs.user_id, lhs.system_save_data_id, lhs.type, lhs.rank,
|
||||||
|
lhs.index) == std::tie(rhs.program_id, rhs.user_id, rhs.system_save_data_id,
|
||||||
|
rhs.type, rhs.rank, rhs.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline bool operator!=(const SaveDataAttribute& lhs, const SaveDataAttribute& rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaveDataExtraData {
|
||||||
|
SaveDataAttribute attr;
|
||||||
|
u64 owner_id;
|
||||||
|
s64 timestamp;
|
||||||
|
u32 flags;
|
||||||
|
INSERT_PADDING_BYTES(4);
|
||||||
|
s64 available_size;
|
||||||
|
s64 journal_size;
|
||||||
|
s64 commit_id;
|
||||||
|
INSERT_PADDING_BYTES(0x190);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SaveDataExtraData) == 0x200, "SaveDataExtraData has invalid size.");
|
||||||
|
static_assert(std::is_trivially_copyable_v<SaveDataExtraData>,
|
||||||
|
"Data type must be trivially copyable.");
|
||||||
|
|
||||||
|
struct SaveDataFilter {
|
||||||
|
bool use_program_id;
|
||||||
|
bool use_save_data_type;
|
||||||
|
bool use_user_id;
|
||||||
|
bool use_save_data_id;
|
||||||
|
bool use_index;
|
||||||
|
SaveDataRank rank;
|
||||||
|
SaveDataAttribute attribute;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SaveDataFilter) == 0x48, "SaveDataFilter has invalid size.");
|
||||||
|
static_assert(std::is_trivially_copyable_v<SaveDataFilter>,
|
||||||
|
"Data type must be trivially copyable.");
|
||||||
|
|
||||||
|
struct HashSalt {
|
||||||
|
static constexpr size_t Size = 32;
|
||||||
|
|
||||||
|
std::array<u8, Size> value;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivially_copyable_v<HashSalt>, "Data type must be trivially copyable.");
|
||||||
|
static_assert(sizeof(HashSalt) == HashSalt::Size);
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
241
core/fs/fs_string_util.h
Normal file
241
core/fs/fs_string_util.h
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr int Strlen(const T* str) {
|
||||||
|
ASSERT(str != nullptr);
|
||||||
|
|
||||||
|
int length = 0;
|
||||||
|
while (*str++) {
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr int Strnlen(const T* str, std::size_t count) {
|
||||||
|
return Strnlen(str, static_cast<int>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr int Strnlen(const T* str, int count) {
|
||||||
|
ASSERT(str != nullptr);
|
||||||
|
ASSERT(count >= 0);
|
||||||
|
|
||||||
|
int length = 0;
|
||||||
|
while (count-- && *str++) {
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr int Strncmp(const T* lhs, const T* rhs, std::size_t count) {
|
||||||
|
return Strncmp(lhs, rhs, static_cast<int>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr int Strncmp(const T* lhs, const T* rhs, int count) {
|
||||||
|
ASSERT(lhs != nullptr);
|
||||||
|
ASSERT(rhs != nullptr);
|
||||||
|
ASSERT(count >= 0);
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
T l, r;
|
||||||
|
do {
|
||||||
|
l = *(lhs++);
|
||||||
|
r = *(rhs++);
|
||||||
|
} while (l && (l == r) && (--count));
|
||||||
|
|
||||||
|
return l - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static constexpr int Strlcpy(T* dst, const T* src, std::size_t count) {
|
||||||
|
return Strlcpy<T>(dst, src, static_cast<int>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static constexpr int Strlcpy(T* dst, const T* src, int count) {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(src != nullptr);
|
||||||
|
|
||||||
|
const T* cur = src;
|
||||||
|
if (count > 0) {
|
||||||
|
while ((--count) && *cur) {
|
||||||
|
*(dst++) = *(cur++);
|
||||||
|
}
|
||||||
|
*dst = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (*cur) {
|
||||||
|
cur++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<int>(cur - src);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CharacterEncodingResult {
|
||||||
|
CharacterEncodingResult_Success = 0,
|
||||||
|
CharacterEncodingResult_InsufficientLength = 1,
|
||||||
|
CharacterEncodingResult_InvalidFormat = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace impl {
|
||||||
|
|
||||||
|
class CharacterEncodingHelper {
|
||||||
|
public:
|
||||||
|
static constexpr int8_t Utf8NBytesInnerTable[0x100 + 1] = {
|
||||||
|
-1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
|
||||||
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr char GetUtf8NBytes(size_t i) {
|
||||||
|
return static_cast<char>(Utf8NBytesInnerTable[1 + i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace impl
|
||||||
|
|
||||||
|
constexpr inline CharacterEncodingResult ConvertCharacterUtf8ToUtf32(u32* dst, const char* src) {
|
||||||
|
// Check pre-conditions
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(src != nullptr);
|
||||||
|
|
||||||
|
// Perform the conversion
|
||||||
|
const auto* p = src;
|
||||||
|
switch (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[0]))) {
|
||||||
|
case 1:
|
||||||
|
*dst = static_cast<u32>(p[0]);
|
||||||
|
return CharacterEncodingResult_Success;
|
||||||
|
case 2:
|
||||||
|
if ((static_cast<u32>(p[0]) & 0x1E) != 0) {
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) ==
|
||||||
|
0) {
|
||||||
|
*dst = (static_cast<u32>(p[0] & 0x1F) << 6) | (static_cast<u32>(p[1] & 0x3F) << 0);
|
||||||
|
return CharacterEncodingResult_Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0) {
|
||||||
|
const u32 c = (static_cast<u32>(p[0] & 0xF) << 12) |
|
||||||
|
(static_cast<u32>(p[1] & 0x3F) << 6) |
|
||||||
|
(static_cast<u32>(p[2] & 0x3F) << 0);
|
||||||
|
if ((c & 0xF800) != 0 && (c & 0xF800) != 0xD800) {
|
||||||
|
*dst = c;
|
||||||
|
return CharacterEncodingResult_Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
case 4:
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[3])) == 0) {
|
||||||
|
const u32 c =
|
||||||
|
(static_cast<u32>(p[0] & 0x7) << 18) | (static_cast<u32>(p[1] & 0x3F) << 12) |
|
||||||
|
(static_cast<u32>(p[2] & 0x3F) << 6) | (static_cast<u32>(p[3] & 0x3F) << 0);
|
||||||
|
if (c >= 0x10000 && c < 0x110000) {
|
||||||
|
*dst = c;
|
||||||
|
return CharacterEncodingResult_Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We failed to convert
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline CharacterEncodingResult PickOutCharacterFromUtf8String(char* dst,
|
||||||
|
const char** str) {
|
||||||
|
// Check pre-conditions
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(str != nullptr);
|
||||||
|
ASSERT(*str != nullptr);
|
||||||
|
|
||||||
|
// Clear the output
|
||||||
|
dst[0] = 0;
|
||||||
|
dst[1] = 0;
|
||||||
|
dst[2] = 0;
|
||||||
|
dst[3] = 0;
|
||||||
|
|
||||||
|
// Perform the conversion
|
||||||
|
const auto* p = *str;
|
||||||
|
u32 c = static_cast<u32>(*p);
|
||||||
|
switch (impl::CharacterEncodingHelper::GetUtf8NBytes(c)) {
|
||||||
|
case 1:
|
||||||
|
dst[0] = (*str)[0];
|
||||||
|
++(*str);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if ((p[0] & 0x1E) != 0) {
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) ==
|
||||||
|
0) {
|
||||||
|
c = (static_cast<u32>(p[0] & 0x1F) << 6) | (static_cast<u32>(p[1] & 0x3F) << 0);
|
||||||
|
dst[0] = (*str)[0];
|
||||||
|
dst[1] = (*str)[1];
|
||||||
|
(*str) += 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
case 3:
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0) {
|
||||||
|
c = (static_cast<u32>(p[0] & 0xF) << 12) | (static_cast<u32>(p[1] & 0x3F) << 6) |
|
||||||
|
(static_cast<u32>(p[2] & 0x3F) << 0);
|
||||||
|
if ((c & 0xF800) != 0 && (c & 0xF800) != 0xD800) {
|
||||||
|
dst[0] = (*str)[0];
|
||||||
|
dst[1] = (*str)[1];
|
||||||
|
dst[2] = (*str)[2];
|
||||||
|
(*str) += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
case 4:
|
||||||
|
if (impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[1])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[2])) == 0 &&
|
||||||
|
impl::CharacterEncodingHelper::GetUtf8NBytes(static_cast<unsigned char>(p[3])) == 0) {
|
||||||
|
c = (static_cast<u32>(p[0] & 0x7) << 18) | (static_cast<u32>(p[1] & 0x3F) << 12) |
|
||||||
|
(static_cast<u32>(p[2] & 0x3F) << 6) | (static_cast<u32>(p[3] & 0x3F) << 0);
|
||||||
|
if (c >= 0x10000 && c < 0x110000) {
|
||||||
|
dst[0] = (*str)[0];
|
||||||
|
dst[1] = (*str)[1];
|
||||||
|
dst[2] = (*str)[2];
|
||||||
|
dst[3] = (*str)[3];
|
||||||
|
(*str) += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
default:
|
||||||
|
return CharacterEncodingResult_InvalidFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CharacterEncodingResult_Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
91
core/fs/fsa/fs_i_directory.h
Normal file
91
core/fs/fsa/fs_i_directory.h
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fs_directory.h"
|
||||||
|
#include "core/file_sys/fs_file.h"
|
||||||
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
|
#include "core/file_sys/savedata_factory.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys::Fsa {
|
||||||
|
|
||||||
|
class IDirectory {
|
||||||
|
public:
|
||||||
|
explicit IDirectory(VirtualDir backend_, OpenDirectoryMode mode)
|
||||||
|
: backend(std::move(backend_)) {
|
||||||
|
// TODO(DarkLordZach): Verify that this is the correct behavior.
|
||||||
|
// Build entry index now to save time later.
|
||||||
|
if (True(mode & OpenDirectoryMode::Directory)) {
|
||||||
|
BuildEntryIndex(backend->GetSubdirectories(), DirectoryEntryType::Directory);
|
||||||
|
}
|
||||||
|
if (True(mode & OpenDirectoryMode::File)) {
|
||||||
|
BuildEntryIndex(backend->GetFiles(), DirectoryEntryType::File);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
virtual ~IDirectory() {}
|
||||||
|
|
||||||
|
Result Read(s64* out_count, DirectoryEntry* out_entries, s64 max_entries) {
|
||||||
|
R_UNLESS(out_count != nullptr, ResultNullptrArgument);
|
||||||
|
if (max_entries == 0) {
|
||||||
|
*out_count = 0;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
R_UNLESS(out_entries != nullptr, ResultNullptrArgument);
|
||||||
|
R_UNLESS(max_entries > 0, ResultInvalidArgument);
|
||||||
|
R_RETURN(this->DoRead(out_count, out_entries, max_entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryCount(s64* out) {
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
R_RETURN(this->DoGetEntryCount(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result DoRead(s64* out_count, DirectoryEntry* out_entries, s64 max_entries) {
|
||||||
|
const u64 actual_entries =
|
||||||
|
std::min(static_cast<u64>(max_entries), entries.size() - next_entry_index);
|
||||||
|
const auto* begin = reinterpret_cast<u8*>(entries.data() + next_entry_index);
|
||||||
|
const auto* end = reinterpret_cast<u8*>(entries.data() + next_entry_index + actual_entries);
|
||||||
|
const auto range_size = static_cast<std::size_t>(std::distance(begin, end));
|
||||||
|
|
||||||
|
next_entry_index += actual_entries;
|
||||||
|
*out_count = actual_entries;
|
||||||
|
|
||||||
|
std::memcpy(out_entries, begin, range_size);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetEntryCount(s64* out) {
|
||||||
|
*out = entries.size() - next_entry_index;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this when VFS is gone
|
||||||
|
template <typename T>
|
||||||
|
void BuildEntryIndex(const std::vector<T>& new_data, DirectoryEntryType type) {
|
||||||
|
entries.reserve(entries.size() + new_data.size());
|
||||||
|
|
||||||
|
for (const auto& new_entry : new_data) {
|
||||||
|
auto name = new_entry->GetName();
|
||||||
|
|
||||||
|
if (type == DirectoryEntryType::File && name == GetSaveDataSizeFileName()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.emplace_back(name, static_cast<s8>(type),
|
||||||
|
type == DirectoryEntryType::Directory ? 0 : new_entry->GetSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir backend;
|
||||||
|
std::vector<DirectoryEntry> entries;
|
||||||
|
u64 next_entry_index = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys::Fsa
|
||||||
167
core/fs/fsa/fs_i_file.h
Normal file
167
core/fs/fsa/fs_i_file.h
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/overflow.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fs_file.h"
|
||||||
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
|
#include "core/file_sys/fs_operate_range.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys::Fsa {
|
||||||
|
|
||||||
|
class IFile {
|
||||||
|
public:
|
||||||
|
explicit IFile(VirtualFile backend_) : backend(std::move(backend_)) {}
|
||||||
|
virtual ~IFile() {}
|
||||||
|
|
||||||
|
Result Read(size_t* out, s64 offset, void* buffer, size_t size, const ReadOption& option) {
|
||||||
|
// Check that we have an output pointer
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// If we have nothing to read, just succeed
|
||||||
|
if (size == 0) {
|
||||||
|
*out = 0;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the read is valid
|
||||||
|
R_UNLESS(buffer != nullptr, ResultNullptrArgument);
|
||||||
|
R_UNLESS(offset >= 0, ResultOutOfRange);
|
||||||
|
R_UNLESS(Common::CanAddWithoutOverflow<s64>(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Do the read
|
||||||
|
R_RETURN(this->DoRead(out, offset, buffer, size, option));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Read(size_t* out, s64 offset, void* buffer, size_t size) {
|
||||||
|
R_RETURN(this->Read(out, offset, buffer, size, ReadOption::None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetSize(s64* out) {
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
R_RETURN(this->DoGetSize(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Flush() {
|
||||||
|
R_RETURN(this->DoFlush());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Write(s64 offset, const void* buffer, size_t size, const WriteOption& option) {
|
||||||
|
// Handle the zero-size case
|
||||||
|
if (size == 0) {
|
||||||
|
if (option.HasFlushFlag()) {
|
||||||
|
R_TRY(this->Flush());
|
||||||
|
}
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the write is valid
|
||||||
|
R_UNLESS(buffer != nullptr, ResultNullptrArgument);
|
||||||
|
R_UNLESS(offset >= 0, ResultOutOfRange);
|
||||||
|
R_UNLESS(Common::CanAddWithoutOverflow<s64>(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
R_RETURN(this->DoWrite(offset, buffer, size, option));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result SetSize(s64 size) {
|
||||||
|
R_UNLESS(size >= 0, ResultOutOfRange);
|
||||||
|
R_RETURN(this->DoSetSize(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OperateRange(void* dst, size_t dst_size, OperationId op_id, s64 offset, s64 size,
|
||||||
|
const void* src, size_t src_size) {
|
||||||
|
R_RETURN(this->DoOperateRange(dst, dst_size, op_id, offset, size, src, src_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OperateRange(OperationId op_id, s64 offset, s64 size) {
|
||||||
|
R_RETURN(this->DoOperateRange(nullptr, 0, op_id, offset, size, nullptr, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Result DryRead(size_t* out, s64 offset, size_t size, const ReadOption& option,
|
||||||
|
OpenMode open_mode) {
|
||||||
|
// Check that we can read
|
||||||
|
R_UNLESS(static_cast<u32>(open_mode & OpenMode::Read) != 0, ResultReadNotPermitted);
|
||||||
|
|
||||||
|
// Get the file size, and validate our offset
|
||||||
|
s64 file_size = 0;
|
||||||
|
R_TRY(this->DoGetSize(std::addressof(file_size)));
|
||||||
|
R_UNLESS(offset <= file_size, ResultOutOfRange);
|
||||||
|
|
||||||
|
*out = static_cast<size_t>(std::min(file_size - offset, static_cast<s64>(size)));
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DrySetSize(s64 size, OpenMode open_mode) {
|
||||||
|
// Check that we can write
|
||||||
|
R_UNLESS(static_cast<u32>(open_mode & OpenMode::Write) != 0, ResultWriteNotPermitted);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DryWrite(bool* out_append, s64 offset, size_t size, const WriteOption& option,
|
||||||
|
OpenMode open_mode) {
|
||||||
|
// Check that we can write
|
||||||
|
R_UNLESS(static_cast<u32>(open_mode & OpenMode::Write) != 0, ResultWriteNotPermitted);
|
||||||
|
|
||||||
|
// Get the file size
|
||||||
|
s64 file_size = 0;
|
||||||
|
R_TRY(this->DoGetSize(&file_size));
|
||||||
|
|
||||||
|
// Determine if we need to append
|
||||||
|
*out_append = false;
|
||||||
|
if (file_size < offset + static_cast<s64>(size)) {
|
||||||
|
R_UNLESS(static_cast<u32>(open_mode & OpenMode::AllowAppend) != 0,
|
||||||
|
ResultFileExtensionWithoutOpenModeAllowAppend);
|
||||||
|
*out_append = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result DoRead(size_t* out, s64 offset, void* buffer, size_t size, const ReadOption& option) {
|
||||||
|
const auto read_size = backend->Read(static_cast<u8*>(buffer), size, offset);
|
||||||
|
*out = read_size;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetSize(s64* out) {
|
||||||
|
*out = backend->GetSize();
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoFlush() {
|
||||||
|
// Exists for SDK compatibiltity -- No need to flush file.
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoWrite(s64 offset, const void* buffer, size_t size, const WriteOption& option) {
|
||||||
|
const std::size_t written = backend->Write(static_cast<const u8*>(buffer), size, offset);
|
||||||
|
|
||||||
|
ASSERT_MSG(written == size,
|
||||||
|
"Could not write all bytes to file (requested={:016X}, actual={:016X}).", size,
|
||||||
|
written);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoSetSize(s64 size) {
|
||||||
|
backend->Resize(size);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoOperateRange(void* dst, size_t dst_size, OperationId op_id, s64 offset, s64 size,
|
||||||
|
const void* src, size_t src_size) {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile backend;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys::Fsa
|
||||||
206
core/fs/fsa/fs_i_filesystem.h
Normal file
206
core/fs/fsa/fs_i_filesystem.h
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
|
#include "core/file_sys/fs_path.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
|
||||||
|
namespace FileSys::Fsa {
|
||||||
|
|
||||||
|
class IFile;
|
||||||
|
class IDirectory;
|
||||||
|
|
||||||
|
enum class QueryId : u32 {
|
||||||
|
SetConcatenationFileAttribute = 0,
|
||||||
|
UpdateMac = 1,
|
||||||
|
IsSignedSystemPartitionOnSdCardValid = 2,
|
||||||
|
QueryUnpreparedFileInformation = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
class IFileSystem {
|
||||||
|
public:
|
||||||
|
explicit IFileSystem(VirtualDir backend_) : backend{std::move(backend_)} {}
|
||||||
|
virtual ~IFileSystem() {}
|
||||||
|
|
||||||
|
Result CreateFile(const Path& path, s64 size, CreateOption option) {
|
||||||
|
R_UNLESS(size >= 0, ResultOutOfRange);
|
||||||
|
R_RETURN(this->DoCreateFile(path, size, static_cast<int>(option)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result CreateFile(const Path& path, s64 size) {
|
||||||
|
R_RETURN(this->CreateFile(path, size, CreateOption::None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DeleteFile(const Path& path) {
|
||||||
|
R_RETURN(this->DoDeleteFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result CreateDirectory(const Path& path) {
|
||||||
|
R_RETURN(this->DoCreateDirectory(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DeleteDirectory(const Path& path) {
|
||||||
|
R_RETURN(this->DoDeleteDirectory(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DeleteDirectoryRecursively(const Path& path) {
|
||||||
|
R_RETURN(this->DoDeleteDirectoryRecursively(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result RenameFile(const Path& old_path, const Path& new_path) {
|
||||||
|
R_RETURN(this->DoRenameFile(old_path, new_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result RenameDirectory(const Path& old_path, const Path& new_path) {
|
||||||
|
R_RETURN(this->DoRenameDirectory(old_path, new_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryType(DirectoryEntryType* out, const Path& path) {
|
||||||
|
R_RETURN(this->DoGetEntryType(out, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OpenFile(VirtualFile* out_file, const Path& path, OpenMode mode) {
|
||||||
|
R_UNLESS(out_file != nullptr, ResultNullptrArgument);
|
||||||
|
R_UNLESS(static_cast<u32>(mode & OpenMode::ReadWrite) != 0, ResultInvalidOpenMode);
|
||||||
|
R_UNLESS(static_cast<u32>(mode & ~OpenMode::All) == 0, ResultInvalidOpenMode);
|
||||||
|
R_RETURN(this->DoOpenFile(out_file, path, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OpenDirectory(VirtualDir* out_dir, const Path& path, OpenDirectoryMode mode) {
|
||||||
|
R_UNLESS(out_dir != nullptr, ResultNullptrArgument);
|
||||||
|
R_UNLESS(static_cast<u64>(mode & OpenDirectoryMode::All) != 0, ResultInvalidOpenMode);
|
||||||
|
R_UNLESS(static_cast<u64>(
|
||||||
|
mode & ~(OpenDirectoryMode::All | OpenDirectoryMode::NotRequireFileSize)) == 0,
|
||||||
|
ResultInvalidOpenMode);
|
||||||
|
R_RETURN(this->DoOpenDirectory(out_dir, path, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Commit() {
|
||||||
|
R_RETURN(this->DoCommit());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetFreeSpaceSize(s64* out, const Path& path) {
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
R_RETURN(this->DoGetFreeSpaceSize(out, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetTotalSpaceSize(s64* out, const Path& path) {
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
R_RETURN(this->DoGetTotalSpaceSize(out, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result CleanDirectoryRecursively(const Path& path) {
|
||||||
|
R_RETURN(this->DoCleanDirectoryRecursively(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetFileTimeStampRaw(FileTimeStampRaw* out, const Path& path) {
|
||||||
|
R_UNLESS(out != nullptr, ResultNullptrArgument);
|
||||||
|
R_RETURN(this->DoGetFileTimeStampRaw(out, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result QueryEntry(char* dst, size_t dst_size, const char* src, size_t src_size, QueryId query,
|
||||||
|
const Path& path) {
|
||||||
|
R_RETURN(this->DoQueryEntry(dst, dst_size, src, src_size, query, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// These aren't accessible as commands
|
||||||
|
Result CommitProvisionally(s64 counter) {
|
||||||
|
R_RETURN(this->DoCommitProvisionally(counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Rollback() {
|
||||||
|
R_RETURN(this->DoRollback());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Flush() {
|
||||||
|
R_RETURN(this->DoFlush());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result DoCreateFile(const Path& path, s64 size, int flags) {
|
||||||
|
R_RETURN(backend.CreateFile(path.GetString(), size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoDeleteFile(const Path& path) {
|
||||||
|
R_RETURN(backend.DeleteFile(path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoCreateDirectory(const Path& path) {
|
||||||
|
R_RETURN(backend.CreateDirectory(path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoDeleteDirectory(const Path& path) {
|
||||||
|
R_RETURN(backend.DeleteDirectory(path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoDeleteDirectoryRecursively(const Path& path) {
|
||||||
|
R_RETURN(backend.DeleteDirectoryRecursively(path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoRenameFile(const Path& old_path, const Path& new_path) {
|
||||||
|
R_RETURN(backend.RenameFile(old_path.GetString(), new_path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoRenameDirectory(const Path& old_path, const Path& new_path) {
|
||||||
|
R_RETURN(backend.RenameDirectory(old_path.GetString(), new_path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetEntryType(DirectoryEntryType* out, const Path& path) {
|
||||||
|
R_RETURN(backend.GetEntryType(out, path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoOpenFile(VirtualFile* out_file, const Path& path, OpenMode mode) {
|
||||||
|
R_RETURN(backend.OpenFile(out_file, path.GetString(), mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoOpenDirectory(VirtualDir* out_directory, const Path& path, OpenDirectoryMode mode) {
|
||||||
|
R_RETURN(backend.OpenDirectory(out_directory, path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoCommit() {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetFreeSpaceSize(s64* out, const Path& path) {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetTotalSpaceSize(s64* out, const Path& path) {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoCleanDirectoryRecursively(const Path& path) {
|
||||||
|
R_RETURN(backend.CleanDirectoryRecursively(path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoGetFileTimeStampRaw(FileTimeStampRaw* out, const Path& path) {
|
||||||
|
R_RETURN(backend.GetFileTimeStampRaw(out, path.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoQueryEntry(char* dst, size_t dst_size, const char* src, size_t src_size, QueryId query,
|
||||||
|
const Path& path) {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These aren't accessible as commands
|
||||||
|
Result DoCommitProvisionally(s64 counter) {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoRollback() {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DoFlush() {
|
||||||
|
R_THROW(ResultNotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Service::FileSystem::VfsDirectoryServiceWrapper backend;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys::Fsa
|
||||||
354
core/fs/fsmitm_romfsbuild.cpp
Normal file
354
core/fs/fsmitm_romfsbuild.cpp
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "core/file_sys/fsmitm_romfsbuild.h"
|
||||||
|
#include "core/file_sys/ips_layer.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u64 FS_MAX_PATH = 0x301;
|
||||||
|
|
||||||
|
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||||
|
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
||||||
|
|
||||||
|
// Types for building a RomFS.
|
||||||
|
struct RomFSHeader {
|
||||||
|
u64 header_size;
|
||||||
|
u64 dir_hash_table_ofs;
|
||||||
|
u64 dir_hash_table_size;
|
||||||
|
u64 dir_table_ofs;
|
||||||
|
u64 dir_table_size;
|
||||||
|
u64 file_hash_table_ofs;
|
||||||
|
u64 file_hash_table_size;
|
||||||
|
u64 file_table_ofs;
|
||||||
|
u64 file_table_size;
|
||||||
|
u64 file_partition_ofs;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RomFSHeader) == 0x50, "RomFSHeader has incorrect size.");
|
||||||
|
|
||||||
|
struct RomFSDirectoryEntry {
|
||||||
|
u32 parent;
|
||||||
|
u32 sibling;
|
||||||
|
u32 child;
|
||||||
|
u32 file;
|
||||||
|
u32 hash;
|
||||||
|
u32 name_size;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RomFSDirectoryEntry) == 0x18, "RomFSDirectoryEntry has incorrect size.");
|
||||||
|
|
||||||
|
struct RomFSFileEntry {
|
||||||
|
u32 parent;
|
||||||
|
u32 sibling;
|
||||||
|
u64 offset;
|
||||||
|
u64 size;
|
||||||
|
u32 hash;
|
||||||
|
u32 name_size;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RomFSFileEntry) == 0x20, "RomFSFileEntry has incorrect size.");
|
||||||
|
|
||||||
|
struct RomFSBuildFileContext;
|
||||||
|
|
||||||
|
struct RomFSBuildDirectoryContext {
|
||||||
|
std::string path;
|
||||||
|
u32 cur_path_ofs = 0;
|
||||||
|
u32 path_len = 0;
|
||||||
|
u32 entry_offset = 0;
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> parent;
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> child;
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> sibling;
|
||||||
|
std::shared_ptr<RomFSBuildFileContext> file;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RomFSBuildFileContext {
|
||||||
|
std::string path;
|
||||||
|
u32 cur_path_ofs = 0;
|
||||||
|
u32 path_len = 0;
|
||||||
|
u32 entry_offset = 0;
|
||||||
|
u64 offset = 0;
|
||||||
|
u64 size = 0;
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> parent;
|
||||||
|
std::shared_ptr<RomFSBuildFileContext> sibling;
|
||||||
|
VirtualFile source;
|
||||||
|
};
|
||||||
|
|
||||||
|
static u32 romfs_calc_path_hash(u32 parent, std::string_view path, u32 start,
|
||||||
|
std::size_t path_len) {
|
||||||
|
u32 hash = parent ^ 123456789;
|
||||||
|
for (u32 i = 0; i < path_len; i++) {
|
||||||
|
hash = (hash >> 5) | (hash << 27);
|
||||||
|
hash ^= path[start + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
static u64 romfs_get_hash_table_count(u64 num_entries) {
|
||||||
|
if (num_entries < 3) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_entries < 19) {
|
||||||
|
return num_entries | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 count = num_entries;
|
||||||
|
while (count % 2 == 0 || count % 3 == 0 || count % 5 == 0 || count % 7 == 0 ||
|
||||||
|
count % 11 == 0 || count % 13 == 0 || count % 17 == 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RomFSBuildContext::VisitDirectory(VirtualDir romfs_dir, VirtualDir ext_dir,
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> parent) {
|
||||||
|
for (auto& child_romfs_file : romfs_dir->GetFiles()) {
|
||||||
|
const auto name = child_romfs_file->GetName();
|
||||||
|
const auto child = std::make_shared<RomFSBuildFileContext>();
|
||||||
|
// Set child's path.
|
||||||
|
child->cur_path_ofs = parent->path_len + 1;
|
||||||
|
child->path_len = child->cur_path_ofs + static_cast<u32>(name.size());
|
||||||
|
child->path = parent->path + "/" + name;
|
||||||
|
|
||||||
|
if (ext_dir != nullptr && ext_dir->GetFile(name + ".stub") != nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check on path_len
|
||||||
|
ASSERT(child->path_len < FS_MAX_PATH);
|
||||||
|
|
||||||
|
child->source = std::move(child_romfs_file);
|
||||||
|
|
||||||
|
if (ext_dir != nullptr) {
|
||||||
|
if (const auto ips = ext_dir->GetFile(name + ".ips")) {
|
||||||
|
if (auto patched = PatchIPS(child->source, ips)) {
|
||||||
|
child->source = std::move(patched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child->size = child->source->GetSize();
|
||||||
|
|
||||||
|
AddFile(parent, std::move(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& child_romfs_dir : romfs_dir->GetSubdirectories()) {
|
||||||
|
const auto name = child_romfs_dir->GetName();
|
||||||
|
const auto child = std::make_shared<RomFSBuildDirectoryContext>();
|
||||||
|
// Set child's path.
|
||||||
|
child->cur_path_ofs = parent->path_len + 1;
|
||||||
|
child->path_len = child->cur_path_ofs + static_cast<u32>(name.size());
|
||||||
|
child->path = parent->path + "/" + name;
|
||||||
|
|
||||||
|
if (ext_dir != nullptr && ext_dir->GetFile(name + ".stub") != nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check on path_len
|
||||||
|
ASSERT(child->path_len < FS_MAX_PATH);
|
||||||
|
|
||||||
|
if (!AddDirectory(parent, child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto child_ext_dir = ext_dir != nullptr ? ext_dir->GetSubdirectory(name) : nullptr;
|
||||||
|
this->VisitDirectory(child_romfs_dir, child_ext_dir, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RomFSBuildContext::AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> dir_ctx) {
|
||||||
|
// Add a new directory.
|
||||||
|
num_dirs++;
|
||||||
|
dir_table_size +=
|
||||||
|
sizeof(RomFSDirectoryEntry) + Common::AlignUp(dir_ctx->path_len - dir_ctx->cur_path_ofs, 4);
|
||||||
|
dir_ctx->parent = std::move(parent_dir_ctx);
|
||||||
|
directories.emplace_back(std::move(dir_ctx));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RomFSBuildContext::AddFile(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
|
||||||
|
std::shared_ptr<RomFSBuildFileContext> file_ctx) {
|
||||||
|
// Add a new file.
|
||||||
|
num_files++;
|
||||||
|
file_table_size +=
|
||||||
|
sizeof(RomFSFileEntry) + Common::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4);
|
||||||
|
file_ctx->parent = std::move(parent_dir_ctx);
|
||||||
|
files.emplace_back(std::move(file_ctx));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RomFSBuildContext::RomFSBuildContext(VirtualDir base_, VirtualDir ext_)
|
||||||
|
: base(std::move(base_)), ext(std::move(ext_)) {
|
||||||
|
root = std::make_shared<RomFSBuildDirectoryContext>();
|
||||||
|
root->path = "\0";
|
||||||
|
directories.emplace_back(root);
|
||||||
|
num_dirs = 1;
|
||||||
|
dir_table_size = 0x18;
|
||||||
|
|
||||||
|
VisitDirectory(base, ext, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
RomFSBuildContext::~RomFSBuildContext() = default;
|
||||||
|
|
||||||
|
std::vector<std::pair<u64, VirtualFile>> RomFSBuildContext::Build() {
|
||||||
|
const u64 dir_hash_table_entry_count = romfs_get_hash_table_count(num_dirs);
|
||||||
|
const u64 file_hash_table_entry_count = romfs_get_hash_table_count(num_files);
|
||||||
|
dir_hash_table_size = 4 * dir_hash_table_entry_count;
|
||||||
|
file_hash_table_size = 4 * file_hash_table_entry_count;
|
||||||
|
|
||||||
|
// Assign metadata pointers.
|
||||||
|
RomFSHeader header{};
|
||||||
|
|
||||||
|
std::vector<u8> metadata(file_hash_table_size + file_table_size + dir_hash_table_size +
|
||||||
|
dir_table_size);
|
||||||
|
u32* const dir_hash_table_pointer = reinterpret_cast<u32*>(metadata.data());
|
||||||
|
u8* const dir_table_pointer = metadata.data() + dir_hash_table_size;
|
||||||
|
u32* const file_hash_table_pointer =
|
||||||
|
reinterpret_cast<u32*>(metadata.data() + dir_hash_table_size + dir_table_size);
|
||||||
|
u8* const file_table_pointer =
|
||||||
|
metadata.data() + dir_hash_table_size + dir_table_size + file_hash_table_size;
|
||||||
|
|
||||||
|
std::span<u32> dir_hash_table(dir_hash_table_pointer, dir_hash_table_entry_count);
|
||||||
|
std::span<u32> file_hash_table(file_hash_table_pointer, file_hash_table_entry_count);
|
||||||
|
std::span<u8> dir_table(dir_table_pointer, dir_table_size);
|
||||||
|
std::span<u8> file_table(file_table_pointer, file_table_size);
|
||||||
|
|
||||||
|
// Initialize hash tables.
|
||||||
|
std::memset(dir_hash_table.data(), 0xFF, dir_hash_table.size_bytes());
|
||||||
|
std::memset(file_hash_table.data(), 0xFF, file_hash_table.size_bytes());
|
||||||
|
|
||||||
|
// Sort tables by name.
|
||||||
|
std::sort(files.begin(), files.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a->path < b->path; });
|
||||||
|
std::sort(directories.begin(), directories.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a->path < b->path; });
|
||||||
|
|
||||||
|
// Determine file offsets.
|
||||||
|
u32 entry_offset = 0;
|
||||||
|
std::shared_ptr<RomFSBuildFileContext> prev_file = nullptr;
|
||||||
|
for (const auto& cur_file : files) {
|
||||||
|
file_partition_size = Common::AlignUp(file_partition_size, 16);
|
||||||
|
cur_file->offset = file_partition_size;
|
||||||
|
file_partition_size += cur_file->size;
|
||||||
|
cur_file->entry_offset = entry_offset;
|
||||||
|
entry_offset +=
|
||||||
|
static_cast<u32>(sizeof(RomFSFileEntry) +
|
||||||
|
Common::AlignUp(cur_file->path_len - cur_file->cur_path_ofs, 4));
|
||||||
|
prev_file = cur_file;
|
||||||
|
}
|
||||||
|
// Assign deferred parent/sibling ownership.
|
||||||
|
for (auto it = files.rbegin(); it != files.rend(); ++it) {
|
||||||
|
auto& cur_file = *it;
|
||||||
|
cur_file->sibling = cur_file->parent->file;
|
||||||
|
cur_file->parent->file = cur_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine directory offsets.
|
||||||
|
entry_offset = 0;
|
||||||
|
for (const auto& cur_dir : directories) {
|
||||||
|
cur_dir->entry_offset = entry_offset;
|
||||||
|
entry_offset +=
|
||||||
|
static_cast<u32>(sizeof(RomFSDirectoryEntry) +
|
||||||
|
Common::AlignUp(cur_dir->path_len - cur_dir->cur_path_ofs, 4));
|
||||||
|
}
|
||||||
|
// Assign deferred parent/sibling ownership.
|
||||||
|
for (auto it = directories.rbegin(); (*it) != root; ++it) {
|
||||||
|
auto& cur_dir = *it;
|
||||||
|
cur_dir->sibling = cur_dir->parent->child;
|
||||||
|
cur_dir->parent->child = cur_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output map.
|
||||||
|
std::vector<std::pair<u64, VirtualFile>> out;
|
||||||
|
out.reserve(num_files + 2);
|
||||||
|
|
||||||
|
// Set header fields.
|
||||||
|
header.header_size = sizeof(RomFSHeader);
|
||||||
|
header.file_hash_table_size = file_hash_table_size;
|
||||||
|
header.file_table_size = file_table_size;
|
||||||
|
header.dir_hash_table_size = dir_hash_table_size;
|
||||||
|
header.dir_table_size = dir_table_size;
|
||||||
|
header.file_partition_ofs = ROMFS_FILEPARTITION_OFS;
|
||||||
|
header.dir_hash_table_ofs = Common::AlignUp(header.file_partition_ofs + file_partition_size, 4);
|
||||||
|
header.dir_table_ofs = header.dir_hash_table_ofs + header.dir_hash_table_size;
|
||||||
|
header.file_hash_table_ofs = header.dir_table_ofs + header.dir_table_size;
|
||||||
|
header.file_table_ofs = header.file_hash_table_ofs + header.file_hash_table_size;
|
||||||
|
|
||||||
|
std::vector<u8> header_data(sizeof(RomFSHeader));
|
||||||
|
std::memcpy(header_data.data(), &header, header_data.size());
|
||||||
|
out.emplace_back(0, std::make_shared<VectorVfsFile>(std::move(header_data)));
|
||||||
|
|
||||||
|
// Populate file tables.
|
||||||
|
for (const auto& cur_file : files) {
|
||||||
|
RomFSFileEntry cur_entry{};
|
||||||
|
|
||||||
|
cur_entry.parent = cur_file->parent->entry_offset;
|
||||||
|
cur_entry.sibling =
|
||||||
|
cur_file->sibling == nullptr ? ROMFS_ENTRY_EMPTY : cur_file->sibling->entry_offset;
|
||||||
|
cur_entry.offset = cur_file->offset;
|
||||||
|
cur_entry.size = cur_file->size;
|
||||||
|
|
||||||
|
const auto name_size = cur_file->path_len - cur_file->cur_path_ofs;
|
||||||
|
const auto hash = romfs_calc_path_hash(cur_file->parent->entry_offset, cur_file->path,
|
||||||
|
cur_file->cur_path_ofs, name_size);
|
||||||
|
cur_entry.hash = file_hash_table[hash % file_hash_table_entry_count];
|
||||||
|
file_hash_table[hash % file_hash_table_entry_count] = cur_file->entry_offset;
|
||||||
|
|
||||||
|
cur_entry.name_size = name_size;
|
||||||
|
|
||||||
|
out.emplace_back(cur_file->offset + ROMFS_FILEPARTITION_OFS, std::move(cur_file->source));
|
||||||
|
std::memcpy(file_table.data() + cur_file->entry_offset, &cur_entry, sizeof(RomFSFileEntry));
|
||||||
|
std::memset(file_table.data() + cur_file->entry_offset + sizeof(RomFSFileEntry), 0,
|
||||||
|
Common::AlignUp(cur_entry.name_size, 4));
|
||||||
|
std::memcpy(file_table.data() + cur_file->entry_offset + sizeof(RomFSFileEntry),
|
||||||
|
cur_file->path.data() + cur_file->cur_path_ofs, name_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate dir tables.
|
||||||
|
for (const auto& cur_dir : directories) {
|
||||||
|
RomFSDirectoryEntry cur_entry{};
|
||||||
|
|
||||||
|
cur_entry.parent = cur_dir == root ? 0 : cur_dir->parent->entry_offset;
|
||||||
|
cur_entry.sibling =
|
||||||
|
cur_dir->sibling == nullptr ? ROMFS_ENTRY_EMPTY : cur_dir->sibling->entry_offset;
|
||||||
|
cur_entry.child =
|
||||||
|
cur_dir->child == nullptr ? ROMFS_ENTRY_EMPTY : cur_dir->child->entry_offset;
|
||||||
|
cur_entry.file = cur_dir->file == nullptr ? ROMFS_ENTRY_EMPTY : cur_dir->file->entry_offset;
|
||||||
|
|
||||||
|
const auto name_size = cur_dir->path_len - cur_dir->cur_path_ofs;
|
||||||
|
const auto hash = romfs_calc_path_hash(cur_dir == root ? 0 : cur_dir->parent->entry_offset,
|
||||||
|
cur_dir->path, cur_dir->cur_path_ofs, name_size);
|
||||||
|
cur_entry.hash = dir_hash_table[hash % dir_hash_table_entry_count];
|
||||||
|
dir_hash_table[hash % dir_hash_table_entry_count] = cur_dir->entry_offset;
|
||||||
|
|
||||||
|
cur_entry.name_size = name_size;
|
||||||
|
|
||||||
|
std::memcpy(dir_table.data() + cur_dir->entry_offset, &cur_entry,
|
||||||
|
sizeof(RomFSDirectoryEntry));
|
||||||
|
std::memset(dir_table.data() + cur_dir->entry_offset + sizeof(RomFSDirectoryEntry), 0,
|
||||||
|
Common::AlignUp(cur_entry.name_size, 4));
|
||||||
|
std::memcpy(dir_table.data() + cur_dir->entry_offset + sizeof(RomFSDirectoryEntry),
|
||||||
|
cur_dir->path.data() + cur_dir->cur_path_ofs, name_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write metadata.
|
||||||
|
out.emplace_back(header.dir_hash_table_ofs,
|
||||||
|
std::make_shared<VectorVfsFile>(std::move(metadata)));
|
||||||
|
|
||||||
|
// Sort the output.
|
||||||
|
std::sort(out.begin(), out.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
50
core/fs/fsmitm_romfsbuild.h
Normal file
50
core/fs/fsmitm_romfsbuild.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
struct RomFSBuildDirectoryContext;
|
||||||
|
struct RomFSBuildFileContext;
|
||||||
|
struct RomFSDirectoryEntry;
|
||||||
|
struct RomFSFileEntry;
|
||||||
|
|
||||||
|
class RomFSBuildContext {
|
||||||
|
public:
|
||||||
|
explicit RomFSBuildContext(VirtualDir base, VirtualDir ext = nullptr);
|
||||||
|
~RomFSBuildContext();
|
||||||
|
|
||||||
|
// This finalizes the context.
|
||||||
|
std::vector<std::pair<u64, VirtualFile>> Build();
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualDir base;
|
||||||
|
VirtualDir ext;
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> root;
|
||||||
|
std::vector<std::shared_ptr<RomFSBuildDirectoryContext>> directories;
|
||||||
|
std::vector<std::shared_ptr<RomFSBuildFileContext>> files;
|
||||||
|
u64 num_dirs = 0;
|
||||||
|
u64 num_files = 0;
|
||||||
|
u64 dir_table_size = 0;
|
||||||
|
u64 file_table_size = 0;
|
||||||
|
u64 dir_hash_table_size = 0;
|
||||||
|
u64 file_hash_table_size = 0;
|
||||||
|
u64 file_partition_size = 0;
|
||||||
|
|
||||||
|
void VisitDirectory(VirtualDir filesys, VirtualDir ext_dir,
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> parent);
|
||||||
|
|
||||||
|
bool AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
|
||||||
|
std::shared_ptr<RomFSBuildDirectoryContext> dir_ctx);
|
||||||
|
bool AddFile(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
|
||||||
|
std::shared_ptr<RomFSBuildFileContext> file_ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
36
core/fs/fssrv/fssrv_sf_path.h
Normal file
36
core/fs/fssrv/fssrv_sf_path.h
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fs_directory.h"
|
||||||
|
|
||||||
|
namespace FileSys::Sf {
|
||||||
|
|
||||||
|
struct Path {
|
||||||
|
char str[EntryNameLengthMax + 1];
|
||||||
|
|
||||||
|
static constexpr Path Encode(const char* p) {
|
||||||
|
Path path = {};
|
||||||
|
for (size_t i = 0; i < sizeof(path) - 1; i++) {
|
||||||
|
path.str[i] = p[i];
|
||||||
|
if (p[i] == '\x00') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr size_t GetPathLength(const Path& path) {
|
||||||
|
size_t len = 0;
|
||||||
|
for (size_t i = 0; i < sizeof(path) - 1 && path.str[i] != '\x00'; i++) {
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivially_copyable_v<Path>, "Path must be trivially copyable.");
|
||||||
|
|
||||||
|
using FspPath = Path;
|
||||||
|
|
||||||
|
} // namespace FileSys::Sf
|
||||||
58
core/fs/fssystem/fs_i_storage.h
Normal file
58
core/fs/fssystem/fs_i_storage.h
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/overflow.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class IStorage : public VfsFile {
|
||||||
|
public:
|
||||||
|
virtual std::string GetName() const override {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual VirtualDir GetContainingDirectory() const override {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool IsWritable() const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool IsReadable() const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Resize(size_t size) override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Rename(std::string_view name) override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline Result CheckAccessRange(s64 offset, s64 size, s64 total_size) {
|
||||||
|
R_UNLESS(offset >= 0, ResultInvalidOffset);
|
||||||
|
R_UNLESS(size >= 0, ResultInvalidSize);
|
||||||
|
R_UNLESS(Common::WrappingAdd(offset, size) >= offset, ResultOutOfRange);
|
||||||
|
R_UNLESS(offset + size <= total_size, ResultOutOfRange);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IReadOnlyStorage : public IStorage {
|
||||||
|
public:
|
||||||
|
virtual bool IsWritable() const override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
46
core/fs/fssystem/fs_types.h
Normal file
46
core/fs/fssystem/fs_types.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
struct Int64 {
|
||||||
|
u32 low;
|
||||||
|
u32 high;
|
||||||
|
|
||||||
|
constexpr void Set(s64 v) {
|
||||||
|
this->low = static_cast<u32>((v & static_cast<u64>(0x00000000FFFFFFFFULL)) >> 0);
|
||||||
|
this->high = static_cast<u32>((v & static_cast<u64>(0xFFFFFFFF00000000ULL)) >> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr s64 Get() const {
|
||||||
|
return (static_cast<s64>(this->high) << 32) | (static_cast<s64>(this->low));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Int64& operator=(s64 v) {
|
||||||
|
this->Set(v);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr operator s64() const {
|
||||||
|
return this->Get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HashSalt {
|
||||||
|
static constexpr size_t Size = 32;
|
||||||
|
|
||||||
|
std::array<u8, Size> value;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<HashSalt>);
|
||||||
|
static_assert(sizeof(HashSalt) == HashSalt::Size);
|
||||||
|
|
||||||
|
constexpr inline size_t IntegrityMinLayerCount = 2;
|
||||||
|
constexpr inline size_t IntegrityMaxLayerCount = 7;
|
||||||
|
constexpr inline size_t IntegrityLayerCountSave = 5;
|
||||||
|
constexpr inline size_t IntegrityLayerCountSaveDataMeta = 4;
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
251
core/fs/fssystem/fssystem_aes_ctr_counter_extended_storage.cpp
Normal file
251
core/fs/fssystem/fssystem_aes_ctr_counter_extended_storage.cpp
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_aes_ctr_counter_extended_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_aes_ctr_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class SoftwareDecryptor final : public AesCtrCounterExtendedStorage::IDecryptor {
|
||||||
|
public:
|
||||||
|
virtual void Decrypt(
|
||||||
|
u8* buf, size_t buf_size, const std::array<u8, AesCtrCounterExtendedStorage::KeySize>& key,
|
||||||
|
const std::array<u8, AesCtrCounterExtendedStorage::IvSize>& iv) override final;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result AesCtrCounterExtendedStorage::CreateSoftwareDecryptor(std::unique_ptr<IDecryptor>* out) {
|
||||||
|
std::unique_ptr<IDecryptor> decryptor = std::make_unique<SoftwareDecryptor>();
|
||||||
|
R_UNLESS(decryptor != nullptr, ResultAllocationMemoryFailedInAesCtrCounterExtendedStorageA);
|
||||||
|
*out = std::move(decryptor);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result AesCtrCounterExtendedStorage::Initialize(const void* key, size_t key_size, u32 secure_value,
|
||||||
|
VirtualFile data_storage,
|
||||||
|
VirtualFile table_storage) {
|
||||||
|
// Read and verify the bucket tree header.
|
||||||
|
BucketTree::Header header;
|
||||||
|
table_storage->ReadObject(std::addressof(header), 0);
|
||||||
|
R_TRY(header.Verify());
|
||||||
|
|
||||||
|
// Determine extents.
|
||||||
|
const auto node_storage_size = QueryNodeStorageSize(header.entry_count);
|
||||||
|
const auto entry_storage_size = QueryEntryStorageSize(header.entry_count);
|
||||||
|
const auto node_storage_offset = QueryHeaderStorageSize();
|
||||||
|
const auto entry_storage_offset = node_storage_offset + node_storage_size;
|
||||||
|
|
||||||
|
// Create a software decryptor.
|
||||||
|
std::unique_ptr<IDecryptor> sw_decryptor;
|
||||||
|
R_TRY(CreateSoftwareDecryptor(std::addressof(sw_decryptor)));
|
||||||
|
|
||||||
|
// Initialize.
|
||||||
|
R_RETURN(this->Initialize(
|
||||||
|
key, key_size, secure_value, 0, data_storage,
|
||||||
|
std::make_shared<OffsetVfsFile>(table_storage, node_storage_size, node_storage_offset),
|
||||||
|
std::make_shared<OffsetVfsFile>(table_storage, entry_storage_size, entry_storage_offset),
|
||||||
|
header.entry_count, std::move(sw_decryptor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result AesCtrCounterExtendedStorage::Initialize(const void* key, size_t key_size, u32 secure_value,
|
||||||
|
s64 counter_offset, VirtualFile data_storage,
|
||||||
|
VirtualFile node_storage, VirtualFile entry_storage,
|
||||||
|
s32 entry_count,
|
||||||
|
std::unique_ptr<IDecryptor>&& decryptor) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(key != nullptr);
|
||||||
|
ASSERT(key_size == KeySize);
|
||||||
|
ASSERT(counter_offset >= 0);
|
||||||
|
ASSERT(decryptor != nullptr);
|
||||||
|
|
||||||
|
// Initialize the bucket tree table.
|
||||||
|
if (entry_count > 0) {
|
||||||
|
R_TRY(
|
||||||
|
m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry), entry_count));
|
||||||
|
} else {
|
||||||
|
m_table.Initialize(NodeSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set members.
|
||||||
|
m_data_storage = data_storage;
|
||||||
|
std::memcpy(m_key.data(), key, key_size);
|
||||||
|
m_secure_value = secure_value;
|
||||||
|
m_counter_offset = counter_offset;
|
||||||
|
m_decryptor = std::move(decryptor);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AesCtrCounterExtendedStorage::Finalize() {
|
||||||
|
if (this->IsInitialized()) {
|
||||||
|
m_table.Finalize();
|
||||||
|
m_data_storage = VirtualFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result AesCtrCounterExtendedStorage::GetEntryList(Entry* out_entries, s32* out_entry_count,
|
||||||
|
s32 entry_count, s64 offset, s64 size) {
|
||||||
|
// Validate pre-conditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(size >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Clear the out count.
|
||||||
|
R_UNLESS(out_entry_count != nullptr, ResultNullptrArgument);
|
||||||
|
*out_entry_count = 0;
|
||||||
|
|
||||||
|
// Succeed if there's no range.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// If we have an output array, we need it to be non-null.
|
||||||
|
R_UNLESS(out_entries != nullptr || entry_count == 0, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Check that our range is valid.
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||||
|
|
||||||
|
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||||
|
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||||
|
ResultInvalidAesCtrCounterExtendedEntryOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to loop over entries.
|
||||||
|
const auto end_offset = offset + static_cast<s64>(size);
|
||||||
|
s32 count = 0;
|
||||||
|
|
||||||
|
auto cur_entry = *visitor.Get<Entry>();
|
||||||
|
while (cur_entry.GetOffset() < end_offset) {
|
||||||
|
// Try to write the entry to the out list.
|
||||||
|
if (entry_count != 0) {
|
||||||
|
if (count >= entry_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::memcpy(out_entries + count, std::addressof(cur_entry), sizeof(Entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
if (visitor.CanMoveNext()) {
|
||||||
|
R_TRY(visitor.MoveNext());
|
||||||
|
cur_entry = *visitor.Get<Entry>();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the output count.
|
||||||
|
*out_entry_count = count;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesCtrCounterExtendedStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Allow zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||||
|
ASSERT(Common::IsAligned(size, BlockSize));
|
||||||
|
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
ASSERT(R_SUCCEEDED(m_table.GetOffsets(std::addressof(table_offsets))));
|
||||||
|
|
||||||
|
ASSERT(table_offsets.IsInclude(offset, size));
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
m_data_storage->Read(buffer, size, offset);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
ASSERT(R_SUCCEEDED(m_table.Find(std::addressof(visitor), offset)));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||||
|
ASSERT(Common::IsAligned(entry_offset, BlockSize));
|
||||||
|
ASSERT(0 <= entry_offset && table_offsets.IsInclude(entry_offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to read in chunks.
|
||||||
|
u8* cur_data = static_cast<u8*>(buffer);
|
||||||
|
auto cur_offset = offset;
|
||||||
|
const auto end_offset = offset + static_cast<s64>(size);
|
||||||
|
|
||||||
|
while (cur_offset < end_offset) {
|
||||||
|
// Get the current entry.
|
||||||
|
const auto cur_entry = *visitor.Get<Entry>();
|
||||||
|
|
||||||
|
// Get and validate the entry's offset.
|
||||||
|
const auto cur_entry_offset = cur_entry.GetOffset();
|
||||||
|
ASSERT(static_cast<size_t>(cur_entry_offset) <= cur_offset);
|
||||||
|
|
||||||
|
// Get and validate the next entry offset.
|
||||||
|
s64 next_entry_offset;
|
||||||
|
if (visitor.CanMoveNext()) {
|
||||||
|
ASSERT(R_SUCCEEDED(visitor.MoveNext()));
|
||||||
|
next_entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||||
|
ASSERT(table_offsets.IsInclude(next_entry_offset));
|
||||||
|
} else {
|
||||||
|
next_entry_offset = table_offsets.end_offset;
|
||||||
|
}
|
||||||
|
ASSERT(Common::IsAligned(next_entry_offset, BlockSize));
|
||||||
|
ASSERT(cur_offset < static_cast<size_t>(next_entry_offset));
|
||||||
|
|
||||||
|
// Get the offset of the entry in the data we read.
|
||||||
|
const auto data_offset = cur_offset - cur_entry_offset;
|
||||||
|
const auto data_size = (next_entry_offset - cur_entry_offset) - data_offset;
|
||||||
|
ASSERT(data_size > 0);
|
||||||
|
|
||||||
|
// Determine how much is left.
|
||||||
|
const auto remaining_size = end_offset - cur_offset;
|
||||||
|
const auto cur_size = static_cast<size_t>(std::min(remaining_size, data_size));
|
||||||
|
ASSERT(cur_size <= size);
|
||||||
|
|
||||||
|
// If necessary, perform decryption.
|
||||||
|
if (cur_entry.encryption_value == Entry::Encryption::Encrypted) {
|
||||||
|
// Make the CTR for the data we're decrypting.
|
||||||
|
const auto counter_offset = m_counter_offset + cur_entry_offset + data_offset;
|
||||||
|
NcaAesCtrUpperIv upper_iv = {
|
||||||
|
.part = {.generation = static_cast<u32>(cur_entry.generation),
|
||||||
|
.secure_value = m_secure_value}};
|
||||||
|
|
||||||
|
std::array<u8, IvSize> iv;
|
||||||
|
AesCtrStorage::MakeIv(iv.data(), IvSize, upper_iv.value, counter_offset);
|
||||||
|
|
||||||
|
// Decrypt.
|
||||||
|
m_decryptor->Decrypt(cur_data, cur_size, m_key, iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_data += cur_size;
|
||||||
|
cur_offset += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoftwareDecryptor::Decrypt(u8* buf, size_t buf_size,
|
||||||
|
const std::array<u8, AesCtrCounterExtendedStorage::KeySize>& key,
|
||||||
|
const std::array<u8, AesCtrCounterExtendedStorage::IvSize>& iv) {
|
||||||
|
Core::Crypto::AESCipher<Core::Crypto::Key128, AesCtrCounterExtendedStorage::KeySize> cipher(
|
||||||
|
key, Core::Crypto::Mode::CTR);
|
||||||
|
cipher.SetIV(iv);
|
||||||
|
cipher.Transcode(buf, buf_size, buf, Core::Crypto::Op::Decrypt);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
114
core/fs/fssystem/fssystem_aes_ctr_counter_extended_storage.h
Normal file
114
core/fs/fssystem/fssystem_aes_ctr_counter_extended_storage.h
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "common/literals.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
|
||||||
|
class AesCtrCounterExtendedStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(AesCtrCounterExtendedStorage);
|
||||||
|
YUZU_NON_MOVEABLE(AesCtrCounterExtendedStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t BlockSize = 0x10;
|
||||||
|
static constexpr size_t KeySize = 0x10;
|
||||||
|
static constexpr size_t IvSize = 0x10;
|
||||||
|
static constexpr size_t NodeSize = 16_KiB;
|
||||||
|
|
||||||
|
class IDecryptor {
|
||||||
|
public:
|
||||||
|
virtual ~IDecryptor() {}
|
||||||
|
virtual void Decrypt(u8* buf, size_t buf_size, const std::array<u8, KeySize>& key,
|
||||||
|
const std::array<u8, IvSize>& iv) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
enum class Encryption : u8 {
|
||||||
|
Encrypted = 0,
|
||||||
|
NotEncrypted = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<u8, sizeof(s64)> offset;
|
||||||
|
Encryption encryption_value;
|
||||||
|
std::array<u8, 3> reserved;
|
||||||
|
s32 generation;
|
||||||
|
|
||||||
|
void SetOffset(s64 value) {
|
||||||
|
std::memcpy(this->offset.data(), std::addressof(value), sizeof(s64));
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetOffset() const {
|
||||||
|
s64 value;
|
||||||
|
std::memcpy(std::addressof(value), this->offset.data(), sizeof(s64));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Entry) == 0x10);
|
||||||
|
static_assert(alignof(Entry) == 4);
|
||||||
|
static_assert(std::is_trivial_v<Entry>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s64 QueryHeaderStorageSize() {
|
||||||
|
return BucketTree::QueryHeaderStorageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result CreateSoftwareDecryptor(std::unique_ptr<IDecryptor>* out);
|
||||||
|
|
||||||
|
public:
|
||||||
|
AesCtrCounterExtendedStorage()
|
||||||
|
: m_table(), m_data_storage(), m_secure_value(), m_counter_offset(), m_decryptor() {}
|
||||||
|
virtual ~AesCtrCounterExtendedStorage() {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const void* key, size_t key_size, u32 secure_value, s64 counter_offset,
|
||||||
|
VirtualFile data_storage, VirtualFile node_storage, VirtualFile entry_storage,
|
||||||
|
s32 entry_count, std::unique_ptr<IDecryptor>&& decryptor);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_table.IsInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
BucketTree::Offsets offsets;
|
||||||
|
ASSERT(R_SUCCEEDED(m_table.GetOffsets(std::addressof(offsets))));
|
||||||
|
|
||||||
|
return offsets.end_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count, s64 offset,
|
||||||
|
s64 size);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result Initialize(const void* key, size_t key_size, u32 secure_value, VirtualFile data_storage,
|
||||||
|
VirtualFile table_storage);
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable BucketTree m_table;
|
||||||
|
VirtualFile m_data_storage;
|
||||||
|
std::array<u8, KeySize> m_key;
|
||||||
|
u32 m_secure_value;
|
||||||
|
s64 m_counter_offset;
|
||||||
|
std::unique_ptr<IDecryptor> m_decryptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
129
core/fs/fssystem/fssystem_aes_ctr_storage.cpp
Normal file
129
core/fs/fssystem/fssystem_aes_ctr_storage.cpp
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_aes_ctr_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_utility.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
void AesCtrStorage::MakeIv(void* dst, size_t dst_size, u64 upper, s64 offset) {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(dst_size == IvSize);
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
|
||||||
|
const uintptr_t out_addr = reinterpret_cast<uintptr_t>(dst);
|
||||||
|
|
||||||
|
*reinterpret_cast<u64_be*>(out_addr + 0) = upper;
|
||||||
|
*reinterpret_cast<s64_be*>(out_addr + sizeof(u64)) = static_cast<s64>(offset / BlockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
AesCtrStorage::AesCtrStorage(VirtualFile base, const void* key, size_t key_size, const void* iv,
|
||||||
|
size_t iv_size)
|
||||||
|
: m_base_storage(std::move(base)) {
|
||||||
|
ASSERT(m_base_storage != nullptr);
|
||||||
|
ASSERT(key != nullptr);
|
||||||
|
ASSERT(iv != nullptr);
|
||||||
|
ASSERT(key_size == KeySize);
|
||||||
|
ASSERT(iv_size == IvSize);
|
||||||
|
|
||||||
|
std::memcpy(m_key.data(), key, KeySize);
|
||||||
|
std::memcpy(m_iv.data(), iv, IvSize);
|
||||||
|
|
||||||
|
m_cipher.emplace(m_key, Core::Crypto::Mode::CTR);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesCtrStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Allow zero-size reads.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure buffer is valid.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// We can only read at block aligned offsets.
|
||||||
|
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||||
|
ASSERT(Common::IsAligned(size, BlockSize));
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
m_base_storage->Read(buffer, size, offset);
|
||||||
|
|
||||||
|
// Setup the counter.
|
||||||
|
std::array<u8, IvSize> ctr;
|
||||||
|
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||||
|
AddCounter(ctr.data(), IvSize, offset / BlockSize);
|
||||||
|
|
||||||
|
// Decrypt.
|
||||||
|
m_cipher->SetIV(ctr);
|
||||||
|
m_cipher->Transcode(buffer, size, buffer, Core::Crypto::Op::Decrypt);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesCtrStorage::Write(const u8* buffer, size_t size, size_t offset) {
|
||||||
|
// Allow zero-size writes.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure buffer is valid.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// We can only write at block aligned offsets.
|
||||||
|
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||||
|
ASSERT(Common::IsAligned(size, BlockSize));
|
||||||
|
|
||||||
|
// Get a pooled buffer.
|
||||||
|
PooledBuffer pooled_buffer;
|
||||||
|
const bool use_work_buffer = true;
|
||||||
|
if (use_work_buffer) {
|
||||||
|
pooled_buffer.Allocate(size, BlockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the counter.
|
||||||
|
std::array<u8, IvSize> ctr;
|
||||||
|
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||||
|
AddCounter(ctr.data(), IvSize, offset / BlockSize);
|
||||||
|
|
||||||
|
// Loop until all data is written.
|
||||||
|
size_t remaining = size;
|
||||||
|
s64 cur_offset = 0;
|
||||||
|
while (remaining > 0) {
|
||||||
|
// Determine data we're writing and where.
|
||||||
|
const size_t write_size =
|
||||||
|
use_work_buffer ? std::min(pooled_buffer.GetSize(), remaining) : remaining;
|
||||||
|
|
||||||
|
void* write_buf;
|
||||||
|
if (use_work_buffer) {
|
||||||
|
write_buf = pooled_buffer.GetBuffer();
|
||||||
|
} else {
|
||||||
|
write_buf = const_cast<u8*>(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the data.
|
||||||
|
m_cipher->SetIV(ctr);
|
||||||
|
m_cipher->Transcode(buffer, write_size, reinterpret_cast<u8*>(write_buf),
|
||||||
|
Core::Crypto::Op::Encrypt);
|
||||||
|
|
||||||
|
// Write the encrypted data.
|
||||||
|
m_base_storage->Write(reinterpret_cast<u8*>(write_buf), write_size, offset + cur_offset);
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_offset += write_size;
|
||||||
|
remaining -= write_size;
|
||||||
|
if (remaining > 0) {
|
||||||
|
AddCounter(ctr.data(), IvSize, write_size / BlockSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesCtrStorage::GetSize() const {
|
||||||
|
return m_base_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
43
core/fs/fssystem/fssystem_aes_ctr_storage.h
Normal file
43
core/fs/fssystem/fssystem_aes_ctr_storage.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/crypto/aes_util.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class AesCtrStorage : public IStorage {
|
||||||
|
YUZU_NON_COPYABLE(AesCtrStorage);
|
||||||
|
YUZU_NON_MOVEABLE(AesCtrStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t BlockSize = 0x10;
|
||||||
|
static constexpr size_t KeySize = 0x10;
|
||||||
|
static constexpr size_t IvSize = 0x10;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void MakeIv(void* dst, size_t dst_size, u64 upper, s64 offset);
|
||||||
|
|
||||||
|
public:
|
||||||
|
AesCtrStorage(VirtualFile base, const void* key, size_t key_size, const void* iv,
|
||||||
|
size_t iv_size);
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override;
|
||||||
|
virtual size_t GetSize() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_base_storage;
|
||||||
|
std::array<u8, KeySize> m_key;
|
||||||
|
std::array<u8, IvSize> m_iv;
|
||||||
|
mutable std::optional<Core::Crypto::AESCipher<Core::Crypto::Key128>> m_cipher;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
112
core/fs/fssystem/fssystem_aes_xts_storage.cpp
Normal file
112
core/fs/fssystem/fssystem_aes_xts_storage.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_aes_xts_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_utility.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
void AesXtsStorage::MakeAesXtsIv(void* dst, size_t dst_size, s64 offset, size_t block_size) {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(dst_size == IvSize);
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
|
||||||
|
const uintptr_t out_addr = reinterpret_cast<uintptr_t>(dst);
|
||||||
|
|
||||||
|
*reinterpret_cast<s64_be*>(out_addr + sizeof(s64)) = offset / block_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
AesXtsStorage::AesXtsStorage(VirtualFile base, const void* key1, const void* key2, size_t key_size,
|
||||||
|
const void* iv, size_t iv_size, size_t block_size)
|
||||||
|
: m_base_storage(std::move(base)), m_block_size(block_size), m_mutex() {
|
||||||
|
ASSERT(m_base_storage != nullptr);
|
||||||
|
ASSERT(key1 != nullptr);
|
||||||
|
ASSERT(key2 != nullptr);
|
||||||
|
ASSERT(iv != nullptr);
|
||||||
|
ASSERT(key_size == KeySize);
|
||||||
|
ASSERT(iv_size == IvSize);
|
||||||
|
ASSERT(Common::IsAligned(m_block_size, AesBlockSize));
|
||||||
|
|
||||||
|
std::memcpy(m_key.data() + 0, key1, KeySize / 2);
|
||||||
|
std::memcpy(m_key.data() + 0x10, key2, KeySize / 2);
|
||||||
|
std::memcpy(m_iv.data(), iv, IvSize);
|
||||||
|
|
||||||
|
m_cipher.emplace(m_key, Core::Crypto::Mode::XTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesXtsStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Allow zero-size reads.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure buffer is valid.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// We can only read at block aligned offsets.
|
||||||
|
ASSERT(Common::IsAligned(offset, AesBlockSize));
|
||||||
|
ASSERT(Common::IsAligned(size, AesBlockSize));
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
m_base_storage->Read(buffer, size, offset);
|
||||||
|
|
||||||
|
// Setup the counter.
|
||||||
|
std::array<u8, IvSize> ctr;
|
||||||
|
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||||
|
AddCounter(ctr.data(), IvSize, offset / m_block_size);
|
||||||
|
|
||||||
|
// Handle any unaligned data before the start.
|
||||||
|
size_t processed_size = 0;
|
||||||
|
if ((offset % m_block_size) != 0) {
|
||||||
|
// Determine the size of the pre-data read.
|
||||||
|
const size_t skip_size =
|
||||||
|
static_cast<size_t>(offset - Common::AlignDown(offset, m_block_size));
|
||||||
|
const size_t data_size = std::min(size, m_block_size - skip_size);
|
||||||
|
|
||||||
|
// Decrypt into a pooled buffer.
|
||||||
|
{
|
||||||
|
PooledBuffer tmp_buf(m_block_size, m_block_size);
|
||||||
|
ASSERT(tmp_buf.GetSize() >= m_block_size);
|
||||||
|
|
||||||
|
std::memset(tmp_buf.GetBuffer(), 0, skip_size);
|
||||||
|
std::memcpy(tmp_buf.GetBuffer() + skip_size, buffer, data_size);
|
||||||
|
|
||||||
|
m_cipher->SetIV(ctr);
|
||||||
|
m_cipher->Transcode(tmp_buf.GetBuffer(), m_block_size, tmp_buf.GetBuffer(),
|
||||||
|
Core::Crypto::Op::Decrypt);
|
||||||
|
|
||||||
|
std::memcpy(buffer, tmp_buf.GetBuffer() + skip_size, data_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCounter(ctr.data(), IvSize, 1);
|
||||||
|
processed_size += data_size;
|
||||||
|
ASSERT(processed_size == std::min(size, m_block_size - skip_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt aligned chunks.
|
||||||
|
char* cur = reinterpret_cast<char*>(buffer) + processed_size;
|
||||||
|
size_t remaining = size - processed_size;
|
||||||
|
while (remaining > 0) {
|
||||||
|
const size_t cur_size = std::min(m_block_size, remaining);
|
||||||
|
|
||||||
|
m_cipher->SetIV(ctr);
|
||||||
|
m_cipher->Transcode(cur, cur_size, cur, Core::Crypto::Op::Decrypt);
|
||||||
|
|
||||||
|
remaining -= cur_size;
|
||||||
|
cur += cur_size;
|
||||||
|
|
||||||
|
AddCounter(ctr.data(), IvSize, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AesXtsStorage::GetSize() const {
|
||||||
|
return m_base_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
43
core/fs/fssystem/fssystem_aes_xts_storage.h
Normal file
43
core/fs/fssystem/fssystem_aes_xts_storage.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/crypto/aes_util.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class AesXtsStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(AesXtsStorage);
|
||||||
|
YUZU_NON_MOVEABLE(AesXtsStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t AesBlockSize = 0x10;
|
||||||
|
static constexpr size_t KeySize = 0x20;
|
||||||
|
static constexpr size_t IvSize = 0x10;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void MakeAesXtsIv(void* dst, size_t dst_size, s64 offset, size_t block_size);
|
||||||
|
|
||||||
|
public:
|
||||||
|
AesXtsStorage(VirtualFile base, const void* key1, const void* key2, size_t key_size,
|
||||||
|
const void* iv, size_t iv_size, size_t block_size);
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
virtual size_t GetSize() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_base_storage;
|
||||||
|
std::array<u8, KeySize> m_key;
|
||||||
|
std::array<u8, IvSize> m_iv;
|
||||||
|
const size_t m_block_size;
|
||||||
|
std::mutex m_mutex;
|
||||||
|
mutable std::optional<Core::Crypto::AESCipher<Core::Crypto::Key256>> m_cipher;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
146
core/fs/fssystem/fssystem_alignment_matching_storage.h
Normal file
146
core/fs/fssystem/fssystem_alignment_matching_storage.h
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage_impl.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
template <size_t DataAlign_, size_t BufferAlign_>
|
||||||
|
class AlignmentMatchingStorage : public IStorage {
|
||||||
|
YUZU_NON_COPYABLE(AlignmentMatchingStorage);
|
||||||
|
YUZU_NON_MOVEABLE(AlignmentMatchingStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t DataAlign = DataAlign_;
|
||||||
|
static constexpr size_t BufferAlign = BufferAlign_;
|
||||||
|
|
||||||
|
static constexpr size_t DataAlignMax = 0x200;
|
||||||
|
static_assert(DataAlign <= DataAlignMax);
|
||||||
|
static_assert(Common::IsPowerOfTwo(DataAlign));
|
||||||
|
static_assert(Common::IsPowerOfTwo(BufferAlign));
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_base_storage;
|
||||||
|
s64 m_base_storage_size;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AlignmentMatchingStorage(VirtualFile bs) : m_base_storage(std::move(bs)) {}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
// Allocate a work buffer on stack.
|
||||||
|
alignas(DataAlignMax) std::array<char, DataAlign> work_buf;
|
||||||
|
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
s64 bs_size = this->GetSize();
|
||||||
|
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||||
|
|
||||||
|
return AlignmentMatchingStorageImpl::Read(m_base_storage, work_buf.data(), work_buf.size(),
|
||||||
|
DataAlign, BufferAlign, offset, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||||
|
// Allocate a work buffer on stack.
|
||||||
|
alignas(DataAlignMax) std::array<char, DataAlign> work_buf;
|
||||||
|
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
s64 bs_size = this->GetSize();
|
||||||
|
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||||
|
|
||||||
|
return AlignmentMatchingStorageImpl::Write(m_base_storage, work_buf.data(), work_buf.size(),
|
||||||
|
DataAlign, BufferAlign, offset, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return m_base_storage->GetSize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <size_t BufferAlign_>
|
||||||
|
class AlignmentMatchingStoragePooledBuffer : public IStorage {
|
||||||
|
YUZU_NON_COPYABLE(AlignmentMatchingStoragePooledBuffer);
|
||||||
|
YUZU_NON_MOVEABLE(AlignmentMatchingStoragePooledBuffer);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t BufferAlign = BufferAlign_;
|
||||||
|
|
||||||
|
static_assert(Common::IsPowerOfTwo(BufferAlign));
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_base_storage;
|
||||||
|
s64 m_base_storage_size;
|
||||||
|
size_t m_data_align;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AlignmentMatchingStoragePooledBuffer(VirtualFile bs, size_t da)
|
||||||
|
: m_base_storage(std::move(bs)), m_data_align(da) {
|
||||||
|
ASSERT(Common::IsPowerOfTwo(da));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
s64 bs_size = this->GetSize();
|
||||||
|
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||||
|
|
||||||
|
// Allocate a pooled buffer.
|
||||||
|
PooledBuffer pooled_buffer;
|
||||||
|
pooled_buffer.AllocateParticularlyLarge(m_data_align, m_data_align);
|
||||||
|
|
||||||
|
return AlignmentMatchingStorageImpl::Read(m_base_storage, pooled_buffer.GetBuffer(),
|
||||||
|
pooled_buffer.GetSize(), m_data_align,
|
||||||
|
BufferAlign, offset, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
s64 bs_size = this->GetSize();
|
||||||
|
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||||
|
|
||||||
|
// Allocate a pooled buffer.
|
||||||
|
PooledBuffer pooled_buffer;
|
||||||
|
pooled_buffer.AllocateParticularlyLarge(m_data_align, m_data_align);
|
||||||
|
|
||||||
|
return AlignmentMatchingStorageImpl::Write(m_base_storage, pooled_buffer.GetBuffer(),
|
||||||
|
pooled_buffer.GetSize(), m_data_align,
|
||||||
|
BufferAlign, offset, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return m_base_storage->GetSize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
204
core/fs/fssystem/fssystem_alignment_matching_storage_impl.cpp
Normal file
204
core/fs/fssystem/fssystem_alignment_matching_storage_impl.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage_impl.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr size_t GetRoundDownDifference(T x, size_t align) {
|
||||||
|
return static_cast<size_t>(x - Common::AlignDown(x, align));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr size_t GetRoundUpDifference(T x, size_t align) {
|
||||||
|
return static_cast<size_t>(Common::AlignUp(x, align) - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
size_t GetRoundUpDifference(T* x, size_t align) {
|
||||||
|
return GetRoundUpDifference(reinterpret_cast<uintptr_t>(x), align);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
size_t AlignmentMatchingStorageImpl::Read(VirtualFile base_storage, char* work_buf,
|
||||||
|
size_t work_buf_size, size_t data_alignment,
|
||||||
|
size_t buffer_alignment, s64 offset, u8* buffer,
|
||||||
|
size_t size) {
|
||||||
|
// Check preconditions.
|
||||||
|
ASSERT(work_buf_size >= data_alignment);
|
||||||
|
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Determine extents.
|
||||||
|
u8* aligned_core_buffer;
|
||||||
|
s64 core_offset;
|
||||||
|
size_t core_size;
|
||||||
|
size_t buffer_gap;
|
||||||
|
size_t offset_gap;
|
||||||
|
s64 covered_offset;
|
||||||
|
|
||||||
|
const size_t offset_round_up_difference = GetRoundUpDifference(offset, data_alignment);
|
||||||
|
if (Common::IsAligned(reinterpret_cast<uintptr_t>(buffer) + offset_round_up_difference,
|
||||||
|
buffer_alignment)) {
|
||||||
|
aligned_core_buffer = buffer + offset_round_up_difference;
|
||||||
|
|
||||||
|
core_offset = Common::AlignUp(offset, data_alignment);
|
||||||
|
core_size = (size < offset_round_up_difference)
|
||||||
|
? 0
|
||||||
|
: Common::AlignDown(size - offset_round_up_difference, data_alignment);
|
||||||
|
buffer_gap = 0;
|
||||||
|
offset_gap = 0;
|
||||||
|
|
||||||
|
covered_offset = core_size > 0 ? core_offset : offset;
|
||||||
|
} else {
|
||||||
|
const size_t buffer_round_up_difference = GetRoundUpDifference(buffer, buffer_alignment);
|
||||||
|
|
||||||
|
aligned_core_buffer = buffer + buffer_round_up_difference;
|
||||||
|
|
||||||
|
core_offset = Common::AlignDown(offset, data_alignment);
|
||||||
|
core_size = (size < buffer_round_up_difference)
|
||||||
|
? 0
|
||||||
|
: Common::AlignDown(size - buffer_round_up_difference, data_alignment);
|
||||||
|
buffer_gap = buffer_round_up_difference;
|
||||||
|
offset_gap = GetRoundDownDifference(offset, data_alignment);
|
||||||
|
|
||||||
|
covered_offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the core portion.
|
||||||
|
if (core_size > 0) {
|
||||||
|
base_storage->Read(aligned_core_buffer, core_size, core_offset);
|
||||||
|
|
||||||
|
if (offset_gap != 0 || buffer_gap != 0) {
|
||||||
|
std::memmove(aligned_core_buffer - buffer_gap, aligned_core_buffer + offset_gap,
|
||||||
|
core_size - offset_gap);
|
||||||
|
core_size -= offset_gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the head portion.
|
||||||
|
if (offset < covered_offset) {
|
||||||
|
const s64 head_offset = Common::AlignDown(offset, data_alignment);
|
||||||
|
const size_t head_size = static_cast<size_t>(covered_offset - offset);
|
||||||
|
|
||||||
|
ASSERT(GetRoundDownDifference(offset, data_alignment) + head_size <= work_buf_size);
|
||||||
|
|
||||||
|
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||||
|
std::memcpy(buffer, work_buf + GetRoundDownDifference(offset, data_alignment), head_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the tail portion.
|
||||||
|
s64 tail_offset = covered_offset + core_size;
|
||||||
|
size_t remaining_tail_size = static_cast<size_t>((offset + size) - tail_offset);
|
||||||
|
while (remaining_tail_size > 0) {
|
||||||
|
const auto aligned_tail_offset = Common::AlignDown(tail_offset, data_alignment);
|
||||||
|
const auto cur_size =
|
||||||
|
std::min(static_cast<size_t>(aligned_tail_offset + data_alignment - tail_offset),
|
||||||
|
remaining_tail_size);
|
||||||
|
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||||
|
|
||||||
|
ASSERT((tail_offset - offset) + cur_size <= size);
|
||||||
|
ASSERT((tail_offset - aligned_tail_offset) + cur_size <= data_alignment);
|
||||||
|
std::memcpy(reinterpret_cast<char*>(buffer) + (tail_offset - offset),
|
||||||
|
work_buf + (tail_offset - aligned_tail_offset), cur_size);
|
||||||
|
|
||||||
|
remaining_tail_size -= cur_size;
|
||||||
|
tail_offset += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t AlignmentMatchingStorageImpl::Write(VirtualFile base_storage, char* work_buf,
|
||||||
|
size_t work_buf_size, size_t data_alignment,
|
||||||
|
size_t buffer_alignment, s64 offset, const u8* buffer,
|
||||||
|
size_t size) {
|
||||||
|
// Check preconditions.
|
||||||
|
ASSERT(work_buf_size >= data_alignment);
|
||||||
|
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Determine extents.
|
||||||
|
const u8* aligned_core_buffer;
|
||||||
|
s64 core_offset;
|
||||||
|
size_t core_size;
|
||||||
|
s64 covered_offset;
|
||||||
|
|
||||||
|
const size_t offset_round_up_difference = GetRoundUpDifference(offset, data_alignment);
|
||||||
|
if (Common::IsAligned(reinterpret_cast<uintptr_t>(buffer) + offset_round_up_difference,
|
||||||
|
buffer_alignment)) {
|
||||||
|
aligned_core_buffer = buffer + offset_round_up_difference;
|
||||||
|
|
||||||
|
core_offset = Common::AlignUp(offset, data_alignment);
|
||||||
|
core_size = (size < offset_round_up_difference)
|
||||||
|
? 0
|
||||||
|
: Common::AlignDown(size - offset_round_up_difference, data_alignment);
|
||||||
|
|
||||||
|
covered_offset = core_size > 0 ? core_offset : offset;
|
||||||
|
} else {
|
||||||
|
aligned_core_buffer = nullptr;
|
||||||
|
|
||||||
|
core_offset = Common::AlignDown(offset, data_alignment);
|
||||||
|
core_size = 0;
|
||||||
|
|
||||||
|
covered_offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the core portion.
|
||||||
|
if (core_size > 0) {
|
||||||
|
base_storage->Write(aligned_core_buffer, core_size, core_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the head portion.
|
||||||
|
if (offset < covered_offset) {
|
||||||
|
const s64 head_offset = Common::AlignDown(offset, data_alignment);
|
||||||
|
const size_t head_size = static_cast<size_t>(covered_offset - offset);
|
||||||
|
|
||||||
|
ASSERT((offset - head_offset) + head_size <= data_alignment);
|
||||||
|
|
||||||
|
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||||
|
std::memcpy(work_buf + (offset - head_offset), buffer, head_size);
|
||||||
|
base_storage->Write(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the tail portion.
|
||||||
|
s64 tail_offset = covered_offset + core_size;
|
||||||
|
size_t remaining_tail_size = static_cast<size_t>((offset + size) - tail_offset);
|
||||||
|
while (remaining_tail_size > 0) {
|
||||||
|
ASSERT(static_cast<size_t>(tail_offset - offset) < size);
|
||||||
|
|
||||||
|
const auto aligned_tail_offset = Common::AlignDown(tail_offset, data_alignment);
|
||||||
|
const auto cur_size =
|
||||||
|
std::min(static_cast<size_t>(aligned_tail_offset + data_alignment - tail_offset),
|
||||||
|
remaining_tail_size);
|
||||||
|
|
||||||
|
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||||
|
std::memcpy(work_buf + GetRoundDownDifference(tail_offset, data_alignment),
|
||||||
|
buffer + (tail_offset - offset), cur_size);
|
||||||
|
base_storage->Write(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||||
|
|
||||||
|
remaining_tail_size -= cur_size;
|
||||||
|
tail_offset += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
21
core/fs/fssystem/fssystem_alignment_matching_storage_impl.h
Normal file
21
core/fs/fssystem/fssystem_alignment_matching_storage_impl.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class AlignmentMatchingStorageImpl {
|
||||||
|
public:
|
||||||
|
static size_t Read(VirtualFile base_storage, char* work_buf, size_t work_buf_size,
|
||||||
|
size_t data_alignment, size_t buffer_alignment, s64 offset, u8* buffer,
|
||||||
|
size_t size);
|
||||||
|
static size_t Write(VirtualFile base_storage, char* work_buf, size_t work_buf_size,
|
||||||
|
size_t data_alignment, size_t buffer_alignment, s64 offset,
|
||||||
|
const u8* buffer, size_t size);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
598
core/fs/fssystem/fssystem_bucket_tree.cpp
Normal file
598
core/fs/fssystem/fssystem_bucket_tree.cpp
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree_utils.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Node = impl::BucketTreeNode<const s64*>;
|
||||||
|
static_assert(sizeof(Node) == sizeof(BucketTree::NodeHeader));
|
||||||
|
static_assert(std::is_trivial_v<Node>);
|
||||||
|
|
||||||
|
constexpr inline s32 NodeHeaderSize = sizeof(BucketTree::NodeHeader);
|
||||||
|
|
||||||
|
class StorageNode {
|
||||||
|
private:
|
||||||
|
class Offset {
|
||||||
|
public:
|
||||||
|
using difference_type = s64;
|
||||||
|
|
||||||
|
private:
|
||||||
|
s64 m_offset;
|
||||||
|
s32 m_stride;
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr Offset(s64 offset, s32 stride) : m_offset(offset), m_stride(stride) {}
|
||||||
|
|
||||||
|
constexpr Offset& operator++() {
|
||||||
|
m_offset += m_stride;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
constexpr Offset operator++(int) {
|
||||||
|
Offset ret(*this);
|
||||||
|
m_offset += m_stride;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Offset& operator--() {
|
||||||
|
m_offset -= m_stride;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
constexpr Offset operator--(int) {
|
||||||
|
Offset ret(*this);
|
||||||
|
m_offset -= m_stride;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr difference_type operator-(const Offset& rhs) const {
|
||||||
|
return (m_offset - rhs.m_offset) / m_stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Offset operator+(difference_type ofs) const {
|
||||||
|
return Offset(m_offset + ofs * m_stride, m_stride);
|
||||||
|
}
|
||||||
|
constexpr Offset operator-(difference_type ofs) const {
|
||||||
|
return Offset(m_offset - ofs * m_stride, m_stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Offset& operator+=(difference_type ofs) {
|
||||||
|
m_offset += ofs * m_stride;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
constexpr Offset& operator-=(difference_type ofs) {
|
||||||
|
m_offset -= ofs * m_stride;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool operator==(const Offset& rhs) const {
|
||||||
|
return m_offset == rhs.m_offset;
|
||||||
|
}
|
||||||
|
constexpr bool operator!=(const Offset& rhs) const {
|
||||||
|
return m_offset != rhs.m_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr s64 Get() const {
|
||||||
|
return m_offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Offset m_start;
|
||||||
|
const s32 m_count;
|
||||||
|
s32 m_index;
|
||||||
|
|
||||||
|
public:
|
||||||
|
StorageNode(size_t size, s32 count)
|
||||||
|
: m_start(NodeHeaderSize, static_cast<s32>(size)), m_count(count), m_index(-1) {}
|
||||||
|
StorageNode(s64 ofs, size_t size, s32 count)
|
||||||
|
: m_start(NodeHeaderSize + ofs, static_cast<s32>(size)), m_count(count), m_index(-1) {}
|
||||||
|
|
||||||
|
s32 GetIndex() const {
|
||||||
|
return m_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Find(const char* buffer, s64 virtual_address) {
|
||||||
|
s32 end = m_count;
|
||||||
|
auto pos = m_start;
|
||||||
|
|
||||||
|
while (end > 0) {
|
||||||
|
auto half = end / 2;
|
||||||
|
auto mid = pos + half;
|
||||||
|
|
||||||
|
s64 offset = 0;
|
||||||
|
std::memcpy(std::addressof(offset), buffer + mid.Get(), sizeof(s64));
|
||||||
|
|
||||||
|
if (offset <= virtual_address) {
|
||||||
|
pos = mid + 1;
|
||||||
|
end -= half + 1;
|
||||||
|
} else {
|
||||||
|
end = half;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_index = static_cast<s32>(pos - m_start) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Find(VirtualFile storage, s64 virtual_address) {
|
||||||
|
s32 end = m_count;
|
||||||
|
auto pos = m_start;
|
||||||
|
|
||||||
|
while (end > 0) {
|
||||||
|
auto half = end / 2;
|
||||||
|
auto mid = pos + half;
|
||||||
|
|
||||||
|
s64 offset = 0;
|
||||||
|
storage->ReadObject(std::addressof(offset), mid.Get());
|
||||||
|
|
||||||
|
if (offset <= virtual_address) {
|
||||||
|
pos = mid + 1;
|
||||||
|
end -= half + 1;
|
||||||
|
} else {
|
||||||
|
end = half;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_index = static_cast<s32>(pos - m_start) - 1;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void BucketTree::Header::Format(s32 entry_count_) {
|
||||||
|
ASSERT(entry_count_ >= 0);
|
||||||
|
|
||||||
|
this->magic = Magic;
|
||||||
|
this->version = Version;
|
||||||
|
this->entry_count = entry_count_;
|
||||||
|
this->reserved = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Header::Verify() const {
|
||||||
|
R_UNLESS(this->magic == Magic, ResultInvalidBucketTreeSignature);
|
||||||
|
R_UNLESS(this->entry_count >= 0, ResultInvalidBucketTreeEntryCount);
|
||||||
|
R_UNLESS(this->version <= Version, ResultUnsupportedVersion);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::NodeHeader::Verify(s32 node_index, size_t node_size, size_t entry_size) const {
|
||||||
|
R_UNLESS(this->index == node_index, ResultInvalidBucketTreeNodeIndex);
|
||||||
|
R_UNLESS(entry_size != 0 && node_size >= entry_size + NodeHeaderSize, ResultInvalidSize);
|
||||||
|
|
||||||
|
const size_t max_entry_count = (node_size - NodeHeaderSize) / entry_size;
|
||||||
|
R_UNLESS(this->count > 0 && static_cast<size_t>(this->count) <= max_entry_count,
|
||||||
|
ResultInvalidBucketTreeNodeEntryCount);
|
||||||
|
R_UNLESS(this->offset >= 0, ResultInvalidBucketTreeNodeOffset);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Initialize(VirtualFile node_storage, VirtualFile entry_storage, size_t node_size,
|
||||||
|
size_t entry_size, s32 entry_count) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(entry_size >= sizeof(s64));
|
||||||
|
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||||
|
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||||
|
ASSERT(!this->IsInitialized());
|
||||||
|
|
||||||
|
// Ensure valid entry count.
|
||||||
|
R_UNLESS(entry_count > 0, ResultInvalidArgument);
|
||||||
|
|
||||||
|
// Allocate node.
|
||||||
|
R_UNLESS(m_node_l1.Allocate(node_size), ResultBufferAllocationFailed);
|
||||||
|
ON_RESULT_FAILURE {
|
||||||
|
m_node_l1.Free(node_size);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read node.
|
||||||
|
node_storage->Read(reinterpret_cast<u8*>(m_node_l1.Get()), node_size);
|
||||||
|
|
||||||
|
// Verify node.
|
||||||
|
R_TRY(m_node_l1->Verify(0, node_size, sizeof(s64)));
|
||||||
|
|
||||||
|
// Validate offsets.
|
||||||
|
const auto offset_count = GetOffsetCount(node_size);
|
||||||
|
const auto entry_set_count = GetEntrySetCount(node_size, entry_size, entry_count);
|
||||||
|
const auto* const node = m_node_l1.Get<Node>();
|
||||||
|
|
||||||
|
s64 start_offset;
|
||||||
|
if (offset_count < entry_set_count && node->GetCount() < offset_count) {
|
||||||
|
start_offset = *node->GetEnd();
|
||||||
|
} else {
|
||||||
|
start_offset = *node->GetBegin();
|
||||||
|
}
|
||||||
|
const auto end_offset = node->GetEndOffset();
|
||||||
|
|
||||||
|
R_UNLESS(0 <= start_offset && start_offset <= node->GetBeginOffset(),
|
||||||
|
ResultInvalidBucketTreeEntryOffset);
|
||||||
|
R_UNLESS(start_offset < end_offset, ResultInvalidBucketTreeEntryOffset);
|
||||||
|
|
||||||
|
// Set member variables.
|
||||||
|
m_node_storage = node_storage;
|
||||||
|
m_entry_storage = entry_storage;
|
||||||
|
m_node_size = node_size;
|
||||||
|
m_entry_size = entry_size;
|
||||||
|
m_entry_count = entry_count;
|
||||||
|
m_offset_count = offset_count;
|
||||||
|
m_entry_set_count = entry_set_count;
|
||||||
|
|
||||||
|
m_offset_cache.offsets.start_offset = start_offset;
|
||||||
|
m_offset_cache.offsets.end_offset = end_offset;
|
||||||
|
m_offset_cache.is_initialized = true;
|
||||||
|
|
||||||
|
// We succeeded.
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BucketTree::Initialize(size_t node_size, s64 end_offset) {
|
||||||
|
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||||
|
ASSERT(end_offset > 0);
|
||||||
|
ASSERT(!this->IsInitialized());
|
||||||
|
|
||||||
|
m_node_size = node_size;
|
||||||
|
|
||||||
|
m_offset_cache.offsets.start_offset = 0;
|
||||||
|
m_offset_cache.offsets.end_offset = end_offset;
|
||||||
|
m_offset_cache.is_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BucketTree::Finalize() {
|
||||||
|
if (this->IsInitialized()) {
|
||||||
|
m_node_storage = VirtualFile();
|
||||||
|
m_entry_storage = VirtualFile();
|
||||||
|
m_node_l1.Free(m_node_size);
|
||||||
|
m_node_size = 0;
|
||||||
|
m_entry_size = 0;
|
||||||
|
m_entry_count = 0;
|
||||||
|
m_offset_count = 0;
|
||||||
|
m_entry_set_count = 0;
|
||||||
|
|
||||||
|
m_offset_cache.offsets.start_offset = 0;
|
||||||
|
m_offset_cache.offsets.end_offset = 0;
|
||||||
|
m_offset_cache.is_initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Find(Visitor* visitor, s64 virtual_address) {
|
||||||
|
ASSERT(visitor != nullptr);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
R_UNLESS(virtual_address >= 0, ResultInvalidOffset);
|
||||||
|
R_UNLESS(!this->IsEmpty(), ResultOutOfRange);
|
||||||
|
|
||||||
|
BucketTree::Offsets offsets;
|
||||||
|
R_TRY(this->GetOffsets(std::addressof(offsets)));
|
||||||
|
|
||||||
|
R_TRY(visitor->Initialize(this, offsets));
|
||||||
|
|
||||||
|
R_RETURN(visitor->Find(virtual_address));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::InvalidateCache() {
|
||||||
|
// Reset our offsets.
|
||||||
|
m_offset_cache.is_initialized = false;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::EnsureOffsetCache() {
|
||||||
|
// If we already have an offset cache, we're good.
|
||||||
|
R_SUCCEED_IF(m_offset_cache.is_initialized);
|
||||||
|
|
||||||
|
// Acquire exclusive right to edit the offset cache.
|
||||||
|
std::scoped_lock lk(m_offset_cache.mutex);
|
||||||
|
|
||||||
|
// Check again, to be sure.
|
||||||
|
R_SUCCEED_IF(m_offset_cache.is_initialized);
|
||||||
|
|
||||||
|
// Read/verify L1.
|
||||||
|
m_node_storage->Read(reinterpret_cast<u8*>(m_node_l1.Get()), m_node_size);
|
||||||
|
R_TRY(m_node_l1->Verify(0, m_node_size, sizeof(s64)));
|
||||||
|
|
||||||
|
// Get the node.
|
||||||
|
auto* const node = m_node_l1.Get<Node>();
|
||||||
|
|
||||||
|
s64 start_offset;
|
||||||
|
if (m_offset_count < m_entry_set_count && node->GetCount() < m_offset_count) {
|
||||||
|
start_offset = *node->GetEnd();
|
||||||
|
} else {
|
||||||
|
start_offset = *node->GetBegin();
|
||||||
|
}
|
||||||
|
const auto end_offset = node->GetEndOffset();
|
||||||
|
|
||||||
|
R_UNLESS(0 <= start_offset && start_offset <= node->GetBeginOffset(),
|
||||||
|
ResultInvalidBucketTreeEntryOffset);
|
||||||
|
R_UNLESS(start_offset < end_offset, ResultInvalidBucketTreeEntryOffset);
|
||||||
|
|
||||||
|
m_offset_cache.offsets.start_offset = start_offset;
|
||||||
|
m_offset_cache.offsets.end_offset = end_offset;
|
||||||
|
m_offset_cache.is_initialized = true;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::Initialize(const BucketTree* tree, const BucketTree::Offsets& offsets) {
|
||||||
|
ASSERT(tree != nullptr);
|
||||||
|
ASSERT(m_tree == nullptr || m_tree == tree);
|
||||||
|
|
||||||
|
if (m_entry == nullptr) {
|
||||||
|
m_entry = ::operator new(tree->m_entry_size);
|
||||||
|
R_UNLESS(m_entry != nullptr, ResultBufferAllocationFailed);
|
||||||
|
|
||||||
|
m_tree = tree;
|
||||||
|
m_offsets = offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::MoveNext() {
|
||||||
|
R_UNLESS(this->IsValid(), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Invalidate our index, and read the header for the next index.
|
||||||
|
auto entry_index = m_entry_index + 1;
|
||||||
|
if (entry_index == m_entry_set.info.count) {
|
||||||
|
const auto entry_set_index = m_entry_set.info.index + 1;
|
||||||
|
R_UNLESS(entry_set_index < m_entry_set_count, ResultOutOfRange);
|
||||||
|
|
||||||
|
m_entry_index = -1;
|
||||||
|
|
||||||
|
const auto end = m_entry_set.info.end;
|
||||||
|
|
||||||
|
const auto entry_set_size = m_tree->m_node_size;
|
||||||
|
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||||
|
|
||||||
|
m_tree->m_entry_storage->ReadObject(std::addressof(m_entry_set), entry_set_offset);
|
||||||
|
R_TRY(m_entry_set.header.Verify(entry_set_index, entry_set_size, m_tree->m_entry_size));
|
||||||
|
|
||||||
|
R_UNLESS(m_entry_set.info.start == end && m_entry_set.info.start < m_entry_set.info.end,
|
||||||
|
ResultInvalidBucketTreeEntrySetOffset);
|
||||||
|
|
||||||
|
entry_index = 0;
|
||||||
|
} else {
|
||||||
|
m_entry_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the new entry.
|
||||||
|
const auto entry_size = m_tree->m_entry_size;
|
||||||
|
const auto entry_offset = impl::GetBucketTreeEntryOffset(
|
||||||
|
m_entry_set.info.index, m_tree->m_node_size, entry_size, entry_index);
|
||||||
|
m_tree->m_entry_storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||||
|
|
||||||
|
// Note that we changed index.
|
||||||
|
m_entry_index = entry_index;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::MovePrevious() {
|
||||||
|
R_UNLESS(this->IsValid(), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Invalidate our index, and read the header for the previous index.
|
||||||
|
auto entry_index = m_entry_index;
|
||||||
|
if (entry_index == 0) {
|
||||||
|
R_UNLESS(m_entry_set.info.index > 0, ResultOutOfRange);
|
||||||
|
|
||||||
|
m_entry_index = -1;
|
||||||
|
|
||||||
|
const auto start = m_entry_set.info.start;
|
||||||
|
|
||||||
|
const auto entry_set_size = m_tree->m_node_size;
|
||||||
|
const auto entry_set_index = m_entry_set.info.index - 1;
|
||||||
|
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||||
|
|
||||||
|
m_tree->m_entry_storage->ReadObject(std::addressof(m_entry_set), entry_set_offset);
|
||||||
|
R_TRY(m_entry_set.header.Verify(entry_set_index, entry_set_size, m_tree->m_entry_size));
|
||||||
|
|
||||||
|
R_UNLESS(m_entry_set.info.end == start && m_entry_set.info.start < m_entry_set.info.end,
|
||||||
|
ResultInvalidBucketTreeEntrySetOffset);
|
||||||
|
|
||||||
|
entry_index = m_entry_set.info.count;
|
||||||
|
} else {
|
||||||
|
m_entry_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
--entry_index;
|
||||||
|
|
||||||
|
// Read the new entry.
|
||||||
|
const auto entry_size = m_tree->m_entry_size;
|
||||||
|
const auto entry_offset = impl::GetBucketTreeEntryOffset(
|
||||||
|
m_entry_set.info.index, m_tree->m_node_size, entry_size, entry_index);
|
||||||
|
m_tree->m_entry_storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||||
|
|
||||||
|
// Note that we changed index.
|
||||||
|
m_entry_index = entry_index;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::Find(s64 virtual_address) {
|
||||||
|
ASSERT(m_tree != nullptr);
|
||||||
|
|
||||||
|
// Get the node.
|
||||||
|
const auto* const node = m_tree->m_node_l1.Get<Node>();
|
||||||
|
R_UNLESS(virtual_address < node->GetEndOffset(), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Get the entry set index.
|
||||||
|
s32 entry_set_index = -1;
|
||||||
|
if (m_tree->IsExistOffsetL2OnL1() && virtual_address < node->GetBeginOffset()) {
|
||||||
|
const auto start = node->GetEnd();
|
||||||
|
const auto end = node->GetBegin() + m_tree->m_offset_count;
|
||||||
|
|
||||||
|
auto pos = std::upper_bound(start, end, virtual_address);
|
||||||
|
R_UNLESS(start < pos, ResultOutOfRange);
|
||||||
|
--pos;
|
||||||
|
|
||||||
|
entry_set_index = static_cast<s32>(pos - start);
|
||||||
|
} else {
|
||||||
|
const auto start = node->GetBegin();
|
||||||
|
const auto end = node->GetEnd();
|
||||||
|
|
||||||
|
auto pos = std::upper_bound(start, end, virtual_address);
|
||||||
|
R_UNLESS(start < pos, ResultOutOfRange);
|
||||||
|
--pos;
|
||||||
|
|
||||||
|
if (m_tree->IsExistL2()) {
|
||||||
|
const auto node_index = static_cast<s32>(pos - start);
|
||||||
|
R_UNLESS(0 <= node_index && node_index < m_tree->m_offset_count,
|
||||||
|
ResultInvalidBucketTreeNodeOffset);
|
||||||
|
|
||||||
|
R_TRY(this->FindEntrySet(std::addressof(entry_set_index), virtual_address, node_index));
|
||||||
|
} else {
|
||||||
|
entry_set_index = static_cast<s32>(pos - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the entry set index.
|
||||||
|
R_UNLESS(0 <= entry_set_index && entry_set_index < m_tree->m_entry_set_count,
|
||||||
|
ResultInvalidBucketTreeNodeOffset);
|
||||||
|
|
||||||
|
// Find the entry.
|
||||||
|
R_TRY(this->FindEntry(virtual_address, entry_set_index));
|
||||||
|
|
||||||
|
// Set count.
|
||||||
|
m_entry_set_count = m_tree->m_entry_set_count;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntrySet(s32* out_index, s64 virtual_address, s32 node_index) {
|
||||||
|
const auto node_size = m_tree->m_node_size;
|
||||||
|
|
||||||
|
PooledBuffer pool(node_size, 1);
|
||||||
|
if (node_size <= pool.GetSize()) {
|
||||||
|
R_RETURN(
|
||||||
|
this->FindEntrySetWithBuffer(out_index, virtual_address, node_index, pool.GetBuffer()));
|
||||||
|
} else {
|
||||||
|
pool.Deallocate();
|
||||||
|
R_RETURN(this->FindEntrySetWithoutBuffer(out_index, virtual_address, node_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntrySetWithBuffer(s32* out_index, s64 virtual_address,
|
||||||
|
s32 node_index, char* buffer) {
|
||||||
|
// Calculate node extents.
|
||||||
|
const auto node_size = m_tree->m_node_size;
|
||||||
|
const auto node_offset = (node_index + 1) * static_cast<s64>(node_size);
|
||||||
|
VirtualFile storage = m_tree->m_node_storage;
|
||||||
|
|
||||||
|
// Read the node.
|
||||||
|
storage->Read(reinterpret_cast<u8*>(buffer), node_size, node_offset);
|
||||||
|
|
||||||
|
// Validate the header.
|
||||||
|
NodeHeader header;
|
||||||
|
std::memcpy(std::addressof(header), buffer, NodeHeaderSize);
|
||||||
|
R_TRY(header.Verify(node_index, node_size, sizeof(s64)));
|
||||||
|
|
||||||
|
// Create the node, and find.
|
||||||
|
StorageNode node(sizeof(s64), header.count);
|
||||||
|
node.Find(buffer, virtual_address);
|
||||||
|
R_UNLESS(node.GetIndex() >= 0, ResultInvalidBucketTreeVirtualOffset);
|
||||||
|
|
||||||
|
// Return the index.
|
||||||
|
*out_index = static_cast<s32>(m_tree->GetEntrySetIndex(header.index, node.GetIndex()));
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntrySetWithoutBuffer(s32* out_index, s64 virtual_address,
|
||||||
|
s32 node_index) {
|
||||||
|
// Calculate node extents.
|
||||||
|
const auto node_size = m_tree->m_node_size;
|
||||||
|
const auto node_offset = (node_index + 1) * static_cast<s64>(node_size);
|
||||||
|
VirtualFile storage = m_tree->m_node_storage;
|
||||||
|
|
||||||
|
// Read and validate the header.
|
||||||
|
NodeHeader header;
|
||||||
|
storage->ReadObject(std::addressof(header), node_offset);
|
||||||
|
R_TRY(header.Verify(node_index, node_size, sizeof(s64)));
|
||||||
|
|
||||||
|
// Create the node, and find.
|
||||||
|
StorageNode node(node_offset, sizeof(s64), header.count);
|
||||||
|
R_TRY(node.Find(storage, virtual_address));
|
||||||
|
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||||
|
|
||||||
|
// Return the index.
|
||||||
|
*out_index = static_cast<s32>(m_tree->GetEntrySetIndex(header.index, node.GetIndex()));
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntry(s64 virtual_address, s32 entry_set_index) {
|
||||||
|
const auto entry_set_size = m_tree->m_node_size;
|
||||||
|
|
||||||
|
PooledBuffer pool(entry_set_size, 1);
|
||||||
|
if (entry_set_size <= pool.GetSize()) {
|
||||||
|
R_RETURN(this->FindEntryWithBuffer(virtual_address, entry_set_index, pool.GetBuffer()));
|
||||||
|
} else {
|
||||||
|
pool.Deallocate();
|
||||||
|
R_RETURN(this->FindEntryWithoutBuffer(virtual_address, entry_set_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntryWithBuffer(s64 virtual_address, s32 entry_set_index,
|
||||||
|
char* buffer) {
|
||||||
|
// Calculate entry set extents.
|
||||||
|
const auto entry_size = m_tree->m_entry_size;
|
||||||
|
const auto entry_set_size = m_tree->m_node_size;
|
||||||
|
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||||
|
VirtualFile storage = m_tree->m_entry_storage;
|
||||||
|
|
||||||
|
// Read the entry set.
|
||||||
|
storage->Read(reinterpret_cast<u8*>(buffer), entry_set_size, entry_set_offset);
|
||||||
|
|
||||||
|
// Validate the entry_set.
|
||||||
|
EntrySetHeader entry_set;
|
||||||
|
std::memcpy(std::addressof(entry_set), buffer, sizeof(EntrySetHeader));
|
||||||
|
R_TRY(entry_set.header.Verify(entry_set_index, entry_set_size, entry_size));
|
||||||
|
|
||||||
|
// Create the node, and find.
|
||||||
|
StorageNode node(entry_size, entry_set.info.count);
|
||||||
|
node.Find(buffer, virtual_address);
|
||||||
|
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||||
|
|
||||||
|
// Copy the data into entry.
|
||||||
|
const auto entry_index = node.GetIndex();
|
||||||
|
const auto entry_offset = impl::GetBucketTreeEntryOffset(0, entry_size, entry_index);
|
||||||
|
std::memcpy(m_entry, buffer + entry_offset, entry_size);
|
||||||
|
|
||||||
|
// Set our entry set/index.
|
||||||
|
m_entry_set = entry_set;
|
||||||
|
m_entry_index = entry_index;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result BucketTree::Visitor::FindEntryWithoutBuffer(s64 virtual_address, s32 entry_set_index) {
|
||||||
|
// Calculate entry set extents.
|
||||||
|
const auto entry_size = m_tree->m_entry_size;
|
||||||
|
const auto entry_set_size = m_tree->m_node_size;
|
||||||
|
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||||
|
VirtualFile storage = m_tree->m_entry_storage;
|
||||||
|
|
||||||
|
// Read and validate the entry_set.
|
||||||
|
EntrySetHeader entry_set;
|
||||||
|
storage->ReadObject(std::addressof(entry_set), entry_set_offset);
|
||||||
|
R_TRY(entry_set.header.Verify(entry_set_index, entry_set_size, entry_size));
|
||||||
|
|
||||||
|
// Create the node, and find.
|
||||||
|
StorageNode node(entry_set_offset, entry_size, entry_set.info.count);
|
||||||
|
R_TRY(node.Find(storage, virtual_address));
|
||||||
|
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||||
|
|
||||||
|
// Copy the data into entry.
|
||||||
|
const auto entry_index = node.GetIndex();
|
||||||
|
const auto entry_offset =
|
||||||
|
impl::GetBucketTreeEntryOffset(entry_set_offset, entry_size, entry_index);
|
||||||
|
storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||||
|
|
||||||
|
// Set our entry set/index.
|
||||||
|
m_entry_set = entry_set;
|
||||||
|
m_entry_index = entry_index;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
416
core/fs/fssystem/fssystem_bucket_tree.h
Normal file
416
core/fs/fssystem/fssystem_bucket_tree.h
Normal file
|
|
@ -0,0 +1,416 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/literals.h"
|
||||||
|
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
|
||||||
|
class BucketTree {
|
||||||
|
YUZU_NON_COPYABLE(BucketTree);
|
||||||
|
YUZU_NON_MOVEABLE(BucketTree);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr u32 Magic = Common::MakeMagic('B', 'K', 'T', 'R');
|
||||||
|
static constexpr u32 Version = 1;
|
||||||
|
|
||||||
|
static constexpr size_t NodeSizeMin = 1_KiB;
|
||||||
|
static constexpr size_t NodeSizeMax = 512_KiB;
|
||||||
|
|
||||||
|
public:
|
||||||
|
class Visitor;
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
s32 entry_count;
|
||||||
|
s32 reserved;
|
||||||
|
|
||||||
|
void Format(s32 entry_count);
|
||||||
|
Result Verify() const;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<Header>);
|
||||||
|
static_assert(sizeof(Header) == 0x10);
|
||||||
|
|
||||||
|
struct NodeHeader {
|
||||||
|
s32 index;
|
||||||
|
s32 count;
|
||||||
|
s64 offset;
|
||||||
|
|
||||||
|
Result Verify(s32 node_index, size_t node_size, size_t entry_size) const;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NodeHeader>);
|
||||||
|
static_assert(sizeof(NodeHeader) == 0x10);
|
||||||
|
|
||||||
|
struct Offsets {
|
||||||
|
s64 start_offset;
|
||||||
|
s64 end_offset;
|
||||||
|
|
||||||
|
constexpr bool IsInclude(s64 offset) const {
|
||||||
|
return this->start_offset <= offset && offset < this->end_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsInclude(s64 offset, s64 size) const {
|
||||||
|
return size > 0 && this->start_offset <= offset && size <= (this->end_offset - offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<Offsets>);
|
||||||
|
static_assert(sizeof(Offsets) == 0x10);
|
||||||
|
|
||||||
|
struct OffsetCache {
|
||||||
|
Offsets offsets;
|
||||||
|
std::mutex mutex;
|
||||||
|
bool is_initialized;
|
||||||
|
|
||||||
|
OffsetCache() : offsets{-1, -1}, mutex(), is_initialized(false) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ContinuousReadingInfo {
|
||||||
|
public:
|
||||||
|
constexpr ContinuousReadingInfo() : m_read_size(), m_skip_count(), m_done() {}
|
||||||
|
|
||||||
|
constexpr void Reset() {
|
||||||
|
m_read_size = 0;
|
||||||
|
m_skip_count = 0;
|
||||||
|
m_done = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetSkipCount(s32 count) {
|
||||||
|
ASSERT(count >= 0);
|
||||||
|
m_skip_count = count;
|
||||||
|
}
|
||||||
|
constexpr s32 GetSkipCount() const {
|
||||||
|
return m_skip_count;
|
||||||
|
}
|
||||||
|
constexpr bool CheckNeedScan() {
|
||||||
|
return (--m_skip_count) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void Done() {
|
||||||
|
m_read_size = 0;
|
||||||
|
m_done = true;
|
||||||
|
}
|
||||||
|
constexpr bool IsDone() const {
|
||||||
|
return m_done;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetReadSize(size_t size) {
|
||||||
|
m_read_size = size;
|
||||||
|
}
|
||||||
|
constexpr size_t GetReadSize() const {
|
||||||
|
return m_read_size;
|
||||||
|
}
|
||||||
|
constexpr bool CanDo() const {
|
||||||
|
return m_read_size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t m_read_size;
|
||||||
|
s32 m_skip_count;
|
||||||
|
bool m_done;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
class NodeBuffer {
|
||||||
|
YUZU_NON_COPYABLE(NodeBuffer);
|
||||||
|
|
||||||
|
public:
|
||||||
|
NodeBuffer() : m_header() {}
|
||||||
|
|
||||||
|
~NodeBuffer() {
|
||||||
|
ASSERT(m_header == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBuffer(NodeBuffer&& rhs) : m_header(rhs.m_header) {
|
||||||
|
rhs.m_header = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBuffer& operator=(NodeBuffer&& rhs) {
|
||||||
|
if (this != std::addressof(rhs)) {
|
||||||
|
ASSERT(m_header == nullptr);
|
||||||
|
|
||||||
|
m_header = rhs.m_header;
|
||||||
|
|
||||||
|
rhs.m_header = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Allocate(size_t node_size) {
|
||||||
|
ASSERT(m_header == nullptr);
|
||||||
|
|
||||||
|
m_header = ::operator new(node_size, std::align_val_t{sizeof(s64)});
|
||||||
|
|
||||||
|
// ASSERT(Common::IsAligned(m_header, sizeof(s64)));
|
||||||
|
|
||||||
|
return m_header != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Free(size_t node_size) {
|
||||||
|
if (m_header) {
|
||||||
|
::operator delete(m_header, std::align_val_t{sizeof(s64)});
|
||||||
|
m_header = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillZero(size_t node_size) const {
|
||||||
|
if (m_header) {
|
||||||
|
std::memset(m_header, 0, node_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeHeader* Get() const {
|
||||||
|
return reinterpret_cast<NodeHeader*>(m_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeHeader* operator->() const {
|
||||||
|
return this->Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T* Get() const {
|
||||||
|
static_assert(std::is_trivial_v<T>);
|
||||||
|
static_assert(sizeof(T) == sizeof(NodeHeader));
|
||||||
|
return reinterpret_cast<T*>(m_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void* m_header;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr s32 GetEntryCount(size_t node_size, size_t entry_size) {
|
||||||
|
return static_cast<s32>((node_size - sizeof(NodeHeader)) / entry_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s32 GetOffsetCount(size_t node_size) {
|
||||||
|
return static_cast<s32>((node_size - sizeof(NodeHeader)) / sizeof(s64));
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s32 GetEntrySetCount(size_t node_size, size_t entry_size, s32 entry_count) {
|
||||||
|
const s32 entry_count_per_node = GetEntryCount(node_size, entry_size);
|
||||||
|
return Common::DivideUp(entry_count, entry_count_per_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s32 GetNodeL2Count(size_t node_size, size_t entry_size, s32 entry_count) {
|
||||||
|
const s32 offset_count_per_node = GetOffsetCount(node_size);
|
||||||
|
const s32 entry_set_count = GetEntrySetCount(node_size, entry_size, entry_count);
|
||||||
|
|
||||||
|
if (entry_set_count <= offset_count_per_node) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s32 node_l2_count = Common::DivideUp(entry_set_count, offset_count_per_node);
|
||||||
|
ASSERT(node_l2_count <= offset_count_per_node);
|
||||||
|
|
||||||
|
return Common::DivideUp(entry_set_count - (offset_count_per_node - (node_l2_count - 1)),
|
||||||
|
offset_count_per_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
BucketTree()
|
||||||
|
: m_node_storage(), m_entry_storage(), m_node_l1(), m_node_size(), m_entry_size(),
|
||||||
|
m_entry_count(), m_offset_count(), m_entry_set_count(), m_offset_cache() {}
|
||||||
|
~BucketTree() {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile node_storage, VirtualFile entry_storage, size_t node_size,
|
||||||
|
size_t entry_size, s32 entry_count);
|
||||||
|
void Initialize(size_t node_size, s64 end_offset);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_node_size > 0;
|
||||||
|
}
|
||||||
|
bool IsEmpty() const {
|
||||||
|
return m_entry_size == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Find(Visitor* visitor, s64 virtual_address);
|
||||||
|
Result InvalidateCache();
|
||||||
|
|
||||||
|
s32 GetEntryCount() const {
|
||||||
|
return m_entry_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetOffsets(Offsets* out) {
|
||||||
|
// Ensure we have an offset cache.
|
||||||
|
R_TRY(this->EnsureOffsetCache());
|
||||||
|
|
||||||
|
// Set the output.
|
||||||
|
*out = m_offset_cache.offsets;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s64 QueryHeaderStorageSize() {
|
||||||
|
return sizeof(Header);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryNodeStorageSize(size_t node_size, size_t entry_size,
|
||||||
|
s32 entry_count) {
|
||||||
|
ASSERT(entry_size >= sizeof(s64));
|
||||||
|
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||||
|
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||||
|
ASSERT(entry_count >= 0);
|
||||||
|
|
||||||
|
if (entry_count <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (1 + GetNodeL2Count(node_size, entry_size, entry_count)) *
|
||||||
|
static_cast<s64>(node_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryEntryStorageSize(size_t node_size, size_t entry_size,
|
||||||
|
s32 entry_count) {
|
||||||
|
ASSERT(entry_size >= sizeof(s64));
|
||||||
|
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||||
|
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||||
|
ASSERT(entry_count >= 0);
|
||||||
|
|
||||||
|
if (entry_count <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return GetEntrySetCount(node_size, entry_size, entry_count) * static_cast<s64>(node_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename EntryType>
|
||||||
|
struct ContinuousReadingParam {
|
||||||
|
s64 offset;
|
||||||
|
size_t size;
|
||||||
|
NodeHeader entry_set;
|
||||||
|
s32 entry_index;
|
||||||
|
Offsets offsets;
|
||||||
|
EntryType entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename EntryType>
|
||||||
|
Result ScanContinuousReading(ContinuousReadingInfo* out_info,
|
||||||
|
const ContinuousReadingParam<EntryType>& param) const;
|
||||||
|
|
||||||
|
bool IsExistL2() const {
|
||||||
|
return m_offset_count < m_entry_set_count;
|
||||||
|
}
|
||||||
|
bool IsExistOffsetL2OnL1() const {
|
||||||
|
return this->IsExistL2() && m_node_l1->count < m_offset_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetEntrySetIndex(s32 node_index, s32 offset_index) const {
|
||||||
|
return (m_offset_count - m_node_l1->count) + (m_offset_count * node_index) + offset_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result EnsureOffsetCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable VirtualFile m_node_storage;
|
||||||
|
mutable VirtualFile m_entry_storage;
|
||||||
|
NodeBuffer m_node_l1;
|
||||||
|
size_t m_node_size;
|
||||||
|
size_t m_entry_size;
|
||||||
|
s32 m_entry_count;
|
||||||
|
s32 m_offset_count;
|
||||||
|
s32 m_entry_set_count;
|
||||||
|
OffsetCache m_offset_cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BucketTree::Visitor {
|
||||||
|
YUZU_NON_COPYABLE(Visitor);
|
||||||
|
YUZU_NON_MOVEABLE(Visitor);
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr Visitor()
|
||||||
|
: m_tree(), m_entry(), m_entry_index(-1), m_entry_set_count(), m_entry_set{} {}
|
||||||
|
~Visitor() {
|
||||||
|
if (m_entry != nullptr) {
|
||||||
|
::operator delete(m_entry, m_tree->m_entry_size);
|
||||||
|
m_tree = nullptr;
|
||||||
|
m_entry = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValid() const {
|
||||||
|
return m_entry_index >= 0;
|
||||||
|
}
|
||||||
|
bool CanMoveNext() const {
|
||||||
|
return this->IsValid() && (m_entry_index + 1 < m_entry_set.info.count ||
|
||||||
|
m_entry_set.info.index + 1 < m_entry_set_count);
|
||||||
|
}
|
||||||
|
bool CanMovePrevious() const {
|
||||||
|
return this->IsValid() && (m_entry_index > 0 || m_entry_set.info.index > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result MoveNext();
|
||||||
|
Result MovePrevious();
|
||||||
|
|
||||||
|
template <typename EntryType>
|
||||||
|
Result ScanContinuousReading(ContinuousReadingInfo* out_info, s64 offset, size_t size) const;
|
||||||
|
|
||||||
|
const void* Get() const {
|
||||||
|
ASSERT(this->IsValid());
|
||||||
|
return m_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T* Get() const {
|
||||||
|
ASSERT(this->IsValid());
|
||||||
|
return reinterpret_cast<const T*>(m_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BucketTree* GetTree() const {
|
||||||
|
return m_tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result Initialize(const BucketTree* tree, const BucketTree::Offsets& offsets);
|
||||||
|
|
||||||
|
Result Find(s64 virtual_address);
|
||||||
|
|
||||||
|
Result FindEntrySet(s32* out_index, s64 virtual_address, s32 node_index);
|
||||||
|
Result FindEntrySetWithBuffer(s32* out_index, s64 virtual_address, s32 node_index,
|
||||||
|
char* buffer);
|
||||||
|
Result FindEntrySetWithoutBuffer(s32* out_index, s64 virtual_address, s32 node_index);
|
||||||
|
|
||||||
|
Result FindEntry(s64 virtual_address, s32 entry_set_index);
|
||||||
|
Result FindEntryWithBuffer(s64 virtual_address, s32 entry_set_index, char* buffer);
|
||||||
|
Result FindEntryWithoutBuffer(s64 virtual_address, s32 entry_set_index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class BucketTree;
|
||||||
|
|
||||||
|
union EntrySetHeader {
|
||||||
|
NodeHeader header;
|
||||||
|
struct Info {
|
||||||
|
s32 index;
|
||||||
|
s32 count;
|
||||||
|
s64 end;
|
||||||
|
s64 start;
|
||||||
|
} info;
|
||||||
|
static_assert(std::is_trivial_v<Info>);
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<EntrySetHeader>);
|
||||||
|
|
||||||
|
const BucketTree* m_tree;
|
||||||
|
BucketTree::Offsets m_offsets;
|
||||||
|
void* m_entry;
|
||||||
|
s32 m_entry_index;
|
||||||
|
s32 m_entry_set_count;
|
||||||
|
EntrySetHeader m_entry_set;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
170
core/fs/fssystem/fssystem_bucket_tree_template_impl.h
Normal file
170
core/fs/fssystem/fssystem_bucket_tree_template_impl.h
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree_utils.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
template <typename EntryType>
|
||||||
|
Result BucketTree::ScanContinuousReading(ContinuousReadingInfo* out_info,
|
||||||
|
const ContinuousReadingParam<EntryType>& param) const {
|
||||||
|
static_assert(std::is_trivial_v<ContinuousReadingParam<EntryType>>);
|
||||||
|
|
||||||
|
// Validate our preconditions.
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
ASSERT(out_info != nullptr);
|
||||||
|
ASSERT(m_entry_size == sizeof(EntryType));
|
||||||
|
|
||||||
|
// Reset the output.
|
||||||
|
out_info->Reset();
|
||||||
|
|
||||||
|
// If there's nothing to read, we're done.
|
||||||
|
R_SUCCEED_IF(param.size == 0);
|
||||||
|
|
||||||
|
// If we're reading a fragment, we're done.
|
||||||
|
R_SUCCEED_IF(param.entry.IsFragment());
|
||||||
|
|
||||||
|
// Validate the first entry.
|
||||||
|
auto entry = param.entry;
|
||||||
|
auto cur_offset = param.offset;
|
||||||
|
R_UNLESS(entry.GetVirtualOffset() <= cur_offset, ResultOutOfRange);
|
||||||
|
|
||||||
|
// Create a pooled buffer for our scan.
|
||||||
|
PooledBuffer pool(m_node_size, 1);
|
||||||
|
char* buffer = nullptr;
|
||||||
|
|
||||||
|
s64 entry_storage_size = m_entry_storage->GetSize();
|
||||||
|
|
||||||
|
// Read the node.
|
||||||
|
if (m_node_size <= pool.GetSize()) {
|
||||||
|
buffer = pool.GetBuffer();
|
||||||
|
const auto ofs = param.entry_set.index * static_cast<s64>(m_node_size);
|
||||||
|
R_UNLESS(m_node_size + ofs <= static_cast<size_t>(entry_storage_size),
|
||||||
|
ResultInvalidBucketTreeNodeEntryCount);
|
||||||
|
|
||||||
|
m_entry_storage->Read(reinterpret_cast<u8*>(buffer), m_node_size, ofs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate extents.
|
||||||
|
const auto end_offset = cur_offset + static_cast<s64>(param.size);
|
||||||
|
s64 phys_offset = entry.GetPhysicalOffset();
|
||||||
|
|
||||||
|
// Start merge tracking.
|
||||||
|
s64 merge_size = 0;
|
||||||
|
s64 readable_size = 0;
|
||||||
|
bool merged = false;
|
||||||
|
|
||||||
|
// Iterate.
|
||||||
|
auto entry_index = param.entry_index;
|
||||||
|
for (const auto entry_count = param.entry_set.count; entry_index < entry_count; ++entry_index) {
|
||||||
|
// If we're past the end, we're done.
|
||||||
|
if (end_offset <= cur_offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the entry offset.
|
||||||
|
const auto entry_offset = entry.GetVirtualOffset();
|
||||||
|
R_UNLESS(entry_offset <= cur_offset, ResultInvalidIndirectEntryOffset);
|
||||||
|
|
||||||
|
// Get the next entry.
|
||||||
|
EntryType next_entry = {};
|
||||||
|
s64 next_entry_offset;
|
||||||
|
|
||||||
|
if (entry_index + 1 < entry_count) {
|
||||||
|
if (buffer != nullptr) {
|
||||||
|
const auto ofs = impl::GetBucketTreeEntryOffset(0, m_entry_size, entry_index + 1);
|
||||||
|
std::memcpy(std::addressof(next_entry), buffer + ofs, m_entry_size);
|
||||||
|
} else {
|
||||||
|
const auto ofs = impl::GetBucketTreeEntryOffset(param.entry_set.index, m_node_size,
|
||||||
|
m_entry_size, entry_index + 1);
|
||||||
|
m_entry_storage->ReadObject(std::addressof(next_entry), ofs);
|
||||||
|
}
|
||||||
|
|
||||||
|
next_entry_offset = next_entry.GetVirtualOffset();
|
||||||
|
R_UNLESS(param.offsets.IsInclude(next_entry_offset), ResultInvalidIndirectEntryOffset);
|
||||||
|
} else {
|
||||||
|
next_entry_offset = param.entry_set.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the next entry offset.
|
||||||
|
R_UNLESS(cur_offset < next_entry_offset, ResultInvalidIndirectEntryOffset);
|
||||||
|
|
||||||
|
// Determine the much data there is.
|
||||||
|
const auto data_size = next_entry_offset - cur_offset;
|
||||||
|
ASSERT(data_size > 0);
|
||||||
|
|
||||||
|
// Determine how much data we should read.
|
||||||
|
const auto remaining_size = end_offset - cur_offset;
|
||||||
|
const size_t read_size = static_cast<size_t>(std::min(data_size, remaining_size));
|
||||||
|
ASSERT(read_size <= param.size);
|
||||||
|
|
||||||
|
// Update our merge tracking.
|
||||||
|
if (entry.IsFragment()) {
|
||||||
|
// If we can't merge, stop looping.
|
||||||
|
if (EntryType::FragmentSizeMax <= read_size || remaining_size <= data_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, add the current size to the merge size.
|
||||||
|
merge_size += read_size;
|
||||||
|
} else {
|
||||||
|
// If we can't merge, stop looping.
|
||||||
|
if (phys_offset != entry.GetPhysicalOffset()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the size to the readable amount.
|
||||||
|
readable_size += merge_size + read_size;
|
||||||
|
ASSERT(readable_size <= static_cast<s64>(param.size));
|
||||||
|
|
||||||
|
// Update whether we've merged.
|
||||||
|
merged |= merge_size > 0;
|
||||||
|
merge_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_offset += read_size;
|
||||||
|
ASSERT(cur_offset <= end_offset);
|
||||||
|
|
||||||
|
phys_offset += next_entry_offset - entry_offset;
|
||||||
|
entry = next_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we merged, set our readable size.
|
||||||
|
if (merged) {
|
||||||
|
out_info->SetReadSize(static_cast<size_t>(readable_size));
|
||||||
|
}
|
||||||
|
out_info->SetSkipCount(entry_index - param.entry_index);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename EntryType>
|
||||||
|
Result BucketTree::Visitor::ScanContinuousReading(ContinuousReadingInfo* out_info, s64 offset,
|
||||||
|
size_t size) const {
|
||||||
|
static_assert(std::is_trivial_v<EntryType>);
|
||||||
|
ASSERT(this->IsValid());
|
||||||
|
|
||||||
|
// Create our parameters.
|
||||||
|
ContinuousReadingParam<EntryType> param = {
|
||||||
|
.offset = offset,
|
||||||
|
.size = size,
|
||||||
|
.entry_set = m_entry_set.header,
|
||||||
|
.entry_index = m_entry_index,
|
||||||
|
.offsets{},
|
||||||
|
.entry{},
|
||||||
|
};
|
||||||
|
std::memcpy(std::addressof(param.offsets), std::addressof(m_offsets),
|
||||||
|
sizeof(BucketTree::Offsets));
|
||||||
|
std::memcpy(std::addressof(param.entry), m_entry, sizeof(EntryType));
|
||||||
|
|
||||||
|
// Scan.
|
||||||
|
R_RETURN(m_tree->ScanContinuousReading<EntryType>(out_info, param));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
110
core/fs/fssystem/fssystem_bucket_tree_utils.h
Normal file
110
core/fs/fssystem/fssystem_bucket_tree_utils.h
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
|
||||||
|
namespace FileSys::impl {
|
||||||
|
|
||||||
|
class SafeValue {
|
||||||
|
public:
|
||||||
|
static s64 GetInt64(const void* ptr) {
|
||||||
|
s64 value;
|
||||||
|
std::memcpy(std::addressof(value), ptr, sizeof(s64));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static s64 GetInt64(const s64* ptr) {
|
||||||
|
return GetInt64(static_cast<const void*>(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
static s64 GetInt64(const s64& v) {
|
||||||
|
return GetInt64(std::addressof(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetInt64(void* dst, const void* src) {
|
||||||
|
std::memcpy(dst, src, sizeof(s64));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetInt64(void* dst, const s64* src) {
|
||||||
|
return SetInt64(dst, static_cast<const void*>(src));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetInt64(void* dst, const s64& v) {
|
||||||
|
return SetInt64(dst, std::addressof(v));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename IteratorType>
|
||||||
|
struct BucketTreeNode {
|
||||||
|
using Header = BucketTree::NodeHeader;
|
||||||
|
|
||||||
|
Header header;
|
||||||
|
|
||||||
|
s32 GetCount() const {
|
||||||
|
return this->header.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* GetArray() {
|
||||||
|
return std::addressof(this->header) + 1;
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
T* GetArray() {
|
||||||
|
return reinterpret_cast<T*>(this->GetArray());
|
||||||
|
}
|
||||||
|
const void* GetArray() const {
|
||||||
|
return std::addressof(this->header) + 1;
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
const T* GetArray() const {
|
||||||
|
return reinterpret_cast<const T*>(this->GetArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetBeginOffset() const {
|
||||||
|
return *this->GetArray<s64>();
|
||||||
|
}
|
||||||
|
s64 GetEndOffset() const {
|
||||||
|
return this->header.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
IteratorType GetBegin() {
|
||||||
|
return IteratorType(this->GetArray<s64>());
|
||||||
|
}
|
||||||
|
IteratorType GetEnd() {
|
||||||
|
return IteratorType(this->GetArray<s64>()) + this->header.count;
|
||||||
|
}
|
||||||
|
IteratorType GetBegin() const {
|
||||||
|
return IteratorType(this->GetArray<s64>());
|
||||||
|
}
|
||||||
|
IteratorType GetEnd() const {
|
||||||
|
return IteratorType(this->GetArray<s64>()) + this->header.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
IteratorType GetBegin(size_t entry_size) {
|
||||||
|
return IteratorType(this->GetArray(), entry_size);
|
||||||
|
}
|
||||||
|
IteratorType GetEnd(size_t entry_size) {
|
||||||
|
return IteratorType(this->GetArray(), entry_size) + this->header.count;
|
||||||
|
}
|
||||||
|
IteratorType GetBegin(size_t entry_size) const {
|
||||||
|
return IteratorType(this->GetArray(), entry_size);
|
||||||
|
}
|
||||||
|
IteratorType GetEnd(size_t entry_size) const {
|
||||||
|
return IteratorType(this->GetArray(), entry_size) + this->header.count;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr inline s64 GetBucketTreeEntryOffset(s64 entry_set_offset, size_t entry_size,
|
||||||
|
s32 entry_index) {
|
||||||
|
return entry_set_offset + sizeof(BucketTree::NodeHeader) +
|
||||||
|
entry_index * static_cast<s64>(entry_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline s64 GetBucketTreeEntryOffset(s32 entry_set_index, size_t node_size,
|
||||||
|
size_t entry_size, s32 entry_index) {
|
||||||
|
return GetBucketTreeEntryOffset(entry_set_index * static_cast<s64>(node_size), entry_size,
|
||||||
|
entry_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys::impl
|
||||||
963
core/fs/fssystem/fssystem_compressed_storage.h
Normal file
963
core/fs/fssystem/fssystem_compressed_storage.h
Normal file
|
|
@ -0,0 +1,963 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/literals.h"
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_compression_common.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
|
||||||
|
class CompressedStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(CompressedStorage);
|
||||||
|
YUZU_NON_MOVEABLE(CompressedStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr size_t NodeSize = 16_KiB;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
s64 virt_offset;
|
||||||
|
s64 phys_offset;
|
||||||
|
CompressionType compression_type;
|
||||||
|
s32 phys_size;
|
||||||
|
|
||||||
|
s64 GetPhysicalSize() const {
|
||||||
|
return this->phys_size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<Entry>);
|
||||||
|
static_assert(sizeof(Entry) == 0x18);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
class CompressedStorageCore {
|
||||||
|
YUZU_NON_COPYABLE(CompressedStorageCore);
|
||||||
|
YUZU_NON_MOVEABLE(CompressedStorageCore);
|
||||||
|
|
||||||
|
public:
|
||||||
|
CompressedStorageCore() : m_table(), m_data_storage() {}
|
||||||
|
|
||||||
|
~CompressedStorageCore() {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
Result Initialize(VirtualFile data_storage, VirtualFile node_storage,
|
||||||
|
VirtualFile entry_storage, s32 bktr_entry_count, size_t block_size_max,
|
||||||
|
size_t continuous_reading_size_max,
|
||||||
|
GetDecompressorFunction get_decompressor) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(0 < block_size_max);
|
||||||
|
ASSERT(block_size_max <= continuous_reading_size_max);
|
||||||
|
ASSERT(get_decompressor != nullptr);
|
||||||
|
|
||||||
|
// Initialize our entry table.
|
||||||
|
R_TRY(m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry),
|
||||||
|
bktr_entry_count));
|
||||||
|
|
||||||
|
// Set our other fields.
|
||||||
|
m_block_size_max = block_size_max;
|
||||||
|
m_continuous_reading_size_max = continuous_reading_size_max;
|
||||||
|
m_data_storage = data_storage;
|
||||||
|
m_get_decompressor_function = get_decompressor;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Finalize() {
|
||||||
|
if (this->IsInitialized()) {
|
||||||
|
m_table.Finalize();
|
||||||
|
m_data_storage = VirtualFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile GetDataStorage() {
|
||||||
|
return m_data_storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetDataStorageSize(s64* out) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(out != nullptr);
|
||||||
|
|
||||||
|
// Get size.
|
||||||
|
*out = m_data_storage->GetSize();
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
BucketTree& GetEntryTable() {
|
||||||
|
return m_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryList(Entry* out_entries, s32* out_read_count, s32 max_entry_count,
|
||||||
|
s64 offset, s64 size) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(size >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Check that we can output the count.
|
||||||
|
R_UNLESS(out_read_count != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Check that we have anything to read at all.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// Check that either we have a buffer, or this is to determine how many we need.
|
||||||
|
if (max_entry_count != 0) {
|
||||||
|
R_UNLESS(out_entries != nullptr, ResultNullptrArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the table offsets.
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||||
|
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||||
|
ResultUnexpectedInCompressedStorageA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the entries.
|
||||||
|
const auto end_offset = offset + size;
|
||||||
|
s32 read_count = 0;
|
||||||
|
while (visitor.Get<Entry>()->virt_offset < end_offset) {
|
||||||
|
// If we should be setting the output, do so.
|
||||||
|
if (max_entry_count != 0) {
|
||||||
|
// Ensure we only read as many entries as we can.
|
||||||
|
if (read_count >= max_entry_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the current output entry.
|
||||||
|
out_entries[read_count] = *visitor.Get<Entry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the read count.
|
||||||
|
++read_count;
|
||||||
|
|
||||||
|
// If we're at the end, we're done.
|
||||||
|
if (!visitor.CanMoveNext()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next entry.
|
||||||
|
R_TRY(visitor.MoveNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the output read count.
|
||||||
|
*out_read_count = read_count;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetSize(s64* out) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(out != nullptr);
|
||||||
|
|
||||||
|
// Get our table offsets.
|
||||||
|
BucketTree::Offsets offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(offsets)));
|
||||||
|
|
||||||
|
// Set the output.
|
||||||
|
*out = offsets.end_offset;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OperatePerEntry(s64 offset, s64 size, auto f) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(size >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Succeed if there's nothing to operate on.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// Get the table offsets.
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||||
|
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||||
|
ResultUnexpectedInCompressedStorageA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to operate in chunks.
|
||||||
|
auto cur_offset = offset;
|
||||||
|
const auto end_offset = offset + static_cast<s64>(size);
|
||||||
|
|
||||||
|
while (cur_offset < end_offset) {
|
||||||
|
// Get the current entry.
|
||||||
|
const auto cur_entry = *visitor.Get<Entry>();
|
||||||
|
|
||||||
|
// Get and validate the entry's offset.
|
||||||
|
const auto cur_entry_offset = cur_entry.virt_offset;
|
||||||
|
R_UNLESS(cur_entry_offset <= cur_offset, ResultUnexpectedInCompressedStorageA);
|
||||||
|
|
||||||
|
// Get and validate the next entry offset.
|
||||||
|
s64 next_entry_offset;
|
||||||
|
if (visitor.CanMoveNext()) {
|
||||||
|
R_TRY(visitor.MoveNext());
|
||||||
|
next_entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||||
|
R_UNLESS(table_offsets.IsInclude(next_entry_offset),
|
||||||
|
ResultUnexpectedInCompressedStorageA);
|
||||||
|
} else {
|
||||||
|
next_entry_offset = table_offsets.end_offset;
|
||||||
|
}
|
||||||
|
R_UNLESS(cur_offset < next_entry_offset, ResultUnexpectedInCompressedStorageA);
|
||||||
|
|
||||||
|
// Get the offset of the entry in the data we read.
|
||||||
|
const auto data_offset = cur_offset - cur_entry_offset;
|
||||||
|
const auto data_size = (next_entry_offset - cur_entry_offset);
|
||||||
|
ASSERT(data_size > 0);
|
||||||
|
|
||||||
|
// Determine how much is left.
|
||||||
|
const auto remaining_size = end_offset - cur_offset;
|
||||||
|
const auto cur_size = std::min<s64>(remaining_size, data_size - data_offset);
|
||||||
|
ASSERT(cur_size <= size);
|
||||||
|
|
||||||
|
// Get the data storage size.
|
||||||
|
s64 storage_size = m_data_storage->GetSize();
|
||||||
|
|
||||||
|
// Check that our read remains naively physically in bounds.
|
||||||
|
R_UNLESS(0 <= cur_entry.phys_offset && cur_entry.phys_offset <= storage_size,
|
||||||
|
ResultUnexpectedInCompressedStorageC);
|
||||||
|
|
||||||
|
// If we have any compression, verify that we remain physically in bounds.
|
||||||
|
if (cur_entry.compression_type != CompressionType::None) {
|
||||||
|
R_UNLESS(cur_entry.phys_offset + cur_entry.GetPhysicalSize() <= storage_size,
|
||||||
|
ResultUnexpectedInCompressedStorageC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that block alignment requirements are met.
|
||||||
|
if (CompressionTypeUtility::IsBlockAlignmentRequired(cur_entry.compression_type)) {
|
||||||
|
R_UNLESS(Common::IsAligned(cur_entry.phys_offset, CompressionBlockAlignment),
|
||||||
|
ResultUnexpectedInCompressedStorageA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke the operator.
|
||||||
|
bool is_continuous = true;
|
||||||
|
R_TRY(
|
||||||
|
f(std::addressof(is_continuous), cur_entry, data_size, data_offset, cur_size));
|
||||||
|
|
||||||
|
// If not continuous, we're done.
|
||||||
|
if (!is_continuous) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_offset += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
using ReadImplFunction = std::function<Result(void*, size_t)>;
|
||||||
|
using ReadFunction = std::function<Result(size_t, const ReadImplFunction&)>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Result Read(s64 offset, s64 size, const ReadFunction& read_func) {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Succeed immediately, if we have nothing to read.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// Declare read lambda.
|
||||||
|
constexpr int EntriesCountMax = 0x80;
|
||||||
|
struct Entries {
|
||||||
|
CompressionType compression_type;
|
||||||
|
u32 gap_from_prev;
|
||||||
|
u32 physical_size;
|
||||||
|
u32 virtual_size;
|
||||||
|
};
|
||||||
|
std::array<Entries, EntriesCountMax> entries;
|
||||||
|
s32 entry_count = 0;
|
||||||
|
Entry prev_entry = {
|
||||||
|
.virt_offset = -1,
|
||||||
|
.phys_offset{},
|
||||||
|
.compression_type{},
|
||||||
|
.phys_size{},
|
||||||
|
};
|
||||||
|
bool will_allocate_pooled_buffer = false;
|
||||||
|
s64 required_access_physical_offset = 0;
|
||||||
|
s64 required_access_physical_size = 0;
|
||||||
|
|
||||||
|
auto PerformRequiredRead = [&]() -> Result {
|
||||||
|
// If there are no entries, we have nothing to do.
|
||||||
|
R_SUCCEED_IF(entry_count == 0);
|
||||||
|
|
||||||
|
// Get the remaining size in a convenient form.
|
||||||
|
const size_t total_required_size =
|
||||||
|
static_cast<size_t>(required_access_physical_size);
|
||||||
|
|
||||||
|
// Perform the read based on whether we need to allocate a buffer.
|
||||||
|
if (will_allocate_pooled_buffer) {
|
||||||
|
// Allocate a pooled buffer.
|
||||||
|
PooledBuffer pooled_buffer;
|
||||||
|
if (pooled_buffer.GetAllocatableSizeMax() >= total_required_size) {
|
||||||
|
pooled_buffer.Allocate(total_required_size, m_block_size_max);
|
||||||
|
} else {
|
||||||
|
pooled_buffer.AllocateParticularlyLarge(
|
||||||
|
std::min<size_t>(
|
||||||
|
total_required_size,
|
||||||
|
PooledBuffer::GetAllocatableParticularlyLargeSizeMax()),
|
||||||
|
m_block_size_max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read each of the entries.
|
||||||
|
for (s32 entry_idx = 0; entry_idx < entry_count; ++entry_idx) {
|
||||||
|
// Determine the current read size.
|
||||||
|
bool will_use_pooled_buffer = false;
|
||||||
|
const size_t cur_read_size = [&]() -> size_t {
|
||||||
|
if (const size_t target_entry_size =
|
||||||
|
static_cast<size_t>(entries[entry_idx].physical_size) +
|
||||||
|
static_cast<size_t>(entries[entry_idx].gap_from_prev);
|
||||||
|
target_entry_size <= pooled_buffer.GetSize()) {
|
||||||
|
// We'll be using the pooled buffer.
|
||||||
|
will_use_pooled_buffer = true;
|
||||||
|
|
||||||
|
// Determine how much we can read.
|
||||||
|
const size_t max_size = std::min<size_t>(
|
||||||
|
required_access_physical_size, pooled_buffer.GetSize());
|
||||||
|
|
||||||
|
size_t read_size = 0;
|
||||||
|
for (auto n = entry_idx; n < entry_count; ++n) {
|
||||||
|
const size_t cur_entry_size =
|
||||||
|
static_cast<size_t>(entries[n].physical_size) +
|
||||||
|
static_cast<size_t>(entries[n].gap_from_prev);
|
||||||
|
if (read_size + cur_entry_size > max_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_size += cur_entry_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return read_size;
|
||||||
|
} else {
|
||||||
|
// If we don't fit, we must be uncompressed.
|
||||||
|
ASSERT(entries[entry_idx].compression_type ==
|
||||||
|
CompressionType::None);
|
||||||
|
|
||||||
|
// We can perform the whole of an uncompressed read directly.
|
||||||
|
return entries[entry_idx].virtual_size;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Perform the read based on whether or not we'll use the pooled buffer.
|
||||||
|
if (will_use_pooled_buffer) {
|
||||||
|
// Read the compressed data into the pooled buffer.
|
||||||
|
auto* const buffer = pooled_buffer.GetBuffer();
|
||||||
|
m_data_storage->Read(reinterpret_cast<u8*>(buffer), cur_read_size,
|
||||||
|
required_access_physical_offset);
|
||||||
|
|
||||||
|
// Decompress the data.
|
||||||
|
size_t buffer_offset;
|
||||||
|
for (buffer_offset = 0;
|
||||||
|
entry_idx < entry_count &&
|
||||||
|
((static_cast<size_t>(entries[entry_idx].physical_size) +
|
||||||
|
static_cast<size_t>(entries[entry_idx].gap_from_prev)) == 0 ||
|
||||||
|
buffer_offset < cur_read_size);
|
||||||
|
buffer_offset += entries[entry_idx++].physical_size) {
|
||||||
|
// Advance by the relevant gap.
|
||||||
|
buffer_offset += entries[entry_idx].gap_from_prev;
|
||||||
|
|
||||||
|
const auto compression_type = entries[entry_idx].compression_type;
|
||||||
|
switch (compression_type) {
|
||||||
|
case CompressionType::None: {
|
||||||
|
// Check that we can remain within bounds.
|
||||||
|
ASSERT(buffer_offset + entries[entry_idx].virtual_size <=
|
||||||
|
cur_read_size);
|
||||||
|
|
||||||
|
// Perform no decompression.
|
||||||
|
R_TRY(read_func(
|
||||||
|
entries[entry_idx].virtual_size,
|
||||||
|
[&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check that the size is valid.
|
||||||
|
ASSERT(dst_size == entries[entry_idx].virtual_size);
|
||||||
|
|
||||||
|
// We have no compression, so just copy the data
|
||||||
|
// out.
|
||||||
|
std::memcpy(dst, buffer + buffer_offset,
|
||||||
|
entries[entry_idx].virtual_size);
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CompressionType::Zeros: {
|
||||||
|
// Check that we can remain within bounds.
|
||||||
|
ASSERT(buffer_offset <= cur_read_size);
|
||||||
|
|
||||||
|
// Zero the memory.
|
||||||
|
R_TRY(read_func(
|
||||||
|
entries[entry_idx].virtual_size,
|
||||||
|
[&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check that the size is valid.
|
||||||
|
ASSERT(dst_size == entries[entry_idx].virtual_size);
|
||||||
|
|
||||||
|
// The data is zeroes, so zero the buffer.
|
||||||
|
std::memset(dst, 0, entries[entry_idx].virtual_size);
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// Check that we can remain within bounds.
|
||||||
|
ASSERT(buffer_offset + entries[entry_idx].physical_size <=
|
||||||
|
cur_read_size);
|
||||||
|
|
||||||
|
// Get the decompressor.
|
||||||
|
const auto decompressor =
|
||||||
|
this->GetDecompressor(compression_type);
|
||||||
|
R_UNLESS(decompressor != nullptr,
|
||||||
|
ResultUnexpectedInCompressedStorageB);
|
||||||
|
|
||||||
|
// Decompress the data.
|
||||||
|
R_TRY(read_func(entries[entry_idx].virtual_size,
|
||||||
|
[&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check that the size is valid.
|
||||||
|
ASSERT(dst_size ==
|
||||||
|
entries[entry_idx].virtual_size);
|
||||||
|
|
||||||
|
// Perform the decompression.
|
||||||
|
R_RETURN(decompressor(
|
||||||
|
dst, entries[entry_idx].virtual_size,
|
||||||
|
buffer + buffer_offset,
|
||||||
|
entries[entry_idx].physical_size));
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we processed the correct amount of data.
|
||||||
|
ASSERT(buffer_offset == cur_read_size);
|
||||||
|
} else {
|
||||||
|
// Account for the gap from the previous entry.
|
||||||
|
required_access_physical_offset += entries[entry_idx].gap_from_prev;
|
||||||
|
required_access_physical_size -= entries[entry_idx].gap_from_prev;
|
||||||
|
|
||||||
|
// We don't need the buffer (as the data is uncompressed), so just
|
||||||
|
// execute the read.
|
||||||
|
R_TRY(
|
||||||
|
read_func(cur_read_size, [&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check that the size is valid.
|
||||||
|
ASSERT(dst_size == cur_read_size);
|
||||||
|
|
||||||
|
// Perform the read.
|
||||||
|
m_data_storage->Read(reinterpret_cast<u8*>(dst), cur_read_size,
|
||||||
|
required_access_physical_offset);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance on.
|
||||||
|
required_access_physical_offset += cur_read_size;
|
||||||
|
required_access_physical_size -= cur_read_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we have nothing remaining to read.
|
||||||
|
ASSERT(required_access_physical_size == 0);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
} else {
|
||||||
|
// We don't need a buffer, so just execute the read.
|
||||||
|
R_TRY(read_func(total_required_size, [&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check that the size is valid.
|
||||||
|
ASSERT(dst_size == total_required_size);
|
||||||
|
|
||||||
|
// Perform the read.
|
||||||
|
m_data_storage->Read(reinterpret_cast<u8*>(dst), total_required_size,
|
||||||
|
required_access_physical_offset);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
};
|
||||||
|
|
||||||
|
R_TRY(this->OperatePerEntry(
|
||||||
|
offset, size,
|
||||||
|
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||||
|
s64 data_offset, s64 read_size) -> Result {
|
||||||
|
// Determine the physical extents.
|
||||||
|
s64 physical_offset, physical_size;
|
||||||
|
if (CompressionTypeUtility::IsRandomAccessible(entry.compression_type)) {
|
||||||
|
physical_offset = entry.phys_offset + data_offset;
|
||||||
|
physical_size = read_size;
|
||||||
|
} else {
|
||||||
|
physical_offset = entry.phys_offset;
|
||||||
|
physical_size = entry.GetPhysicalSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a pending data storage operation, perform it if we have to.
|
||||||
|
const s64 required_access_physical_end =
|
||||||
|
required_access_physical_offset + required_access_physical_size;
|
||||||
|
if (required_access_physical_size > 0) {
|
||||||
|
const bool required_by_gap =
|
||||||
|
!(required_access_physical_end <= physical_offset &&
|
||||||
|
physical_offset <= Common::AlignUp(required_access_physical_end,
|
||||||
|
CompressionBlockAlignment));
|
||||||
|
const bool required_by_continuous_size =
|
||||||
|
((physical_size + physical_offset) - required_access_physical_end) +
|
||||||
|
required_access_physical_size >
|
||||||
|
static_cast<s64>(m_continuous_reading_size_max);
|
||||||
|
const bool required_by_entry_count = entry_count == EntriesCountMax;
|
||||||
|
if (required_by_gap || required_by_continuous_size ||
|
||||||
|
required_by_entry_count) {
|
||||||
|
// Check that our planned access is sane.
|
||||||
|
ASSERT(!will_allocate_pooled_buffer ||
|
||||||
|
required_access_physical_size <=
|
||||||
|
static_cast<s64>(m_continuous_reading_size_max));
|
||||||
|
|
||||||
|
// Perform the required read.
|
||||||
|
const Result rc = PerformRequiredRead();
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
R_THROW(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset our requirements.
|
||||||
|
prev_entry.virt_offset = -1;
|
||||||
|
required_access_physical_size = 0;
|
||||||
|
entry_count = 0;
|
||||||
|
will_allocate_pooled_buffer = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check that we're within bounds on entries.
|
||||||
|
ASSERT(entry_count < EntriesCountMax);
|
||||||
|
|
||||||
|
// Determine if a buffer allocation is needed.
|
||||||
|
if (entry.compression_type != CompressionType::None ||
|
||||||
|
(prev_entry.virt_offset >= 0 &&
|
||||||
|
entry.virt_offset - prev_entry.virt_offset !=
|
||||||
|
entry.phys_offset - prev_entry.phys_offset)) {
|
||||||
|
will_allocate_pooled_buffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to access the data storage, update our required access parameters.
|
||||||
|
if (CompressionTypeUtility::IsDataStorageAccessRequired(
|
||||||
|
entry.compression_type)) {
|
||||||
|
// If the data is compressed, ensure the access is sane.
|
||||||
|
if (entry.compression_type != CompressionType::None) {
|
||||||
|
R_UNLESS(data_offset == 0, ResultInvalidOffset);
|
||||||
|
R_UNLESS(virtual_data_size == read_size, ResultInvalidSize);
|
||||||
|
R_UNLESS(entry.GetPhysicalSize() <= static_cast<s64>(m_block_size_max),
|
||||||
|
ResultUnexpectedInCompressedStorageD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the required access parameters.
|
||||||
|
s64 gap_from_prev;
|
||||||
|
if (required_access_physical_size > 0) {
|
||||||
|
gap_from_prev = physical_offset - required_access_physical_end;
|
||||||
|
} else {
|
||||||
|
gap_from_prev = 0;
|
||||||
|
required_access_physical_offset = physical_offset;
|
||||||
|
}
|
||||||
|
required_access_physical_size += physical_size + gap_from_prev;
|
||||||
|
|
||||||
|
// Create an entry to access the data storage.
|
||||||
|
entries[entry_count++] = {
|
||||||
|
.compression_type = entry.compression_type,
|
||||||
|
.gap_from_prev = static_cast<u32>(gap_from_prev),
|
||||||
|
.physical_size = static_cast<u32>(physical_size),
|
||||||
|
.virtual_size = static_cast<u32>(read_size),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Verify that we're allowed to be operating on the non-data-storage-access
|
||||||
|
// type.
|
||||||
|
R_UNLESS(entry.compression_type == CompressionType::Zeros,
|
||||||
|
ResultUnexpectedInCompressedStorageB);
|
||||||
|
|
||||||
|
// If we have entries, create a fake entry for the zero region.
|
||||||
|
if (entry_count != 0) {
|
||||||
|
// We need to have a physical size.
|
||||||
|
R_UNLESS(entry.GetPhysicalSize() != 0,
|
||||||
|
ResultUnexpectedInCompressedStorageD);
|
||||||
|
|
||||||
|
// Create a fake entry.
|
||||||
|
entries[entry_count++] = {
|
||||||
|
.compression_type = CompressionType::Zeros,
|
||||||
|
.gap_from_prev = 0,
|
||||||
|
.physical_size = 0,
|
||||||
|
.virtual_size = static_cast<u32>(read_size),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// We have no entries, so we can just perform the read.
|
||||||
|
const Result rc =
|
||||||
|
read_func(static_cast<size_t>(read_size),
|
||||||
|
[&](void* dst, size_t dst_size) -> Result {
|
||||||
|
// Check the space we should zero is correct.
|
||||||
|
ASSERT(dst_size == static_cast<size_t>(read_size));
|
||||||
|
|
||||||
|
// Zero the memory.
|
||||||
|
std::memset(dst, 0, read_size);
|
||||||
|
R_SUCCEED();
|
||||||
|
});
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
R_THROW(rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the previous entry.
|
||||||
|
prev_entry = entry;
|
||||||
|
|
||||||
|
// We're continuous.
|
||||||
|
*out_continuous = true;
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If we still have a pending access, perform it.
|
||||||
|
if (required_access_physical_size != 0) {
|
||||||
|
R_TRY(PerformRequiredRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
DecompressorFunction GetDecompressor(CompressionType type) const {
|
||||||
|
// Check that we can get a decompressor for the type.
|
||||||
|
if (CompressionTypeUtility::IsUnknownType(type)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the decompressor.
|
||||||
|
return m_get_decompressor_function(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_table.IsInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t m_block_size_max;
|
||||||
|
size_t m_continuous_reading_size_max;
|
||||||
|
BucketTree m_table;
|
||||||
|
VirtualFile m_data_storage;
|
||||||
|
GetDecompressorFunction m_get_decompressor_function;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
YUZU_NON_COPYABLE(CacheManager);
|
||||||
|
YUZU_NON_MOVEABLE(CacheManager);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct AccessRange {
|
||||||
|
s64 virtual_offset;
|
||||||
|
s64 virtual_size;
|
||||||
|
u32 physical_size;
|
||||||
|
bool is_block_alignment_required;
|
||||||
|
|
||||||
|
s64 GetEndVirtualOffset() const {
|
||||||
|
return this->virtual_offset + this->virtual_size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<AccessRange>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
CacheManager() = default;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Result Initialize(s64 storage_size, size_t cache_size_0, size_t cache_size_1,
|
||||||
|
size_t max_cache_entries) {
|
||||||
|
// Set our fields.
|
||||||
|
m_storage_size = storage_size;
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Read(CompressedStorageCore& core, s64 offset, void* buffer, size_t size) {
|
||||||
|
// If we have nothing to read, succeed.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// Check that we have a buffer to read into.
|
||||||
|
R_UNLESS(buffer != nullptr, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Check that the read is in bounds.
|
||||||
|
R_UNLESS(offset <= m_storage_size, ResultInvalidOffset);
|
||||||
|
|
||||||
|
// Determine how much we can read.
|
||||||
|
const size_t read_size = std::min<size_t>(size, m_storage_size - offset);
|
||||||
|
|
||||||
|
// Create head/tail ranges.
|
||||||
|
AccessRange head_range = {};
|
||||||
|
AccessRange tail_range = {};
|
||||||
|
bool is_tail_set = false;
|
||||||
|
|
||||||
|
// Operate to determine the head range.
|
||||||
|
R_TRY(core.OperatePerEntry(
|
||||||
|
offset, 1,
|
||||||
|
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||||
|
s64 data_offset, s64 data_read_size) -> Result {
|
||||||
|
// Set the head range.
|
||||||
|
head_range = {
|
||||||
|
.virtual_offset = entry.virt_offset,
|
||||||
|
.virtual_size = virtual_data_size,
|
||||||
|
.physical_size = static_cast<u32>(entry.phys_size),
|
||||||
|
.is_block_alignment_required =
|
||||||
|
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||||
|
entry.compression_type),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If required, set the tail range.
|
||||||
|
if (static_cast<s64>(offset + read_size) <=
|
||||||
|
entry.virt_offset + virtual_data_size) {
|
||||||
|
tail_range = {
|
||||||
|
.virtual_offset = entry.virt_offset,
|
||||||
|
.virtual_size = virtual_data_size,
|
||||||
|
.physical_size = static_cast<u32>(entry.phys_size),
|
||||||
|
.is_block_alignment_required =
|
||||||
|
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||||
|
entry.compression_type),
|
||||||
|
};
|
||||||
|
is_tail_set = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only want to determine the head range, so we're not continuous.
|
||||||
|
*out_continuous = false;
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If necessary, determine the tail range.
|
||||||
|
if (!is_tail_set) {
|
||||||
|
R_TRY(core.OperatePerEntry(
|
||||||
|
offset + read_size - 1, 1,
|
||||||
|
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||||
|
s64 data_offset, s64 data_read_size) -> Result {
|
||||||
|
// Set the tail range.
|
||||||
|
tail_range = {
|
||||||
|
.virtual_offset = entry.virt_offset,
|
||||||
|
.virtual_size = virtual_data_size,
|
||||||
|
.physical_size = static_cast<u32>(entry.phys_size),
|
||||||
|
.is_block_alignment_required =
|
||||||
|
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||||
|
entry.compression_type),
|
||||||
|
};
|
||||||
|
|
||||||
|
// We only want to determine the tail range, so we're not continuous.
|
||||||
|
*out_continuous = false;
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin performing the accesses.
|
||||||
|
s64 cur_offset = offset;
|
||||||
|
size_t cur_size = read_size;
|
||||||
|
char* cur_dst = static_cast<char*>(buffer);
|
||||||
|
|
||||||
|
// Determine our alignment.
|
||||||
|
const bool head_unaligned = head_range.is_block_alignment_required &&
|
||||||
|
(cur_offset != head_range.virtual_offset ||
|
||||||
|
static_cast<s64>(cur_size) < head_range.virtual_size);
|
||||||
|
const bool tail_unaligned = [&]() -> bool {
|
||||||
|
if (tail_range.is_block_alignment_required) {
|
||||||
|
if (static_cast<s64>(cur_size + cur_offset) ==
|
||||||
|
tail_range.GetEndVirtualOffset()) {
|
||||||
|
return false;
|
||||||
|
} else if (!head_unaligned) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return head_range.GetEndVirtualOffset() <
|
||||||
|
static_cast<s64>(cur_size + cur_offset);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Determine start/end offsets.
|
||||||
|
const s64 start_offset =
|
||||||
|
head_range.is_block_alignment_required ? head_range.virtual_offset : cur_offset;
|
||||||
|
const s64 end_offset = tail_range.is_block_alignment_required
|
||||||
|
? tail_range.GetEndVirtualOffset()
|
||||||
|
: cur_offset + cur_size;
|
||||||
|
|
||||||
|
// Perform the read.
|
||||||
|
bool is_burst_reading = false;
|
||||||
|
R_TRY(core.Read(
|
||||||
|
start_offset, end_offset - start_offset,
|
||||||
|
[&](size_t size_buffer_required,
|
||||||
|
const CompressedStorageCore::ReadImplFunction& read_impl) -> Result {
|
||||||
|
// Determine whether we're burst reading.
|
||||||
|
const AccessRange* unaligned_range = nullptr;
|
||||||
|
if (!is_burst_reading) {
|
||||||
|
// Check whether we're using head, tail, or none as unaligned.
|
||||||
|
if (head_unaligned && head_range.virtual_offset <= cur_offset &&
|
||||||
|
cur_offset < head_range.GetEndVirtualOffset()) {
|
||||||
|
unaligned_range = std::addressof(head_range);
|
||||||
|
} else if (tail_unaligned && tail_range.virtual_offset <= cur_offset &&
|
||||||
|
cur_offset < tail_range.GetEndVirtualOffset()) {
|
||||||
|
unaligned_range = std::addressof(tail_range);
|
||||||
|
} else {
|
||||||
|
is_burst_reading = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ASSERT((is_burst_reading ^ (unaligned_range != nullptr)));
|
||||||
|
|
||||||
|
// Perform reading by burst, or not.
|
||||||
|
if (is_burst_reading) {
|
||||||
|
// Check that the access is valid for burst reading.
|
||||||
|
ASSERT(size_buffer_required <= cur_size);
|
||||||
|
|
||||||
|
// Perform the read.
|
||||||
|
Result rc = read_impl(cur_dst, size_buffer_required);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
R_THROW(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_dst += size_buffer_required;
|
||||||
|
cur_offset += size_buffer_required;
|
||||||
|
cur_size -= size_buffer_required;
|
||||||
|
|
||||||
|
// Determine whether we're going to continue burst reading.
|
||||||
|
const s64 offset_aligned =
|
||||||
|
tail_unaligned ? tail_range.virtual_offset : end_offset;
|
||||||
|
ASSERT(cur_offset <= offset_aligned);
|
||||||
|
|
||||||
|
if (offset_aligned <= cur_offset) {
|
||||||
|
is_burst_reading = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're not burst reading, so we have some unaligned range.
|
||||||
|
ASSERT(unaligned_range != nullptr);
|
||||||
|
|
||||||
|
// Check that the size is correct.
|
||||||
|
ASSERT(size_buffer_required ==
|
||||||
|
static_cast<size_t>(unaligned_range->virtual_size));
|
||||||
|
|
||||||
|
// Get a pooled buffer for our read.
|
||||||
|
PooledBuffer pooled_buffer;
|
||||||
|
pooled_buffer.Allocate(size_buffer_required, size_buffer_required);
|
||||||
|
|
||||||
|
// Perform read.
|
||||||
|
Result rc = read_impl(pooled_buffer.GetBuffer(), size_buffer_required);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
R_THROW(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the data we read to the destination.
|
||||||
|
const size_t skip_size = cur_offset - unaligned_range->virtual_offset;
|
||||||
|
const size_t copy_size = std::min<size_t>(
|
||||||
|
cur_size, unaligned_range->GetEndVirtualOffset() - cur_offset);
|
||||||
|
|
||||||
|
std::memcpy(cur_dst, pooled_buffer.GetBuffer() + skip_size, copy_size);
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
cur_dst += copy_size;
|
||||||
|
cur_offset += copy_size;
|
||||||
|
cur_size -= copy_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}));
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
s64 m_storage_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
CompressedStorage() = default;
|
||||||
|
virtual ~CompressedStorage() {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile data_storage, VirtualFile node_storage, VirtualFile entry_storage,
|
||||||
|
s32 bktr_entry_count, size_t block_size_max,
|
||||||
|
size_t continuous_reading_size_max, GetDecompressorFunction get_decompressor,
|
||||||
|
size_t cache_size_0, size_t cache_size_1, s32 max_cache_entries) {
|
||||||
|
// Initialize our core.
|
||||||
|
R_TRY(m_core.Initialize(data_storage, node_storage, entry_storage, bktr_entry_count,
|
||||||
|
block_size_max, continuous_reading_size_max, get_decompressor));
|
||||||
|
|
||||||
|
// Get our core size.
|
||||||
|
s64 core_size = 0;
|
||||||
|
R_TRY(m_core.GetSize(std::addressof(core_size)));
|
||||||
|
|
||||||
|
// Initialize our cache manager.
|
||||||
|
R_TRY(m_cache_manager.Initialize(core_size, cache_size_0, cache_size_1, max_cache_entries));
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Finalize() {
|
||||||
|
m_core.Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile GetDataStorage() {
|
||||||
|
return m_core.GetDataStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetDataStorageSize(s64* out) {
|
||||||
|
R_RETURN(m_core.GetDataStorageSize(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryList(Entry* out_entries, s32* out_read_count, s32 max_entry_count, s64 offset,
|
||||||
|
s64 size) {
|
||||||
|
R_RETURN(m_core.GetEntryList(out_entries, out_read_count, max_entry_count, offset, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
BucketTree& GetEntryTable() {
|
||||||
|
return m_core.GetEntryTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
s64 ret{};
|
||||||
|
m_core.GetSize(&ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
if (R_SUCCEEDED(m_cache_manager.Read(m_core, offset, buffer, size))) {
|
||||||
|
return size;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable CompressedStorageCore m_core;
|
||||||
|
mutable CacheManager m_cache_manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
43
core/fs/fssystem/fssystem_compression_common.h
Normal file
43
core/fs/fssystem/fssystem_compression_common.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class CompressionType : u8 {
|
||||||
|
None = 0,
|
||||||
|
Zeros = 1,
|
||||||
|
Two = 2,
|
||||||
|
Lz4 = 3,
|
||||||
|
Unknown = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
using DecompressorFunction = Result (*)(void*, size_t, const void*, size_t);
|
||||||
|
using GetDecompressorFunction = DecompressorFunction (*)(CompressionType);
|
||||||
|
|
||||||
|
constexpr s64 CompressionBlockAlignment = 0x10;
|
||||||
|
|
||||||
|
namespace CompressionTypeUtility {
|
||||||
|
|
||||||
|
constexpr bool IsBlockAlignmentRequired(CompressionType type) {
|
||||||
|
return type != CompressionType::None && type != CompressionType::Zeros;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsDataStorageAccessRequired(CompressionType type) {
|
||||||
|
return type != CompressionType::Zeros;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsRandomAccessible(CompressionType type) {
|
||||||
|
return type == CompressionType::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsUnknownType(CompressionType type) {
|
||||||
|
return type >= CompressionType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CompressionTypeUtility
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
36
core/fs/fssystem/fssystem_compression_configuration.cpp
Normal file
36
core/fs/fssystem/fssystem_compression_configuration.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/lz4_compression.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_compression_configuration.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Result DecompressLz4(void* dst, size_t dst_size, const void* src, size_t src_size) {
|
||||||
|
auto result = Common::Compression::DecompressDataLZ4(dst, dst_size, src, src_size);
|
||||||
|
R_UNLESS(static_cast<size_t>(result) == dst_size, ResultUnexpectedInCompressedStorageC);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr DecompressorFunction GetNcaDecompressorFunction(CompressionType type) {
|
||||||
|
switch (type) {
|
||||||
|
case CompressionType::Lz4:
|
||||||
|
return DecompressLz4;
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const NcaCompressionConfiguration& GetNcaCompressionConfiguration() {
|
||||||
|
static const NcaCompressionConfiguration configuration = {
|
||||||
|
.get_decompressor = GetNcaDecompressorFunction,
|
||||||
|
};
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
12
core/fs/fssystem/fssystem_compression_configuration.h
Normal file
12
core/fs/fssystem/fssystem_compression_configuration.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
const NcaCompressionConfiguration& GetNcaCompressionConfiguration();
|
||||||
|
|
||||||
|
}
|
||||||
65
core/fs/fssystem/fssystem_crypto_configuration.cpp
Normal file
65
core/fs/fssystem/fssystem_crypto_configuration.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/crypto/aes_util.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_crypto_configuration.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void GenerateKey(void* dst_key, size_t dst_key_size, const void* src_key, size_t src_key_size,
|
||||||
|
s32 key_type) {
|
||||||
|
if (key_type == static_cast<s32>(KeyType::ZeroKey)) {
|
||||||
|
std::memset(dst_key, 0, dst_key_size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_type == static_cast<s32>(KeyType::InvalidKey) ||
|
||||||
|
key_type < static_cast<s32>(KeyType::ZeroKey) ||
|
||||||
|
key_type >= static_cast<s32>(KeyType::NcaExternalKey)) {
|
||||||
|
std::memset(dst_key, 0xFF, dst_key_size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& instance = Core::Crypto::KeyManager::Instance();
|
||||||
|
|
||||||
|
if (key_type == static_cast<s32>(KeyType::NcaHeaderKey1) ||
|
||||||
|
key_type == static_cast<s32>(KeyType::NcaHeaderKey2)) {
|
||||||
|
const s32 key_index = static_cast<s32>(KeyType::NcaHeaderKey2) == key_type;
|
||||||
|
const auto key = instance.GetKey(Core::Crypto::S256KeyType::Header);
|
||||||
|
std::memcpy(dst_key, key.data() + key_index * 0x10, std::min(dst_key_size, key.size() / 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s32 key_generation =
|
||||||
|
std::max(key_type / NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount, 1) - 1;
|
||||||
|
const s32 key_index = key_type % NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount;
|
||||||
|
|
||||||
|
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(
|
||||||
|
instance.GetKey(Core::Crypto::S128KeyType::KeyArea, key_generation, key_index),
|
||||||
|
Core::Crypto::Mode::ECB);
|
||||||
|
cipher.Transcode(reinterpret_cast<const u8*>(src_key), src_key_size,
|
||||||
|
reinterpret_cast<u8*>(dst_key), Core::Crypto::Op::Decrypt);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const NcaCryptoConfiguration& GetCryptoConfiguration() {
|
||||||
|
static const NcaCryptoConfiguration configuration = {
|
||||||
|
.header_1_sign_key_moduli{},
|
||||||
|
.header_1_sign_key_public_exponent{},
|
||||||
|
.key_area_encryption_key_source{},
|
||||||
|
.header_encryption_key_source{},
|
||||||
|
.header_encrypted_encryption_keys{},
|
||||||
|
.generate_key = GenerateKey,
|
||||||
|
.verify_sign1{},
|
||||||
|
.is_plaintext_header_available{},
|
||||||
|
.is_available_sw_key{},
|
||||||
|
};
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
12
core/fs/fssystem/fssystem_crypto_configuration.h
Normal file
12
core/fs/fssystem/fssystem_crypto_configuration.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
const NcaCryptoConfiguration& GetCryptoConfiguration();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
HierarchicalIntegrityVerificationStorage::HierarchicalIntegrityVerificationStorage()
|
||||||
|
: m_data_size(-1) {
|
||||||
|
for (size_t i = 0; i < MaxLayers - 1; i++) {
|
||||||
|
m_verify_storages[i] = std::make_shared<IntegrityVerificationStorage>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result HierarchicalIntegrityVerificationStorage::Initialize(
|
||||||
|
const HierarchicalIntegrityVerificationInformation& info,
|
||||||
|
HierarchicalStorageInformation storage, int max_data_cache_entries, int max_hash_cache_entries,
|
||||||
|
s8 buffer_level) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(IntegrityMinLayerCount <= info.max_layers && info.max_layers <= IntegrityMaxLayerCount);
|
||||||
|
|
||||||
|
// Set member variables.
|
||||||
|
m_max_layers = info.max_layers;
|
||||||
|
|
||||||
|
// Initialize the top level verification storage.
|
||||||
|
m_verify_storages[0]->Initialize(storage[HierarchicalStorageInformation::MasterStorage],
|
||||||
|
storage[HierarchicalStorageInformation::Layer1Storage],
|
||||||
|
static_cast<s64>(1) << info.info[0].block_order, HashSize,
|
||||||
|
false);
|
||||||
|
|
||||||
|
// Ensure we don't leak state if further initialization goes wrong.
|
||||||
|
ON_RESULT_FAILURE {
|
||||||
|
m_verify_storages[0]->Finalize();
|
||||||
|
m_data_size = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the top level buffer storage.
|
||||||
|
m_buffer_storages[0] = m_verify_storages[0];
|
||||||
|
R_UNLESS(m_buffer_storages[0] != nullptr, ResultAllocationMemoryFailedAllocateShared);
|
||||||
|
|
||||||
|
// Prepare to initialize the level storages.
|
||||||
|
s32 level = 0;
|
||||||
|
|
||||||
|
// Ensure we don't leak state if further initialization goes wrong.
|
||||||
|
ON_RESULT_FAILURE_2 {
|
||||||
|
m_verify_storages[level + 1]->Finalize();
|
||||||
|
for (; level > 0; --level) {
|
||||||
|
m_buffer_storages[level].reset();
|
||||||
|
m_verify_storages[level]->Finalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the level storages.
|
||||||
|
for (; level < m_max_layers - 3; ++level) {
|
||||||
|
// Initialize the verification storage.
|
||||||
|
auto buffer_storage =
|
||||||
|
std::make_shared<OffsetVfsFile>(m_buffer_storages[level], info.info[level].size, 0);
|
||||||
|
m_verify_storages[level + 1]->Initialize(
|
||||||
|
std::move(buffer_storage), storage[level + 2],
|
||||||
|
static_cast<s64>(1) << info.info[level + 1].block_order,
|
||||||
|
static_cast<s64>(1) << info.info[level].block_order, false);
|
||||||
|
|
||||||
|
// Initialize the buffer storage.
|
||||||
|
m_buffer_storages[level + 1] = m_verify_storages[level + 1];
|
||||||
|
R_UNLESS(m_buffer_storages[level + 1] != nullptr,
|
||||||
|
ResultAllocationMemoryFailedAllocateShared);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the final level storage.
|
||||||
|
{
|
||||||
|
// Initialize the verification storage.
|
||||||
|
auto buffer_storage =
|
||||||
|
std::make_shared<OffsetVfsFile>(m_buffer_storages[level], info.info[level].size, 0);
|
||||||
|
m_verify_storages[level + 1]->Initialize(
|
||||||
|
std::move(buffer_storage), storage[level + 2],
|
||||||
|
static_cast<s64>(1) << info.info[level + 1].block_order,
|
||||||
|
static_cast<s64>(1) << info.info[level].block_order, true);
|
||||||
|
|
||||||
|
// Initialize the buffer storage.
|
||||||
|
m_buffer_storages[level + 1] = m_verify_storages[level + 1];
|
||||||
|
R_UNLESS(m_buffer_storages[level + 1] != nullptr,
|
||||||
|
ResultAllocationMemoryFailedAllocateShared);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the data size.
|
||||||
|
m_data_size = info.info[level + 1].size;
|
||||||
|
|
||||||
|
// We succeeded.
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HierarchicalIntegrityVerificationStorage::Finalize() {
|
||||||
|
if (m_data_size >= 0) {
|
||||||
|
m_data_size = 0;
|
||||||
|
|
||||||
|
for (s32 level = m_max_layers - 2; level >= 0; --level) {
|
||||||
|
m_buffer_storages[level].reset();
|
||||||
|
m_verify_storages[level]->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_data_size = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HierarchicalIntegrityVerificationStorage::Read(u8* buffer, size_t size,
|
||||||
|
size_t offset) const {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(m_data_size >= 0);
|
||||||
|
|
||||||
|
// Succeed if zero-size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
return m_buffer_storages[m_max_layers - 2]->Read(buffer, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HierarchicalIntegrityVerificationStorage::GetSize() const {
|
||||||
|
return m_data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_types.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_integrity_verification_storage.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||||
|
Int64 offset;
|
||||||
|
Int64 size;
|
||||||
|
s32 block_order;
|
||||||
|
std::array<u8, 4> reserved;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationLevelInformation>);
|
||||||
|
static_assert(sizeof(HierarchicalIntegrityVerificationLevelInformation) == 0x18);
|
||||||
|
static_assert(alignof(HierarchicalIntegrityVerificationLevelInformation) == 0x4);
|
||||||
|
|
||||||
|
struct HierarchicalIntegrityVerificationInformation {
|
||||||
|
u32 max_layers;
|
||||||
|
std::array<HierarchicalIntegrityVerificationLevelInformation, IntegrityMaxLayerCount - 1> info;
|
||||||
|
HashSalt seed;
|
||||||
|
|
||||||
|
s64 GetLayeredHashSize() const {
|
||||||
|
return this->info[this->max_layers - 2].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetDataOffset() const {
|
||||||
|
return this->info[this->max_layers - 2].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetDataSize() const {
|
||||||
|
return this->info[this->max_layers - 2].size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationInformation>);
|
||||||
|
|
||||||
|
struct HierarchicalIntegrityVerificationMetaInformation {
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
u32 master_hash_size;
|
||||||
|
HierarchicalIntegrityVerificationInformation level_hash_info;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationMetaInformation>);
|
||||||
|
|
||||||
|
struct HierarchicalIntegrityVerificationSizeSet {
|
||||||
|
s64 control_size;
|
||||||
|
s64 master_hash_size;
|
||||||
|
std::array<s64, IntegrityMaxLayerCount - 2> layered_hash_sizes;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationSizeSet>);
|
||||||
|
|
||||||
|
class HierarchicalIntegrityVerificationStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(HierarchicalIntegrityVerificationStorage);
|
||||||
|
YUZU_NON_MOVEABLE(HierarchicalIntegrityVerificationStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
using GenerateRandomFunction = void (*)(void* dst, size_t size);
|
||||||
|
|
||||||
|
class HierarchicalStorageInformation {
|
||||||
|
public:
|
||||||
|
enum {
|
||||||
|
MasterStorage = 0,
|
||||||
|
Layer1Storage = 1,
|
||||||
|
Layer2Storage = 2,
|
||||||
|
Layer3Storage = 3,
|
||||||
|
Layer4Storage = 4,
|
||||||
|
Layer5Storage = 5,
|
||||||
|
DataStorage = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<VirtualFile, DataStorage + 1> m_storages;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void SetMasterHashStorage(VirtualFile s) {
|
||||||
|
m_storages[MasterStorage] = s;
|
||||||
|
}
|
||||||
|
void SetLayer1HashStorage(VirtualFile s) {
|
||||||
|
m_storages[Layer1Storage] = s;
|
||||||
|
}
|
||||||
|
void SetLayer2HashStorage(VirtualFile s) {
|
||||||
|
m_storages[Layer2Storage] = s;
|
||||||
|
}
|
||||||
|
void SetLayer3HashStorage(VirtualFile s) {
|
||||||
|
m_storages[Layer3Storage] = s;
|
||||||
|
}
|
||||||
|
void SetLayer4HashStorage(VirtualFile s) {
|
||||||
|
m_storages[Layer4Storage] = s;
|
||||||
|
}
|
||||||
|
void SetLayer5HashStorage(VirtualFile s) {
|
||||||
|
m_storages[Layer5Storage] = s;
|
||||||
|
}
|
||||||
|
void SetDataStorage(VirtualFile s) {
|
||||||
|
m_storages[DataStorage] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile& operator[](s32 index) {
|
||||||
|
ASSERT(MasterStorage <= index && index <= DataStorage);
|
||||||
|
return m_storages[index];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
HierarchicalIntegrityVerificationStorage();
|
||||||
|
virtual ~HierarchicalIntegrityVerificationStorage() override {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const HierarchicalIntegrityVerificationInformation& info,
|
||||||
|
HierarchicalStorageInformation storage, int max_data_cache_entries,
|
||||||
|
int max_hash_cache_entries, s8 buffer_level);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
virtual size_t GetSize() const override;
|
||||||
|
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_data_size >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetL1HashVerificationBlockSize() const {
|
||||||
|
return m_verify_storages[m_max_layers - 2]->GetBlockSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile GetL1HashStorage() {
|
||||||
|
return std::make_shared<OffsetVfsFile>(
|
||||||
|
m_buffer_storages[m_max_layers - 3],
|
||||||
|
Common::DivideUp(m_data_size, this->GetL1HashVerificationBlockSize()), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s8 GetDefaultDataCacheBufferLevel(u32 max_layers) {
|
||||||
|
return static_cast<s8>(16 + max_layers - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static constexpr s64 HashSize = 256 / 8;
|
||||||
|
static constexpr size_t MaxLayers = IntegrityMaxLayerCount;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static GenerateRandomFunction s_generate_random;
|
||||||
|
|
||||||
|
static void SetGenerateRandomFunction(GenerateRandomFunction func) {
|
||||||
|
s_generate_random = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend struct HierarchicalIntegrityVerificationMetaInformation;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<std::shared_ptr<IntegrityVerificationStorage>, MaxLayers - 1> m_verify_storages;
|
||||||
|
std::array<VirtualFile, MaxLayers - 1> m_buffer_storages;
|
||||||
|
s64 m_data_size;
|
||||||
|
s32 m_max_layers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
82
core/fs/fssystem/fssystem_hierarchical_sha256_storage.cpp
Normal file
82
core/fs/fssystem/fssystem_hierarchical_sha256_storage.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_hierarchical_sha256_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
s32 Log2(s32 value) {
|
||||||
|
ASSERT(value > 0);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(value));
|
||||||
|
|
||||||
|
s32 log = 0;
|
||||||
|
while ((value >>= 1) > 0) {
|
||||||
|
++log;
|
||||||
|
}
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result HierarchicalSha256Storage::Initialize(VirtualFile* base_storages, s32 layer_count,
|
||||||
|
size_t htbs, void* hash_buf, size_t hash_buf_size) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(layer_count == LayerCount);
|
||||||
|
ASSERT(Common::IsPowerOfTwo(htbs));
|
||||||
|
ASSERT(hash_buf != nullptr);
|
||||||
|
|
||||||
|
// Set size tracking members.
|
||||||
|
m_hash_target_block_size = static_cast<s32>(htbs);
|
||||||
|
m_log_size_ratio = Log2(m_hash_target_block_size / HashSize);
|
||||||
|
|
||||||
|
// Get the base storage size.
|
||||||
|
m_base_storage_size = base_storages[2]->GetSize();
|
||||||
|
{
|
||||||
|
auto size_guard = SCOPE_GUARD {
|
||||||
|
m_base_storage_size = 0;
|
||||||
|
};
|
||||||
|
R_UNLESS(m_base_storage_size <= static_cast<s64>(HashSize)
|
||||||
|
<< m_log_size_ratio << m_log_size_ratio,
|
||||||
|
ResultHierarchicalSha256BaseStorageTooLarge);
|
||||||
|
size_guard.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set hash buffer tracking members.
|
||||||
|
m_base_storage = base_storages[2];
|
||||||
|
m_hash_buffer = static_cast<char*>(hash_buf);
|
||||||
|
m_hash_buffer_size = hash_buf_size;
|
||||||
|
|
||||||
|
// Read the master hash.
|
||||||
|
std::array<u8, HashSize> master_hash{};
|
||||||
|
base_storages[0]->ReadObject(std::addressof(master_hash));
|
||||||
|
|
||||||
|
// Read and validate the data being hashed.
|
||||||
|
s64 hash_storage_size = base_storages[1]->GetSize();
|
||||||
|
ASSERT(Common::IsAligned(hash_storage_size, HashSize));
|
||||||
|
ASSERT(hash_storage_size <= m_hash_target_block_size);
|
||||||
|
ASSERT(hash_storage_size <= static_cast<s64>(m_hash_buffer_size));
|
||||||
|
|
||||||
|
base_storages[1]->Read(reinterpret_cast<u8*>(m_hash_buffer),
|
||||||
|
static_cast<size_t>(hash_storage_size), 0);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HierarchicalSha256Storage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Succeed if zero-size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that we have a buffer to read into.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Read the data.
|
||||||
|
return m_base_storage->Read(buffer, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
44
core/fs/fssystem/fssystem_hierarchical_sha256_storage.h
Normal file
44
core/fs/fssystem/fssystem_hierarchical_sha256_storage.h
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class HierarchicalSha256Storage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(HierarchicalSha256Storage);
|
||||||
|
YUZU_NON_MOVEABLE(HierarchicalSha256Storage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s32 LayerCount = 3;
|
||||||
|
static constexpr size_t HashSize = 256 / 8;
|
||||||
|
|
||||||
|
public:
|
||||||
|
HierarchicalSha256Storage() : m_mutex() {}
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile* base_storages, s32 layer_count, size_t htbs, void* hash_buf,
|
||||||
|
size_t hash_buf_size);
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return m_base_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t length, size_t offset) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_base_storage;
|
||||||
|
s64 m_base_storage_size;
|
||||||
|
char* m_hash_buffer;
|
||||||
|
size_t m_hash_buffer_size;
|
||||||
|
s32 m_hash_target_block_size;
|
||||||
|
s32 m_log_size_ratio;
|
||||||
|
std::mutex m_mutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
119
core/fs/fssystem/fssystem_indirect_storage.cpp
Normal file
119
core/fs/fssystem/fssystem_indirect_storage.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_indirect_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
Result IndirectStorage::Initialize(VirtualFile table_storage) {
|
||||||
|
// Read and verify the bucket tree header.
|
||||||
|
BucketTree::Header header;
|
||||||
|
table_storage->ReadObject(std::addressof(header));
|
||||||
|
R_TRY(header.Verify());
|
||||||
|
|
||||||
|
// Determine extents.
|
||||||
|
const auto node_storage_size = QueryNodeStorageSize(header.entry_count);
|
||||||
|
const auto entry_storage_size = QueryEntryStorageSize(header.entry_count);
|
||||||
|
const auto node_storage_offset = QueryHeaderStorageSize();
|
||||||
|
const auto entry_storage_offset = node_storage_offset + node_storage_size;
|
||||||
|
|
||||||
|
// Initialize.
|
||||||
|
R_RETURN(this->Initialize(
|
||||||
|
std::make_shared<OffsetVfsFile>(table_storage, node_storage_size, node_storage_offset),
|
||||||
|
std::make_shared<OffsetVfsFile>(table_storage, entry_storage_size, entry_storage_offset),
|
||||||
|
header.entry_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IndirectStorage::Finalize() {
|
||||||
|
if (this->IsInitialized()) {
|
||||||
|
m_table.Finalize();
|
||||||
|
for (auto i = 0; i < StorageCount; i++) {
|
||||||
|
m_data_storage[i] = VirtualFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result IndirectStorage::GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count,
|
||||||
|
s64 offset, s64 size) {
|
||||||
|
// Validate pre-conditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(size >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Clear the out count.
|
||||||
|
R_UNLESS(out_entry_count != nullptr, ResultNullptrArgument);
|
||||||
|
*out_entry_count = 0;
|
||||||
|
|
||||||
|
// Succeed if there's no range.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// If we have an output array, we need it to be non-null.
|
||||||
|
R_UNLESS(out_entries != nullptr || entry_count == 0, ResultNullptrArgument);
|
||||||
|
|
||||||
|
// Check that our range is valid.
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||||
|
|
||||||
|
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||||
|
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||||
|
ResultInvalidIndirectEntryOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to loop over entries.
|
||||||
|
const auto end_offset = offset + static_cast<s64>(size);
|
||||||
|
s32 count = 0;
|
||||||
|
|
||||||
|
auto cur_entry = *visitor.Get<Entry>();
|
||||||
|
while (cur_entry.GetVirtualOffset() < end_offset) {
|
||||||
|
// Try to write the entry to the out list.
|
||||||
|
if (entry_count != 0) {
|
||||||
|
if (count >= entry_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::memcpy(out_entries + count, std::addressof(cur_entry), sizeof(Entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
if (visitor.CanMoveNext()) {
|
||||||
|
R_TRY(visitor.MoveNext());
|
||||||
|
cur_entry = *visitor.Get<Entry>();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the output count.
|
||||||
|
*out_entry_count = count;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t IndirectStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Validate pre-conditions.
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Succeed if there's nothing to read.
|
||||||
|
if (size == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const_cast<IndirectStorage*>(this)->OperatePerEntry<true, true>(
|
||||||
|
offset, size,
|
||||||
|
[=](VirtualFile storage, s64 data_offset, s64 cur_offset, s64 cur_size) -> Result {
|
||||||
|
storage->Read(reinterpret_cast<u8*>(buffer) + (cur_offset - offset),
|
||||||
|
static_cast<size_t>(cur_size), data_offset);
|
||||||
|
R_SUCCEED();
|
||||||
|
});
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
294
core/fs/fssystem/fssystem_indirect_storage.h
Normal file
294
core/fs/fssystem/fssystem_indirect_storage.h
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_bucket_tree_template_impl.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class IndirectStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(IndirectStorage);
|
||||||
|
YUZU_NON_MOVEABLE(IndirectStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s32 StorageCount = 2;
|
||||||
|
static constexpr size_t NodeSize = 16_KiB;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::array<u8, sizeof(s64)> virt_offset;
|
||||||
|
std::array<u8, sizeof(s64)> phys_offset;
|
||||||
|
s32 storage_index;
|
||||||
|
|
||||||
|
void SetVirtualOffset(const s64& ofs) {
|
||||||
|
std::memcpy(this->virt_offset.data(), std::addressof(ofs), sizeof(s64));
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetVirtualOffset() const {
|
||||||
|
s64 offset;
|
||||||
|
std::memcpy(std::addressof(offset), this->virt_offset.data(), sizeof(s64));
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPhysicalOffset(const s64& ofs) {
|
||||||
|
std::memcpy(this->phys_offset.data(), std::addressof(ofs), sizeof(s64));
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetPhysicalOffset() const {
|
||||||
|
s64 offset;
|
||||||
|
std::memcpy(std::addressof(offset), this->phys_offset.data(), sizeof(s64));
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<Entry>);
|
||||||
|
static_assert(sizeof(Entry) == 0x14);
|
||||||
|
|
||||||
|
struct EntryData {
|
||||||
|
s64 virt_offset;
|
||||||
|
s64 phys_offset;
|
||||||
|
s32 storage_index;
|
||||||
|
|
||||||
|
void Set(const Entry& entry) {
|
||||||
|
this->virt_offset = entry.GetVirtualOffset();
|
||||||
|
this->phys_offset = entry.GetPhysicalOffset();
|
||||||
|
this->storage_index = entry.storage_index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<EntryData>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
IndirectStorage() : m_table(), m_data_storage() {}
|
||||||
|
virtual ~IndirectStorage() {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile table_storage);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_table.IsInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile node_storage, VirtualFile entry_storage, s32 entry_count) {
|
||||||
|
R_RETURN(
|
||||||
|
m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry), entry_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetStorage(s32 idx, VirtualFile storage) {
|
||||||
|
ASSERT(0 <= idx && idx < StorageCount);
|
||||||
|
m_data_storage[idx] = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void SetStorage(s32 idx, T storage, s64 offset, s64 size) {
|
||||||
|
ASSERT(0 <= idx && idx < StorageCount);
|
||||||
|
m_data_storage[idx] = std::make_shared<OffsetVfsFile>(storage, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count, s64 offset,
|
||||||
|
s64 size);
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
BucketTree::Offsets offsets{};
|
||||||
|
m_table.GetOffsets(std::addressof(offsets));
|
||||||
|
|
||||||
|
return offsets.end_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s64 QueryHeaderStorageSize() {
|
||||||
|
return BucketTree::QueryHeaderStorageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||||
|
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
BucketTree& GetEntryTable() {
|
||||||
|
return m_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile& GetDataStorage(s32 index) {
|
||||||
|
ASSERT(0 <= index && index < StorageCount);
|
||||||
|
return m_data_storage[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <bool ContinuousCheck, bool RangeCheck, typename F>
|
||||||
|
Result OperatePerEntry(s64 offset, s64 size, F func);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ContinuousReadingEntry {
|
||||||
|
static constexpr size_t FragmentSizeMax = 4_KiB;
|
||||||
|
|
||||||
|
IndirectStorage::Entry entry;
|
||||||
|
|
||||||
|
s64 GetVirtualOffset() const {
|
||||||
|
return this->entry.GetVirtualOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 GetPhysicalOffset() const {
|
||||||
|
return this->entry.GetPhysicalOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFragment() const {
|
||||||
|
return this->entry.storage_index != 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<ContinuousReadingEntry>);
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable BucketTree m_table;
|
||||||
|
std::array<VirtualFile, StorageCount> m_data_storage;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <bool ContinuousCheck, bool RangeCheck, typename F>
|
||||||
|
Result IndirectStorage::OperatePerEntry(s64 offset, s64 size, F func) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(offset >= 0);
|
||||||
|
ASSERT(size >= 0);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
// Succeed if there's nothing to operate on.
|
||||||
|
R_SUCCEED_IF(size == 0);
|
||||||
|
|
||||||
|
// Get the table offsets.
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||||
|
|
||||||
|
// Find the offset in our tree.
|
||||||
|
BucketTree::Visitor visitor;
|
||||||
|
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||||
|
{
|
||||||
|
const auto entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||||
|
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||||
|
ResultInvalidIndirectEntryOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to operate in chunks.
|
||||||
|
auto cur_offset = offset;
|
||||||
|
const auto end_offset = offset + static_cast<s64>(size);
|
||||||
|
BucketTree::ContinuousReadingInfo cr_info;
|
||||||
|
|
||||||
|
while (cur_offset < end_offset) {
|
||||||
|
// Get the current entry.
|
||||||
|
const auto cur_entry = *visitor.Get<Entry>();
|
||||||
|
|
||||||
|
// Get and validate the entry's offset.
|
||||||
|
const auto cur_entry_offset = cur_entry.GetVirtualOffset();
|
||||||
|
R_UNLESS(cur_entry_offset <= cur_offset, ResultInvalidIndirectEntryOffset);
|
||||||
|
|
||||||
|
// Validate the storage index.
|
||||||
|
R_UNLESS(0 <= cur_entry.storage_index && cur_entry.storage_index < StorageCount,
|
||||||
|
ResultInvalidIndirectEntryStorageIndex);
|
||||||
|
|
||||||
|
// If we need to check the continuous info, do so.
|
||||||
|
if constexpr (ContinuousCheck) {
|
||||||
|
// Scan, if we need to.
|
||||||
|
if (cr_info.CheckNeedScan()) {
|
||||||
|
R_TRY(visitor.ScanContinuousReading<ContinuousReadingEntry>(
|
||||||
|
std::addressof(cr_info), cur_offset,
|
||||||
|
static_cast<size_t>(end_offset - cur_offset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a base storage entry.
|
||||||
|
if (cr_info.CanDo()) {
|
||||||
|
// Ensure that we can process.
|
||||||
|
R_UNLESS(cur_entry.storage_index == 0, ResultInvalidIndirectEntryStorageIndex);
|
||||||
|
|
||||||
|
// Ensure that we remain within range.
|
||||||
|
const auto data_offset = cur_offset - cur_entry_offset;
|
||||||
|
const auto cur_entry_phys_offset = cur_entry.GetPhysicalOffset();
|
||||||
|
const auto cur_size = static_cast<s64>(cr_info.GetReadSize());
|
||||||
|
|
||||||
|
// If we should, verify the range.
|
||||||
|
if constexpr (RangeCheck) {
|
||||||
|
// Get the current data storage's size.
|
||||||
|
s64 cur_data_storage_size = m_data_storage[0]->GetSize();
|
||||||
|
|
||||||
|
R_UNLESS(0 <= cur_entry_phys_offset &&
|
||||||
|
cur_entry_phys_offset <= cur_data_storage_size,
|
||||||
|
ResultInvalidIndirectEntryOffset);
|
||||||
|
R_UNLESS(cur_entry_phys_offset + data_offset + cur_size <=
|
||||||
|
cur_data_storage_size,
|
||||||
|
ResultInvalidIndirectStorageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operate.
|
||||||
|
R_TRY(func(m_data_storage[0], cur_entry_phys_offset + data_offset, cur_offset,
|
||||||
|
cur_size));
|
||||||
|
|
||||||
|
// Mark as done.
|
||||||
|
cr_info.Done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate the next entry offset.
|
||||||
|
s64 next_entry_offset;
|
||||||
|
if (visitor.CanMoveNext()) {
|
||||||
|
R_TRY(visitor.MoveNext());
|
||||||
|
next_entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||||
|
R_UNLESS(table_offsets.IsInclude(next_entry_offset), ResultInvalidIndirectEntryOffset);
|
||||||
|
} else {
|
||||||
|
next_entry_offset = table_offsets.end_offset;
|
||||||
|
}
|
||||||
|
R_UNLESS(cur_offset < next_entry_offset, ResultInvalidIndirectEntryOffset);
|
||||||
|
|
||||||
|
// Get the offset of the entry in the data we read.
|
||||||
|
const auto data_offset = cur_offset - cur_entry_offset;
|
||||||
|
const auto data_size = (next_entry_offset - cur_entry_offset);
|
||||||
|
ASSERT(data_size > 0);
|
||||||
|
|
||||||
|
// Determine how much is left.
|
||||||
|
const auto remaining_size = end_offset - cur_offset;
|
||||||
|
const auto cur_size = std::min<s64>(remaining_size, data_size - data_offset);
|
||||||
|
ASSERT(cur_size <= size);
|
||||||
|
|
||||||
|
// Operate, if we need to.
|
||||||
|
bool needs_operate;
|
||||||
|
if constexpr (!ContinuousCheck) {
|
||||||
|
needs_operate = true;
|
||||||
|
} else {
|
||||||
|
needs_operate = !cr_info.IsDone() || cur_entry.storage_index != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_operate) {
|
||||||
|
const auto cur_entry_phys_offset = cur_entry.GetPhysicalOffset();
|
||||||
|
|
||||||
|
if constexpr (RangeCheck) {
|
||||||
|
// Get the current data storage's size.
|
||||||
|
s64 cur_data_storage_size = m_data_storage[cur_entry.storage_index]->GetSize();
|
||||||
|
|
||||||
|
// Ensure that we remain within range.
|
||||||
|
R_UNLESS(0 <= cur_entry_phys_offset &&
|
||||||
|
cur_entry_phys_offset <= cur_data_storage_size,
|
||||||
|
ResultIndirectStorageCorrupted);
|
||||||
|
R_UNLESS(cur_entry_phys_offset + data_offset + cur_size <= cur_data_storage_size,
|
||||||
|
ResultIndirectStorageCorrupted);
|
||||||
|
}
|
||||||
|
|
||||||
|
R_TRY(func(m_data_storage[cur_entry.storage_index], cur_entry_phys_offset + data_offset,
|
||||||
|
cur_offset, cur_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
cur_offset += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
30
core/fs/fssystem/fssystem_integrity_romfs_storage.cpp
Normal file
30
core/fs/fssystem/fssystem_integrity_romfs_storage.cpp
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_integrity_romfs_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
Result IntegrityRomFsStorage::Initialize(
|
||||||
|
HierarchicalIntegrityVerificationInformation level_hash_info, Hash master_hash,
|
||||||
|
HierarchicalIntegrityVerificationStorage::HierarchicalStorageInformation storage_info,
|
||||||
|
int max_data_cache_entries, int max_hash_cache_entries, s8 buffer_level) {
|
||||||
|
// Set master hash.
|
||||||
|
m_master_hash = master_hash;
|
||||||
|
m_master_hash_storage = std::make_shared<ArrayVfsFile<sizeof(Hash)>>(m_master_hash.value);
|
||||||
|
R_UNLESS(m_master_hash_storage != nullptr,
|
||||||
|
ResultAllocationMemoryFailedInIntegrityRomFsStorageA);
|
||||||
|
|
||||||
|
// Set the master hash storage.
|
||||||
|
storage_info[0] = m_master_hash_storage;
|
||||||
|
|
||||||
|
// Initialize our integrity storage.
|
||||||
|
R_RETURN(m_integrity_storage.Initialize(level_hash_info, storage_info, max_data_cache_entries,
|
||||||
|
max_hash_cache_entries, buffer_level));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrityRomFsStorage::Finalize() {
|
||||||
|
m_integrity_storage.Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
42
core/fs/fssystem/fssystem_integrity_romfs_storage.h
Normal file
42
core/fs/fssystem/fssystem_integrity_romfs_storage.h
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr inline size_t IntegrityLayerCountRomFs = 7;
|
||||||
|
constexpr inline size_t IntegrityHashLayerBlockSize = 16_KiB;
|
||||||
|
|
||||||
|
class IntegrityRomFsStorage : public IReadOnlyStorage {
|
||||||
|
public:
|
||||||
|
IntegrityRomFsStorage() {}
|
||||||
|
virtual ~IntegrityRomFsStorage() override {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(
|
||||||
|
HierarchicalIntegrityVerificationInformation level_hash_info, Hash master_hash,
|
||||||
|
HierarchicalIntegrityVerificationStorage::HierarchicalStorageInformation storage_info,
|
||||||
|
int max_data_cache_entries, int max_hash_cache_entries, s8 buffer_level);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
return m_integrity_storage.Read(buffer, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return m_integrity_storage.GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
HierarchicalIntegrityVerificationStorage m_integrity_storage;
|
||||||
|
Hash m_master_hash;
|
||||||
|
std::shared_ptr<ArrayVfsFile<sizeof(Hash)>> m_master_hash_storage;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
91
core/fs/fssystem/fssystem_integrity_verification_storage.cpp
Normal file
91
core/fs/fssystem/fssystem_integrity_verification_storage.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_integrity_verification_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr inline u32 ILog2(u32 val) {
|
||||||
|
ASSERT(val > 0);
|
||||||
|
return static_cast<u32>((sizeof(u32) * 8) - 1 - std::countl_zero<u32>(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrityVerificationStorage::Initialize(VirtualFile hs, VirtualFile ds, s64 verif_block_size,
|
||||||
|
s64 upper_layer_verif_block_size, bool is_real_data) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(verif_block_size >= HashSize);
|
||||||
|
|
||||||
|
// Set storages.
|
||||||
|
m_hash_storage = hs;
|
||||||
|
m_data_storage = ds;
|
||||||
|
|
||||||
|
// Set verification block sizes.
|
||||||
|
m_verification_block_size = verif_block_size;
|
||||||
|
m_verification_block_order = ILog2(static_cast<u32>(verif_block_size));
|
||||||
|
ASSERT(m_verification_block_size == 1ll << m_verification_block_order);
|
||||||
|
|
||||||
|
// Set upper layer block sizes.
|
||||||
|
upper_layer_verif_block_size = std::max(upper_layer_verif_block_size, HashSize);
|
||||||
|
m_upper_layer_verification_block_size = upper_layer_verif_block_size;
|
||||||
|
m_upper_layer_verification_block_order = ILog2(static_cast<u32>(upper_layer_verif_block_size));
|
||||||
|
ASSERT(m_upper_layer_verification_block_size == 1ll << m_upper_layer_verification_block_order);
|
||||||
|
|
||||||
|
// Validate sizes.
|
||||||
|
{
|
||||||
|
s64 hash_size = m_hash_storage->GetSize();
|
||||||
|
s64 data_size = m_data_storage->GetSize();
|
||||||
|
ASSERT(((hash_size / HashSize) * m_verification_block_size) >= data_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set data.
|
||||||
|
m_is_real_data = is_real_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrityVerificationStorage::Finalize() {
|
||||||
|
m_hash_storage = VirtualFile();
|
||||||
|
m_data_storage = VirtualFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t IntegrityVerificationStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Succeed if zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate arguments.
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Validate the offset.
|
||||||
|
s64 data_size = m_data_storage->GetSize();
|
||||||
|
ASSERT(offset <= static_cast<size_t>(data_size));
|
||||||
|
|
||||||
|
// Validate the access range.
|
||||||
|
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(
|
||||||
|
offset, size, Common::AlignUp(data_size, static_cast<size_t>(m_verification_block_size)))));
|
||||||
|
|
||||||
|
// Determine the read extents.
|
||||||
|
size_t read_size = size;
|
||||||
|
if (static_cast<s64>(offset + read_size) > data_size) {
|
||||||
|
// Determine the padding sizes.
|
||||||
|
s64 padding_offset = data_size - offset;
|
||||||
|
size_t padding_size = static_cast<size_t>(
|
||||||
|
m_verification_block_size - (padding_offset & (m_verification_block_size - 1)));
|
||||||
|
ASSERT(static_cast<s64>(padding_size) < m_verification_block_size);
|
||||||
|
|
||||||
|
// Clear the padding.
|
||||||
|
std::memset(static_cast<u8*>(buffer) + padding_offset, 0, padding_size);
|
||||||
|
|
||||||
|
// Set the new in-bounds size.
|
||||||
|
read_size = static_cast<size_t>(data_size - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the read.
|
||||||
|
return m_data_storage->Read(buffer, read_size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t IntegrityVerificationStorage::GetSize() const {
|
||||||
|
return m_data_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
65
core/fs/fssystem/fssystem_integrity_verification_storage.h
Normal file
65
core/fs/fssystem/fssystem_integrity_verification_storage.h
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class IntegrityVerificationStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(IntegrityVerificationStorage);
|
||||||
|
YUZU_NON_MOVEABLE(IntegrityVerificationStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr s64 HashSize = 256 / 8;
|
||||||
|
|
||||||
|
struct BlockHash {
|
||||||
|
std::array<u8, HashSize> hash;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<BlockHash>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
IntegrityVerificationStorage()
|
||||||
|
: m_verification_block_size(0), m_verification_block_order(0),
|
||||||
|
m_upper_layer_verification_block_size(0), m_upper_layer_verification_block_order(0) {}
|
||||||
|
virtual ~IntegrityVerificationStorage() override {
|
||||||
|
this->Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Initialize(VirtualFile hs, VirtualFile ds, s64 verif_block_size,
|
||||||
|
s64 upper_layer_verif_block_size, bool is_real_data);
|
||||||
|
void Finalize();
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
virtual size_t GetSize() const override;
|
||||||
|
|
||||||
|
s64 GetBlockSize() const {
|
||||||
|
return m_verification_block_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void SetValidationBit(BlockHash* hash) {
|
||||||
|
ASSERT(hash != nullptr);
|
||||||
|
hash->hash[HashSize - 1] |= 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsValidationBit(const BlockHash* hash) {
|
||||||
|
ASSERT(hash != nullptr);
|
||||||
|
return (hash->hash[HashSize - 1] & 0x80) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_hash_storage;
|
||||||
|
VirtualFile m_data_storage;
|
||||||
|
s64 m_verification_block_size;
|
||||||
|
s64 m_verification_block_order;
|
||||||
|
s64 m_upper_layer_verification_block_size;
|
||||||
|
s64 m_upper_layer_verification_block_order;
|
||||||
|
bool m_is_real_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class MemoryResourceBufferHoldStorage : public IStorage {
|
||||||
|
YUZU_NON_COPYABLE(MemoryResourceBufferHoldStorage);
|
||||||
|
YUZU_NON_MOVEABLE(MemoryResourceBufferHoldStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
MemoryResourceBufferHoldStorage(VirtualFile storage, size_t buffer_size)
|
||||||
|
: m_storage(std::move(storage)), m_buffer(::operator new(buffer_size)),
|
||||||
|
m_buffer_size(buffer_size) {}
|
||||||
|
|
||||||
|
virtual ~MemoryResourceBufferHoldStorage() {
|
||||||
|
// If we have a buffer, deallocate it.
|
||||||
|
if (m_buffer != nullptr) {
|
||||||
|
::operator delete(m_buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValid() const {
|
||||||
|
return m_buffer != nullptr;
|
||||||
|
}
|
||||||
|
void* GetBuffer() const {
|
||||||
|
return m_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(m_storage != nullptr);
|
||||||
|
|
||||||
|
return m_storage->Read(buffer, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(m_storage != nullptr);
|
||||||
|
|
||||||
|
return m_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||||
|
// Check pre-conditions.
|
||||||
|
ASSERT(m_storage != nullptr);
|
||||||
|
|
||||||
|
return m_storage->Write(buffer, size, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_storage;
|
||||||
|
void* m_buffer;
|
||||||
|
size_t m_buffer_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
1351
core/fs/fssystem/fssystem_nca_file_system_driver.cpp
Normal file
1351
core/fs/fssystem/fssystem_nca_file_system_driver.cpp
Normal file
File diff suppressed because it is too large
Load diff
364
core/fs/fssystem/fssystem_nca_file_system_driver.h
Normal file
364
core/fs/fssystem/fssystem_nca_file_system_driver.h
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_compression_common.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class CompressedStorage;
|
||||||
|
class AesCtrCounterExtendedStorage;
|
||||||
|
class IndirectStorage;
|
||||||
|
class SparseStorage;
|
||||||
|
|
||||||
|
struct NcaCryptoConfiguration;
|
||||||
|
|
||||||
|
using KeyGenerationFunction = void (*)(void* dst_key, size_t dst_key_size, const void* src_key,
|
||||||
|
size_t src_key_size, s32 key_type);
|
||||||
|
using VerifySign1Function = bool (*)(const void* sig, size_t sig_size, const void* data,
|
||||||
|
size_t data_size, u8 generation);
|
||||||
|
|
||||||
|
struct NcaCryptoConfiguration {
|
||||||
|
static constexpr size_t Rsa2048KeyModulusSize = 2048 / 8;
|
||||||
|
static constexpr size_t Rsa2048KeyPublicExponentSize = 3;
|
||||||
|
static constexpr size_t Rsa2048KeyPrivateExponentSize = Rsa2048KeyModulusSize;
|
||||||
|
|
||||||
|
static constexpr size_t Aes128KeySize = 128 / 8;
|
||||||
|
|
||||||
|
static constexpr size_t Header1SignatureKeyGenerationMax = 1;
|
||||||
|
|
||||||
|
static constexpr s32 KeyAreaEncryptionKeyIndexCount = 3;
|
||||||
|
static constexpr s32 HeaderEncryptionKeyCount = 2;
|
||||||
|
|
||||||
|
static constexpr u8 KeyAreaEncryptionKeyIndexZeroKey = 0xFF;
|
||||||
|
|
||||||
|
static constexpr size_t KeyGenerationMax = 32;
|
||||||
|
|
||||||
|
std::array<const u8*, Header1SignatureKeyGenerationMax + 1> header_1_sign_key_moduli;
|
||||||
|
std::array<u8, Rsa2048KeyPublicExponentSize> header_1_sign_key_public_exponent;
|
||||||
|
std::array<std::array<u8, Aes128KeySize>, KeyAreaEncryptionKeyIndexCount>
|
||||||
|
key_area_encryption_key_source;
|
||||||
|
std::array<u8, Aes128KeySize> header_encryption_key_source;
|
||||||
|
std::array<std::array<u8, Aes128KeySize>, HeaderEncryptionKeyCount>
|
||||||
|
header_encrypted_encryption_keys;
|
||||||
|
KeyGenerationFunction generate_key;
|
||||||
|
VerifySign1Function verify_sign1;
|
||||||
|
bool is_plaintext_header_available;
|
||||||
|
bool is_available_sw_key;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaCryptoConfiguration>);
|
||||||
|
|
||||||
|
struct NcaCompressionConfiguration {
|
||||||
|
GetDecompressorFunction get_decompressor;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaCompressionConfiguration>);
|
||||||
|
|
||||||
|
constexpr inline s32 KeyAreaEncryptionKeyCount =
|
||||||
|
NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount *
|
||||||
|
NcaCryptoConfiguration::KeyGenerationMax;
|
||||||
|
|
||||||
|
enum class KeyType : s32 {
|
||||||
|
ZeroKey = -2,
|
||||||
|
InvalidKey = -1,
|
||||||
|
NcaHeaderKey1 = KeyAreaEncryptionKeyCount + 0,
|
||||||
|
NcaHeaderKey2 = KeyAreaEncryptionKeyCount + 1,
|
||||||
|
NcaExternalKey = KeyAreaEncryptionKeyCount + 2,
|
||||||
|
SaveDataDeviceUniqueMac = KeyAreaEncryptionKeyCount + 3,
|
||||||
|
SaveDataSeedUniqueMac = KeyAreaEncryptionKeyCount + 4,
|
||||||
|
SaveDataTransferMac = KeyAreaEncryptionKeyCount + 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr inline bool IsInvalidKeyTypeValue(s32 key_type) {
|
||||||
|
return key_type < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr inline s32 GetKeyTypeValue(u8 key_index, u8 key_generation) {
|
||||||
|
if (key_index == NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexZeroKey) {
|
||||||
|
return static_cast<s32>(KeyType::ZeroKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_index >= NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount) {
|
||||||
|
return static_cast<s32>(KeyType::InvalidKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount * key_generation + key_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NcaReader {
|
||||||
|
YUZU_NON_COPYABLE(NcaReader);
|
||||||
|
YUZU_NON_MOVEABLE(NcaReader);
|
||||||
|
|
||||||
|
public:
|
||||||
|
NcaReader();
|
||||||
|
~NcaReader();
|
||||||
|
|
||||||
|
Result Initialize(VirtualFile base_storage, const NcaCryptoConfiguration& crypto_cfg,
|
||||||
|
const NcaCompressionConfiguration& compression_cfg);
|
||||||
|
|
||||||
|
VirtualFile GetSharedBodyStorage();
|
||||||
|
u32 GetMagic() const;
|
||||||
|
NcaHeader::DistributionType GetDistributionType() const;
|
||||||
|
NcaHeader::ContentType GetContentType() const;
|
||||||
|
u8 GetHeaderSign1KeyGeneration() const;
|
||||||
|
u8 GetKeyGeneration() const;
|
||||||
|
u8 GetKeyIndex() const;
|
||||||
|
u64 GetContentSize() const;
|
||||||
|
u64 GetProgramId() const;
|
||||||
|
u32 GetContentIndex() const;
|
||||||
|
u32 GetSdkAddonVersion() const;
|
||||||
|
void GetRightsId(u8* dst, size_t dst_size) const;
|
||||||
|
bool HasFsInfo(s32 index) const;
|
||||||
|
s32 GetFsCount() const;
|
||||||
|
const Hash& GetFsHeaderHash(s32 index) const;
|
||||||
|
void GetFsHeaderHash(Hash* dst, s32 index) const;
|
||||||
|
void GetFsInfo(NcaHeader::FsInfo* dst, s32 index) const;
|
||||||
|
u64 GetFsOffset(s32 index) const;
|
||||||
|
u64 GetFsEndOffset(s32 index) const;
|
||||||
|
u64 GetFsSize(s32 index) const;
|
||||||
|
void GetEncryptedKey(void* dst, size_t size) const;
|
||||||
|
const void* GetDecryptionKey(s32 index) const;
|
||||||
|
bool HasValidInternalKey() const;
|
||||||
|
bool HasInternalDecryptionKeyForAesHw() const;
|
||||||
|
bool IsSoftwareAesPrioritized() const;
|
||||||
|
void PrioritizeSoftwareAes();
|
||||||
|
bool IsAvailableSwKey() const;
|
||||||
|
bool HasExternalDecryptionKey() const;
|
||||||
|
const void* GetExternalDecryptionKey() const;
|
||||||
|
void SetExternalDecryptionKey(const void* src, size_t size);
|
||||||
|
void GetRawData(void* dst, size_t dst_size) const;
|
||||||
|
NcaHeader::EncryptionType GetEncryptionType() const;
|
||||||
|
Result ReadHeader(NcaFsHeader* dst, s32 index) const;
|
||||||
|
|
||||||
|
GetDecompressorFunction GetDecompressor() const;
|
||||||
|
|
||||||
|
bool GetHeaderSign1Valid() const;
|
||||||
|
|
||||||
|
void GetHeaderSign2(void* dst, size_t size) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NcaHeader m_header;
|
||||||
|
std::array<std::array<u8, NcaCryptoConfiguration::Aes128KeySize>,
|
||||||
|
NcaHeader::DecryptionKey_Count>
|
||||||
|
m_decryption_keys;
|
||||||
|
VirtualFile m_body_storage;
|
||||||
|
VirtualFile m_header_storage;
|
||||||
|
std::array<u8, NcaCryptoConfiguration::Aes128KeySize> m_external_decryption_key;
|
||||||
|
bool m_is_software_aes_prioritized;
|
||||||
|
bool m_is_available_sw_key;
|
||||||
|
NcaHeader::EncryptionType m_header_encryption_type;
|
||||||
|
bool m_is_header_sign1_signature_valid;
|
||||||
|
GetDecompressorFunction m_get_decompressor;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NcaFsHeaderReader {
|
||||||
|
YUZU_NON_COPYABLE(NcaFsHeaderReader);
|
||||||
|
YUZU_NON_MOVEABLE(NcaFsHeaderReader);
|
||||||
|
|
||||||
|
public:
|
||||||
|
NcaFsHeaderReader() : m_fs_index(-1) {
|
||||||
|
std::memset(std::addressof(m_data), 0, sizeof(m_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Initialize(const NcaReader& reader, s32 index);
|
||||||
|
bool IsInitialized() const {
|
||||||
|
return m_fs_index >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetRawData(void* dst, size_t dst_size) const;
|
||||||
|
|
||||||
|
NcaFsHeader::HashData& GetHashData();
|
||||||
|
const NcaFsHeader::HashData& GetHashData() const;
|
||||||
|
u16 GetVersion() const;
|
||||||
|
s32 GetFsIndex() const;
|
||||||
|
NcaFsHeader::FsType GetFsType() const;
|
||||||
|
NcaFsHeader::HashType GetHashType() const;
|
||||||
|
NcaFsHeader::EncryptionType GetEncryptionType() const;
|
||||||
|
NcaPatchInfo& GetPatchInfo();
|
||||||
|
const NcaPatchInfo& GetPatchInfo() const;
|
||||||
|
const NcaAesCtrUpperIv GetAesCtrUpperIv() const;
|
||||||
|
|
||||||
|
bool IsSkipLayerHashEncryption() const;
|
||||||
|
Result GetHashTargetOffset(s64* out) const;
|
||||||
|
|
||||||
|
bool ExistsSparseLayer() const;
|
||||||
|
NcaSparseInfo& GetSparseInfo();
|
||||||
|
const NcaSparseInfo& GetSparseInfo() const;
|
||||||
|
|
||||||
|
bool ExistsCompressionLayer() const;
|
||||||
|
NcaCompressionInfo& GetCompressionInfo();
|
||||||
|
const NcaCompressionInfo& GetCompressionInfo() const;
|
||||||
|
|
||||||
|
bool ExistsPatchMetaHashLayer() const;
|
||||||
|
NcaMetaDataHashDataInfo& GetPatchMetaDataHashDataInfo();
|
||||||
|
const NcaMetaDataHashDataInfo& GetPatchMetaDataHashDataInfo() const;
|
||||||
|
NcaFsHeader::MetaDataHashType GetPatchMetaHashType() const;
|
||||||
|
|
||||||
|
bool ExistsSparseMetaHashLayer() const;
|
||||||
|
NcaMetaDataHashDataInfo& GetSparseMetaDataHashDataInfo();
|
||||||
|
const NcaMetaDataHashDataInfo& GetSparseMetaDataHashDataInfo() const;
|
||||||
|
NcaFsHeader::MetaDataHashType GetSparseMetaHashType() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NcaFsHeader m_data;
|
||||||
|
s32 m_fs_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NcaFileSystemDriver {
|
||||||
|
YUZU_NON_COPYABLE(NcaFileSystemDriver);
|
||||||
|
YUZU_NON_MOVEABLE(NcaFileSystemDriver);
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct StorageContext {
|
||||||
|
bool open_raw_storage;
|
||||||
|
VirtualFile body_substorage;
|
||||||
|
std::shared_ptr<SparseStorage> current_sparse_storage;
|
||||||
|
VirtualFile sparse_storage_meta_storage;
|
||||||
|
std::shared_ptr<SparseStorage> original_sparse_storage;
|
||||||
|
void* external_current_sparse_storage;
|
||||||
|
void* external_original_sparse_storage;
|
||||||
|
VirtualFile aes_ctr_ex_storage_meta_storage;
|
||||||
|
VirtualFile aes_ctr_ex_storage_data_storage;
|
||||||
|
std::shared_ptr<AesCtrCounterExtendedStorage> aes_ctr_ex_storage;
|
||||||
|
VirtualFile indirect_storage_meta_storage;
|
||||||
|
std::shared_ptr<IndirectStorage> indirect_storage;
|
||||||
|
VirtualFile fs_data_storage;
|
||||||
|
VirtualFile compressed_storage_meta_storage;
|
||||||
|
std::shared_ptr<CompressedStorage> compressed_storage;
|
||||||
|
|
||||||
|
VirtualFile patch_layer_info_storage;
|
||||||
|
VirtualFile sparse_layer_info_storage;
|
||||||
|
|
||||||
|
VirtualFile external_original_storage;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class AlignmentStorageRequirement {
|
||||||
|
CacheBlockSize = 0,
|
||||||
|
None = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
static Result SetupFsHeaderReader(NcaFsHeaderReader* out, const NcaReader& reader,
|
||||||
|
s32 fs_index);
|
||||||
|
|
||||||
|
public:
|
||||||
|
NcaFileSystemDriver(std::shared_ptr<NcaReader> reader) : m_original_reader(), m_reader(reader) {
|
||||||
|
ASSERT(m_reader != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFileSystemDriver(std::shared_ptr<NcaReader> original_reader,
|
||||||
|
std::shared_ptr<NcaReader> reader)
|
||||||
|
: m_original_reader(original_reader), m_reader(reader) {
|
||||||
|
ASSERT(m_reader != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result OpenStorageWithContext(VirtualFile* out, NcaFsHeaderReader* out_header_reader,
|
||||||
|
s32 fs_index, StorageContext* ctx);
|
||||||
|
|
||||||
|
Result OpenStorage(VirtualFile* out, NcaFsHeaderReader* out_header_reader, s32 fs_index) {
|
||||||
|
// Create a storage context.
|
||||||
|
StorageContext ctx{};
|
||||||
|
|
||||||
|
// Open the storage.
|
||||||
|
R_RETURN(OpenStorageWithContext(out, out_header_reader, fs_index, std::addressof(ctx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
Result CreateStorageByRawStorage(VirtualFile* out, const NcaFsHeaderReader* header_reader,
|
||||||
|
VirtualFile raw_storage, StorageContext* ctx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result OpenStorageImpl(VirtualFile* out, NcaFsHeaderReader* out_header_reader, s32 fs_index,
|
||||||
|
StorageContext* ctx);
|
||||||
|
|
||||||
|
Result OpenIndirectableStorageAsOriginal(VirtualFile* out,
|
||||||
|
const NcaFsHeaderReader* header_reader,
|
||||||
|
StorageContext* ctx);
|
||||||
|
|
||||||
|
Result CreateBodySubStorage(VirtualFile* out, s64 offset, s64 size);
|
||||||
|
|
||||||
|
Result CreateAesCtrStorage(VirtualFile* out, VirtualFile base_storage, s64 offset,
|
||||||
|
const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
AlignmentStorageRequirement alignment_storage_requirement);
|
||||||
|
Result CreateAesXtsStorage(VirtualFile* out, VirtualFile base_storage, s64 offset);
|
||||||
|
|
||||||
|
Result CreateSparseStorageMetaStorage(VirtualFile* out, VirtualFile base_storage, s64 offset,
|
||||||
|
const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
const NcaSparseInfo& sparse_info);
|
||||||
|
Result CreateSparseStorageCore(std::shared_ptr<SparseStorage>* out, VirtualFile base_storage,
|
||||||
|
s64 base_size, VirtualFile meta_storage,
|
||||||
|
const NcaSparseInfo& sparse_info, bool external_info);
|
||||||
|
Result CreateSparseStorage(VirtualFile* out, s64* out_fs_data_offset,
|
||||||
|
std::shared_ptr<SparseStorage>* out_sparse_storage,
|
||||||
|
VirtualFile* out_meta_storage, s32 index,
|
||||||
|
const NcaAesCtrUpperIv& upper_iv, const NcaSparseInfo& sparse_info);
|
||||||
|
|
||||||
|
Result CreateSparseStorageMetaStorageWithVerification(
|
||||||
|
VirtualFile* out, VirtualFile* out_verification, VirtualFile base_storage, s64 offset,
|
||||||
|
const NcaAesCtrUpperIv& upper_iv, const NcaSparseInfo& sparse_info,
|
||||||
|
const NcaMetaDataHashDataInfo& meta_data_hash_data_info);
|
||||||
|
Result CreateSparseStorageWithVerification(
|
||||||
|
VirtualFile* out, s64* out_fs_data_offset,
|
||||||
|
std::shared_ptr<SparseStorage>* out_sparse_storage, VirtualFile* out_meta_storage,
|
||||||
|
VirtualFile* out_verification, s32 index, const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
const NcaSparseInfo& sparse_info, const NcaMetaDataHashDataInfo& meta_data_hash_data_info,
|
||||||
|
NcaFsHeader::MetaDataHashType meta_data_hash_type);
|
||||||
|
|
||||||
|
Result CreateAesCtrExStorageMetaStorage(VirtualFile* out, VirtualFile base_storage, s64 offset,
|
||||||
|
NcaFsHeader::EncryptionType encryption_type,
|
||||||
|
const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
const NcaPatchInfo& patch_info);
|
||||||
|
Result CreateAesCtrExStorage(VirtualFile* out,
|
||||||
|
std::shared_ptr<AesCtrCounterExtendedStorage>* out_ext,
|
||||||
|
VirtualFile base_storage, VirtualFile meta_storage,
|
||||||
|
s64 counter_offset, const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
const NcaPatchInfo& patch_info);
|
||||||
|
|
||||||
|
Result CreateIndirectStorageMetaStorage(VirtualFile* out, VirtualFile base_storage,
|
||||||
|
const NcaPatchInfo& patch_info);
|
||||||
|
Result CreateIndirectStorage(VirtualFile* out, std::shared_ptr<IndirectStorage>* out_ind,
|
||||||
|
VirtualFile base_storage, VirtualFile original_data_storage,
|
||||||
|
VirtualFile meta_storage, const NcaPatchInfo& patch_info);
|
||||||
|
|
||||||
|
Result CreatePatchMetaStorage(VirtualFile* out_aes_ctr_ex_meta, VirtualFile* out_indirect_meta,
|
||||||
|
VirtualFile* out_verification, VirtualFile base_storage,
|
||||||
|
s64 offset, const NcaAesCtrUpperIv& upper_iv,
|
||||||
|
const NcaPatchInfo& patch_info,
|
||||||
|
const NcaMetaDataHashDataInfo& meta_data_hash_data_info);
|
||||||
|
|
||||||
|
Result CreateSha256Storage(VirtualFile* out, VirtualFile base_storage,
|
||||||
|
const NcaFsHeader::HashData::HierarchicalSha256Data& sha256_data);
|
||||||
|
|
||||||
|
Result CreateIntegrityVerificationStorage(
|
||||||
|
VirtualFile* out, VirtualFile base_storage,
|
||||||
|
const NcaFsHeader::HashData::IntegrityMetaInfo& meta_info);
|
||||||
|
Result CreateIntegrityVerificationStorageForMeta(
|
||||||
|
VirtualFile* out, VirtualFile* out_verification, VirtualFile base_storage, s64 offset,
|
||||||
|
const NcaMetaDataHashDataInfo& meta_data_hash_data_info);
|
||||||
|
Result CreateIntegrityVerificationStorageImpl(
|
||||||
|
VirtualFile* out, VirtualFile base_storage,
|
||||||
|
const NcaFsHeader::HashData::IntegrityMetaInfo& meta_info, s64 layer_info_offset,
|
||||||
|
int max_data_cache_entries, int max_hash_cache_entries, s8 buffer_level);
|
||||||
|
|
||||||
|
Result CreateRegionSwitchStorage(VirtualFile* out, const NcaFsHeaderReader* header_reader,
|
||||||
|
VirtualFile inside_storage, VirtualFile outside_storage);
|
||||||
|
|
||||||
|
Result CreateCompressedStorage(VirtualFile* out, std::shared_ptr<CompressedStorage>* out_cmp,
|
||||||
|
VirtualFile* out_meta, VirtualFile base_storage,
|
||||||
|
const NcaCompressionInfo& compression_info);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Result CreateCompressedStorage(VirtualFile* out, std::shared_ptr<CompressedStorage>* out_cmp,
|
||||||
|
VirtualFile* out_meta, VirtualFile base_storage,
|
||||||
|
const NcaCompressionInfo& compression_info,
|
||||||
|
GetDecompressorFunction get_decompressor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<NcaReader> m_original_reader;
|
||||||
|
std::shared_ptr<NcaReader> m_reader;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
20
core/fs/fssystem/fssystem_nca_header.cpp
Normal file
20
core/fs/fssystem/fssystem_nca_header.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
u8 NcaHeader::GetProperKeyGeneration() const {
|
||||||
|
return std::max(this->key_generation, this->key_generation_2);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaPatchInfo::HasIndirectTable() const {
|
||||||
|
return this->indirect_size != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaPatchInfo::HasAesCtrExTable() const {
|
||||||
|
return this->aes_ctr_ex_size != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
338
core/fs/fssystem/fssystem_nca_header.h
Normal file
338
core/fs/fssystem/fssystem_nca_header.h
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/literals.h"
|
||||||
|
|
||||||
|
#include "core/file_sys/errors.h"
|
||||||
|
#include "core/file_sys/fssystem/fs_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
|
||||||
|
struct Hash {
|
||||||
|
static constexpr std::size_t Size = 256 / 8;
|
||||||
|
std::array<u8, Size> value;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Hash) == Hash::Size);
|
||||||
|
static_assert(std::is_trivial_v<Hash>);
|
||||||
|
|
||||||
|
using NcaDigest = Hash;
|
||||||
|
|
||||||
|
struct NcaHeader {
|
||||||
|
enum class ContentType : u8 {
|
||||||
|
Program = 0,
|
||||||
|
Meta = 1,
|
||||||
|
Control = 2,
|
||||||
|
Manual = 3,
|
||||||
|
Data = 4,
|
||||||
|
PublicData = 5,
|
||||||
|
|
||||||
|
Start = Program,
|
||||||
|
End = PublicData,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DistributionType : u8 {
|
||||||
|
Download = 0,
|
||||||
|
GameCard = 1,
|
||||||
|
|
||||||
|
Start = Download,
|
||||||
|
End = GameCard,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class EncryptionType : u8 {
|
||||||
|
Auto = 0,
|
||||||
|
None = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DecryptionKey {
|
||||||
|
DecryptionKey_AesXts = 0,
|
||||||
|
DecryptionKey_AesXts1 = DecryptionKey_AesXts,
|
||||||
|
DecryptionKey_AesXts2 = 1,
|
||||||
|
DecryptionKey_AesCtr = 2,
|
||||||
|
DecryptionKey_AesCtrEx = 3,
|
||||||
|
DecryptionKey_AesCtrHw = 4,
|
||||||
|
DecryptionKey_Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FsInfo {
|
||||||
|
u32 start_sector;
|
||||||
|
u32 end_sector;
|
||||||
|
u32 hash_sectors;
|
||||||
|
u32 reserved;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(FsInfo) == 0x10);
|
||||||
|
static_assert(std::is_trivial_v<FsInfo>);
|
||||||
|
|
||||||
|
static constexpr u32 Magic0 = Common::MakeMagic('N', 'C', 'A', '0');
|
||||||
|
static constexpr u32 Magic1 = Common::MakeMagic('N', 'C', 'A', '1');
|
||||||
|
static constexpr u32 Magic2 = Common::MakeMagic('N', 'C', 'A', '2');
|
||||||
|
static constexpr u32 Magic3 = Common::MakeMagic('N', 'C', 'A', '3');
|
||||||
|
|
||||||
|
static constexpr u32 Magic = Magic3;
|
||||||
|
|
||||||
|
static constexpr std::size_t Size = 1_KiB;
|
||||||
|
static constexpr s32 FsCountMax = 4;
|
||||||
|
static constexpr std::size_t HeaderSignCount = 2;
|
||||||
|
static constexpr std::size_t HeaderSignSize = 0x100;
|
||||||
|
static constexpr std::size_t EncryptedKeyAreaSize = 0x100;
|
||||||
|
static constexpr std::size_t SectorSize = 0x200;
|
||||||
|
static constexpr std::size_t SectorShift = 9;
|
||||||
|
static constexpr std::size_t RightsIdSize = 0x10;
|
||||||
|
static constexpr std::size_t XtsBlockSize = 0x200;
|
||||||
|
static constexpr std::size_t CtrBlockSize = 0x10;
|
||||||
|
|
||||||
|
static_assert(SectorSize == (1 << SectorShift));
|
||||||
|
|
||||||
|
// Data members.
|
||||||
|
std::array<u8, HeaderSignSize> header_sign_1;
|
||||||
|
std::array<u8, HeaderSignSize> header_sign_2;
|
||||||
|
u32 magic;
|
||||||
|
DistributionType distribution_type;
|
||||||
|
ContentType content_type;
|
||||||
|
u8 key_generation;
|
||||||
|
u8 key_index;
|
||||||
|
u64 content_size;
|
||||||
|
u64 program_id;
|
||||||
|
u32 content_index;
|
||||||
|
u32 sdk_addon_version;
|
||||||
|
u8 key_generation_2;
|
||||||
|
u8 header1_signature_key_generation;
|
||||||
|
std::array<u8, 2> reserved_222;
|
||||||
|
std::array<u32, 3> reserved_224;
|
||||||
|
std::array<u8, RightsIdSize> rights_id;
|
||||||
|
std::array<FsInfo, FsCountMax> fs_info;
|
||||||
|
std::array<Hash, FsCountMax> fs_header_hash;
|
||||||
|
std::array<u8, EncryptedKeyAreaSize> encrypted_key_area;
|
||||||
|
|
||||||
|
static constexpr u64 SectorToByte(u32 sector) {
|
||||||
|
return static_cast<u64>(sector) << SectorShift;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr u32 ByteToSector(u64 byte) {
|
||||||
|
return static_cast<u32>(byte >> SectorShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 GetProperKeyGeneration() const;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NcaHeader) == NcaHeader::Size);
|
||||||
|
static_assert(std::is_trivial_v<NcaHeader>);
|
||||||
|
|
||||||
|
struct NcaBucketInfo {
|
||||||
|
static constexpr size_t HeaderSize = 0x10;
|
||||||
|
Int64 offset;
|
||||||
|
Int64 size;
|
||||||
|
std::array<u8, HeaderSize> header;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaBucketInfo>);
|
||||||
|
|
||||||
|
struct NcaPatchInfo {
|
||||||
|
static constexpr size_t Size = 0x40;
|
||||||
|
static constexpr size_t Offset = 0x100;
|
||||||
|
|
||||||
|
Int64 indirect_offset;
|
||||||
|
Int64 indirect_size;
|
||||||
|
std::array<u8, NcaBucketInfo::HeaderSize> indirect_header;
|
||||||
|
Int64 aes_ctr_ex_offset;
|
||||||
|
Int64 aes_ctr_ex_size;
|
||||||
|
std::array<u8, NcaBucketInfo::HeaderSize> aes_ctr_ex_header;
|
||||||
|
|
||||||
|
bool HasIndirectTable() const;
|
||||||
|
bool HasAesCtrExTable() const;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaPatchInfo>);
|
||||||
|
|
||||||
|
union NcaAesCtrUpperIv {
|
||||||
|
u64 value;
|
||||||
|
struct {
|
||||||
|
u32 generation;
|
||||||
|
u32 secure_value;
|
||||||
|
} part;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaAesCtrUpperIv>);
|
||||||
|
|
||||||
|
struct NcaSparseInfo {
|
||||||
|
NcaBucketInfo bucket;
|
||||||
|
Int64 physical_offset;
|
||||||
|
u16 generation;
|
||||||
|
std::array<u8, 6> reserved;
|
||||||
|
|
||||||
|
s64 GetPhysicalSize() const {
|
||||||
|
return this->bucket.offset + this->bucket.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetGeneration() const {
|
||||||
|
return static_cast<u32>(this->generation) << 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaAesCtrUpperIv MakeAesCtrUpperIv(NcaAesCtrUpperIv upper_iv) const {
|
||||||
|
NcaAesCtrUpperIv sparse_upper_iv = upper_iv;
|
||||||
|
sparse_upper_iv.part.generation = this->GetGeneration();
|
||||||
|
return sparse_upper_iv;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaSparseInfo>);
|
||||||
|
|
||||||
|
struct NcaCompressionInfo {
|
||||||
|
NcaBucketInfo bucket;
|
||||||
|
std::array<u8, 8> resreved;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaCompressionInfo>);
|
||||||
|
|
||||||
|
struct NcaMetaDataHashDataInfo {
|
||||||
|
Int64 offset;
|
||||||
|
Int64 size;
|
||||||
|
Hash hash;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<NcaMetaDataHashDataInfo>);
|
||||||
|
|
||||||
|
struct NcaFsHeader {
|
||||||
|
static constexpr size_t Size = 0x200;
|
||||||
|
static constexpr size_t HashDataOffset = 0x8;
|
||||||
|
|
||||||
|
struct Region {
|
||||||
|
Int64 offset;
|
||||||
|
Int64 size;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivial_v<Region>);
|
||||||
|
|
||||||
|
enum class FsType : u8 {
|
||||||
|
RomFs = 0,
|
||||||
|
PartitionFs = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class EncryptionType : u8 {
|
||||||
|
Auto = 0,
|
||||||
|
None = 1,
|
||||||
|
AesXts = 2,
|
||||||
|
AesCtr = 3,
|
||||||
|
AesCtrEx = 4,
|
||||||
|
AesCtrSkipLayerHash = 5,
|
||||||
|
AesCtrExSkipLayerHash = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HashType : u8 {
|
||||||
|
Auto = 0,
|
||||||
|
None = 1,
|
||||||
|
HierarchicalSha256Hash = 2,
|
||||||
|
HierarchicalIntegrityHash = 3,
|
||||||
|
AutoSha3 = 4,
|
||||||
|
HierarchicalSha3256Hash = 5,
|
||||||
|
HierarchicalIntegritySha3Hash = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MetaDataHashType : u8 {
|
||||||
|
None = 0,
|
||||||
|
HierarchicalIntegrity = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
union HashData {
|
||||||
|
struct HierarchicalSha256Data {
|
||||||
|
static constexpr size_t HashLayerCountMax = 5;
|
||||||
|
static const size_t MasterHashOffset;
|
||||||
|
|
||||||
|
Hash fs_data_master_hash;
|
||||||
|
s32 hash_block_size;
|
||||||
|
s32 hash_layer_count;
|
||||||
|
std::array<Region, HashLayerCountMax> hash_layer_region;
|
||||||
|
} hierarchical_sha256_data;
|
||||||
|
static_assert(std::is_trivial_v<HierarchicalSha256Data>);
|
||||||
|
|
||||||
|
struct IntegrityMetaInfo {
|
||||||
|
static const size_t MasterHashOffset;
|
||||||
|
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
u32 master_hash_size;
|
||||||
|
|
||||||
|
struct LevelHashInfo {
|
||||||
|
u32 max_layers;
|
||||||
|
|
||||||
|
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||||
|
static constexpr size_t IntegrityMaxLayerCount = 7;
|
||||||
|
Int64 offset;
|
||||||
|
Int64 size;
|
||||||
|
s32 block_order;
|
||||||
|
std::array<u8, 4> reserved;
|
||||||
|
};
|
||||||
|
std::array<
|
||||||
|
HierarchicalIntegrityVerificationLevelInformation,
|
||||||
|
HierarchicalIntegrityVerificationLevelInformation::IntegrityMaxLayerCount - 1>
|
||||||
|
info;
|
||||||
|
|
||||||
|
struct SignatureSalt {
|
||||||
|
static constexpr size_t Size = 0x20;
|
||||||
|
std::array<u8, Size> value;
|
||||||
|
};
|
||||||
|
SignatureSalt seed;
|
||||||
|
} level_hash_info;
|
||||||
|
|
||||||
|
Hash master_hash;
|
||||||
|
} integrity_meta_info;
|
||||||
|
static_assert(std::is_trivial_v<IntegrityMetaInfo>);
|
||||||
|
|
||||||
|
std::array<u8, NcaPatchInfo::Offset - HashDataOffset> padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
u16 version;
|
||||||
|
FsType fs_type;
|
||||||
|
HashType hash_type;
|
||||||
|
EncryptionType encryption_type;
|
||||||
|
MetaDataHashType meta_data_hash_type;
|
||||||
|
std::array<u8, 2> reserved;
|
||||||
|
HashData hash_data;
|
||||||
|
NcaPatchInfo patch_info;
|
||||||
|
NcaAesCtrUpperIv aes_ctr_upper_iv;
|
||||||
|
NcaSparseInfo sparse_info;
|
||||||
|
NcaCompressionInfo compression_info;
|
||||||
|
NcaMetaDataHashDataInfo meta_data_hash_data_info;
|
||||||
|
std::array<u8, 0x30> pad;
|
||||||
|
|
||||||
|
bool IsSkipLayerHashEncryption() const {
|
||||||
|
return this->encryption_type == EncryptionType::AesCtrSkipLayerHash ||
|
||||||
|
this->encryption_type == EncryptionType::AesCtrExSkipLayerHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetHashTargetOffset(s64* out) const {
|
||||||
|
switch (this->hash_type) {
|
||||||
|
case HashType::HierarchicalIntegrityHash:
|
||||||
|
case HashType::HierarchicalIntegritySha3Hash:
|
||||||
|
*out = this->hash_data.integrity_meta_info.level_hash_info
|
||||||
|
.info[this->hash_data.integrity_meta_info.level_hash_info.max_layers - 2]
|
||||||
|
.offset;
|
||||||
|
R_SUCCEED();
|
||||||
|
case HashType::HierarchicalSha256Hash:
|
||||||
|
case HashType::HierarchicalSha3256Hash:
|
||||||
|
*out =
|
||||||
|
this->hash_data.hierarchical_sha256_data
|
||||||
|
.hash_layer_region[this->hash_data.hierarchical_sha256_data.hash_layer_count -
|
||||||
|
1]
|
||||||
|
.offset;
|
||||||
|
R_SUCCEED();
|
||||||
|
default:
|
||||||
|
R_THROW(ResultInvalidNcaFsHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NcaFsHeader) == NcaFsHeader::Size);
|
||||||
|
static_assert(std::is_trivial_v<NcaFsHeader>);
|
||||||
|
static_assert(offsetof(NcaFsHeader, patch_info) == NcaPatchInfo::Offset);
|
||||||
|
|
||||||
|
inline constexpr const size_t NcaFsHeader::HashData::HierarchicalSha256Data::MasterHashOffset =
|
||||||
|
offsetof(NcaFsHeader, hash_data.hierarchical_sha256_data.fs_data_master_hash);
|
||||||
|
inline constexpr const size_t NcaFsHeader::HashData::IntegrityMetaInfo::MasterHashOffset =
|
||||||
|
offsetof(NcaFsHeader, hash_data.integrity_meta_info.master_hash);
|
||||||
|
|
||||||
|
struct NcaMetaDataHashData {
|
||||||
|
s64 layer_info_offset;
|
||||||
|
NcaFsHeader::HashData::IntegrityMetaInfo integrity_meta_info;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NcaMetaDataHashData) ==
|
||||||
|
sizeof(NcaFsHeader::HashData::IntegrityMetaInfo) + sizeof(s64));
|
||||||
|
static_assert(std::is_trivial_v<NcaMetaDataHashData>);
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
531
core/fs/fssystem/fssystem_nca_reader.cpp
Normal file
531
core/fs/fssystem/fssystem_nca_reader.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_aes_xts_storage.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr inline u32 SdkAddonVersionMin = 0x000B0000;
|
||||||
|
constexpr inline size_t Aes128KeySize = 0x10;
|
||||||
|
constexpr const std::array<u8, Aes128KeySize> ZeroKey{};
|
||||||
|
|
||||||
|
constexpr Result CheckNcaMagic(u32 magic) {
|
||||||
|
// Verify the magic is not a deprecated one.
|
||||||
|
R_UNLESS(magic != NcaHeader::Magic0, ResultUnsupportedSdkVersion);
|
||||||
|
R_UNLESS(magic != NcaHeader::Magic1, ResultUnsupportedSdkVersion);
|
||||||
|
R_UNLESS(magic != NcaHeader::Magic2, ResultUnsupportedSdkVersion);
|
||||||
|
|
||||||
|
// Verify the magic is the current one.
|
||||||
|
R_UNLESS(magic == NcaHeader::Magic3, ResultInvalidNcaSignature);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
NcaReader::NcaReader()
|
||||||
|
: m_body_storage(), m_header_storage(), m_is_software_aes_prioritized(false),
|
||||||
|
m_is_available_sw_key(false), m_header_encryption_type(NcaHeader::EncryptionType::Auto),
|
||||||
|
m_get_decompressor() {
|
||||||
|
std::memset(std::addressof(m_header), 0, sizeof(m_header));
|
||||||
|
std::memset(std::addressof(m_decryption_keys), 0, sizeof(m_decryption_keys));
|
||||||
|
std::memset(std::addressof(m_external_decryption_key), 0, sizeof(m_external_decryption_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaReader::~NcaReader() {}
|
||||||
|
|
||||||
|
Result NcaReader::Initialize(VirtualFile base_storage, const NcaCryptoConfiguration& crypto_cfg,
|
||||||
|
const NcaCompressionConfiguration& compression_cfg) {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(base_storage != nullptr);
|
||||||
|
ASSERT(m_body_storage == nullptr);
|
||||||
|
|
||||||
|
// Create the work header storage storage.
|
||||||
|
VirtualFile work_header_storage;
|
||||||
|
|
||||||
|
// We need to be able to generate keys.
|
||||||
|
R_UNLESS(crypto_cfg.generate_key != nullptr, ResultInvalidArgument);
|
||||||
|
|
||||||
|
// Generate keys for header.
|
||||||
|
using AesXtsStorageForNcaHeader = AesXtsStorage;
|
||||||
|
|
||||||
|
constexpr std::array<s32, NcaCryptoConfiguration::HeaderEncryptionKeyCount>
|
||||||
|
HeaderKeyTypeValues = {
|
||||||
|
static_cast<s32>(KeyType::NcaHeaderKey1),
|
||||||
|
static_cast<s32>(KeyType::NcaHeaderKey2),
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<std::array<u8, NcaCryptoConfiguration::Aes128KeySize>,
|
||||||
|
NcaCryptoConfiguration::HeaderEncryptionKeyCount>
|
||||||
|
header_decryption_keys;
|
||||||
|
for (size_t i = 0; i < NcaCryptoConfiguration::HeaderEncryptionKeyCount; i++) {
|
||||||
|
crypto_cfg.generate_key(header_decryption_keys[i].data(),
|
||||||
|
AesXtsStorageForNcaHeader::KeySize,
|
||||||
|
crypto_cfg.header_encrypted_encryption_keys[i].data(),
|
||||||
|
AesXtsStorageForNcaHeader::KeySize, HeaderKeyTypeValues[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the header storage.
|
||||||
|
std::array<u8, AesXtsStorageForNcaHeader::IvSize> header_iv = {};
|
||||||
|
work_header_storage = std::make_unique<AesXtsStorageForNcaHeader>(
|
||||||
|
base_storage, header_decryption_keys[0].data(), header_decryption_keys[1].data(),
|
||||||
|
AesXtsStorageForNcaHeader::KeySize, header_iv.data(), AesXtsStorageForNcaHeader::IvSize,
|
||||||
|
NcaHeader::XtsBlockSize);
|
||||||
|
|
||||||
|
// Check that we successfully created the storage.
|
||||||
|
R_UNLESS(work_header_storage != nullptr, ResultAllocationMemoryFailedInNcaReaderA);
|
||||||
|
|
||||||
|
// Read the header.
|
||||||
|
work_header_storage->ReadObject(std::addressof(m_header), 0);
|
||||||
|
|
||||||
|
// Validate the magic.
|
||||||
|
if (const Result magic_result = CheckNcaMagic(m_header.magic); R_FAILED(magic_result)) {
|
||||||
|
// Try to use a plaintext header.
|
||||||
|
base_storage->ReadObject(std::addressof(m_header), 0);
|
||||||
|
R_UNLESS(R_SUCCEEDED(CheckNcaMagic(m_header.magic)), magic_result);
|
||||||
|
|
||||||
|
// Configure to use the plaintext header.
|
||||||
|
auto base_storage_size = base_storage->GetSize();
|
||||||
|
work_header_storage = std::make_shared<OffsetVfsFile>(base_storage, base_storage_size, 0);
|
||||||
|
R_UNLESS(work_header_storage != nullptr, ResultAllocationMemoryFailedInNcaReaderA);
|
||||||
|
|
||||||
|
// Set encryption type as plaintext.
|
||||||
|
m_header_encryption_type = NcaHeader::EncryptionType::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the header sign1.
|
||||||
|
if (crypto_cfg.verify_sign1 != nullptr) {
|
||||||
|
const u8* sig = m_header.header_sign_1.data();
|
||||||
|
const size_t sig_size = NcaHeader::HeaderSignSize;
|
||||||
|
const u8* msg =
|
||||||
|
static_cast<const u8*>(static_cast<const void*>(std::addressof(m_header.magic)));
|
||||||
|
const size_t msg_size =
|
||||||
|
NcaHeader::Size - NcaHeader::HeaderSignSize * NcaHeader::HeaderSignCount;
|
||||||
|
|
||||||
|
m_is_header_sign1_signature_valid = crypto_cfg.verify_sign1(
|
||||||
|
sig, sig_size, msg, msg_size, m_header.header1_signature_key_generation);
|
||||||
|
|
||||||
|
if (!m_is_header_sign1_signature_valid) {
|
||||||
|
LOG_WARNING(Common_Filesystem, "Invalid NCA header sign1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the sdk version.
|
||||||
|
R_UNLESS(m_header.sdk_addon_version >= SdkAddonVersionMin, ResultUnsupportedSdkVersion);
|
||||||
|
|
||||||
|
// Validate the key index.
|
||||||
|
R_UNLESS(m_header.key_index < NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount ||
|
||||||
|
m_header.key_index == NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexZeroKey,
|
||||||
|
ResultInvalidNcaKeyIndex);
|
||||||
|
|
||||||
|
// Check if we have a rights id.
|
||||||
|
constexpr const std::array<u8, NcaHeader::RightsIdSize> ZeroRightsId{};
|
||||||
|
if (std::memcmp(ZeroRightsId.data(), m_header.rights_id.data(), NcaHeader::RightsIdSize) == 0) {
|
||||||
|
// If we don't, then we don't have an external key, so we need to generate decryption keys.
|
||||||
|
crypto_cfg.generate_key(
|
||||||
|
m_decryption_keys[NcaHeader::DecryptionKey_AesCtr].data(), Aes128KeySize,
|
||||||
|
m_header.encrypted_key_area.data() + NcaHeader::DecryptionKey_AesCtr * Aes128KeySize,
|
||||||
|
Aes128KeySize, GetKeyTypeValue(m_header.key_index, m_header.GetProperKeyGeneration()));
|
||||||
|
crypto_cfg.generate_key(
|
||||||
|
m_decryption_keys[NcaHeader::DecryptionKey_AesXts1].data(), Aes128KeySize,
|
||||||
|
m_header.encrypted_key_area.data() + NcaHeader::DecryptionKey_AesXts1 * Aes128KeySize,
|
||||||
|
Aes128KeySize, GetKeyTypeValue(m_header.key_index, m_header.GetProperKeyGeneration()));
|
||||||
|
crypto_cfg.generate_key(
|
||||||
|
m_decryption_keys[NcaHeader::DecryptionKey_AesXts2].data(), Aes128KeySize,
|
||||||
|
m_header.encrypted_key_area.data() + NcaHeader::DecryptionKey_AesXts2 * Aes128KeySize,
|
||||||
|
Aes128KeySize, GetKeyTypeValue(m_header.key_index, m_header.GetProperKeyGeneration()));
|
||||||
|
crypto_cfg.generate_key(
|
||||||
|
m_decryption_keys[NcaHeader::DecryptionKey_AesCtrEx].data(), Aes128KeySize,
|
||||||
|
m_header.encrypted_key_area.data() + NcaHeader::DecryptionKey_AesCtrEx * Aes128KeySize,
|
||||||
|
Aes128KeySize, GetKeyTypeValue(m_header.key_index, m_header.GetProperKeyGeneration()));
|
||||||
|
|
||||||
|
// Copy the hardware speed emulation key.
|
||||||
|
std::memcpy(m_decryption_keys[NcaHeader::DecryptionKey_AesCtrHw].data(),
|
||||||
|
m_header.encrypted_key_area.data() +
|
||||||
|
NcaHeader::DecryptionKey_AesCtrHw * Aes128KeySize,
|
||||||
|
Aes128KeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the external decryption key.
|
||||||
|
std::memset(m_external_decryption_key.data(), 0, m_external_decryption_key.size());
|
||||||
|
|
||||||
|
// Set software key availability.
|
||||||
|
m_is_available_sw_key = crypto_cfg.is_available_sw_key;
|
||||||
|
|
||||||
|
// Set our decompressor function getter.
|
||||||
|
m_get_decompressor = compression_cfg.get_decompressor;
|
||||||
|
|
||||||
|
// Set our storages.
|
||||||
|
m_header_storage = std::move(work_header_storage);
|
||||||
|
m_body_storage = std::move(base_storage);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile NcaReader::GetSharedBodyStorage() {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_body_storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NcaReader::GetMagic() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.magic;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaHeader::DistributionType NcaReader::GetDistributionType() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.distribution_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaHeader::ContentType NcaReader::GetContentType() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.content_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 NcaReader::GetHeaderSign1KeyGeneration() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.header1_signature_key_generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 NcaReader::GetKeyGeneration() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.GetProperKeyGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 NcaReader::GetKeyIndex() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.key_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NcaReader::GetContentSize() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.content_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NcaReader::GetProgramId() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.program_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NcaReader::GetContentIndex() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.content_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 NcaReader::GetSdkAddonVersion() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
return m_header.sdk_addon_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetRightsId(u8* dst, size_t dst_size) const {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(dst_size >= NcaHeader::RightsIdSize);
|
||||||
|
|
||||||
|
std::memcpy(dst, m_header.rights_id.data(), NcaHeader::RightsIdSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::HasFsInfo(s32 index) const {
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
return m_header.fs_info[index].start_sector != 0 || m_header.fs_info[index].end_sector != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 NcaReader::GetFsCount() const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
for (s32 i = 0; i < NcaHeader::FsCountMax; i++) {
|
||||||
|
if (!this->HasFsInfo(i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NcaHeader::FsCountMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Hash& NcaReader::GetFsHeaderHash(s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
return m_header.fs_header_hash[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetFsHeaderHash(Hash* dst, s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
std::memcpy(dst, std::addressof(m_header.fs_header_hash[index]), sizeof(*dst));
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetFsInfo(NcaHeader::FsInfo* dst, s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
std::memcpy(dst, std::addressof(m_header.fs_info[index]), sizeof(*dst));
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NcaReader::GetFsOffset(s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
return NcaHeader::SectorToByte(m_header.fs_info[index].start_sector);
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NcaReader::GetFsEndOffset(s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
return NcaHeader::SectorToByte(m_header.fs_info[index].end_sector);
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NcaReader::GetFsSize(s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
return NcaHeader::SectorToByte(m_header.fs_info[index].end_sector -
|
||||||
|
m_header.fs_info[index].start_sector);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetEncryptedKey(void* dst, size_t size) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(size >= NcaHeader::EncryptedKeyAreaSize);
|
||||||
|
|
||||||
|
std::memcpy(dst, m_header.encrypted_key_area.data(), NcaHeader::EncryptedKeyAreaSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const void* NcaReader::GetDecryptionKey(s32 index) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::DecryptionKey_Count);
|
||||||
|
return m_decryption_keys[index].data();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::HasValidInternalKey() const {
|
||||||
|
for (s32 i = 0; i < NcaHeader::DecryptionKey_Count; i++) {
|
||||||
|
if (std::memcmp(ZeroKey.data(), m_header.encrypted_key_area.data() + i * Aes128KeySize,
|
||||||
|
Aes128KeySize) != 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::HasInternalDecryptionKeyForAesHw() const {
|
||||||
|
return std::memcmp(ZeroKey.data(), this->GetDecryptionKey(NcaHeader::DecryptionKey_AesCtrHw),
|
||||||
|
Aes128KeySize) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::IsSoftwareAesPrioritized() const {
|
||||||
|
return m_is_software_aes_prioritized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::PrioritizeSoftwareAes() {
|
||||||
|
m_is_software_aes_prioritized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::IsAvailableSwKey() const {
|
||||||
|
return m_is_available_sw_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::HasExternalDecryptionKey() const {
|
||||||
|
return std::memcmp(ZeroKey.data(), this->GetExternalDecryptionKey(), Aes128KeySize) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const void* NcaReader::GetExternalDecryptionKey() const {
|
||||||
|
return m_external_decryption_key.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::SetExternalDecryptionKey(const void* src, size_t size) {
|
||||||
|
ASSERT(src != nullptr);
|
||||||
|
ASSERT(size == sizeof(m_external_decryption_key));
|
||||||
|
|
||||||
|
std::memcpy(m_external_decryption_key.data(), src, sizeof(m_external_decryption_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetRawData(void* dst, size_t dst_size) const {
|
||||||
|
ASSERT(m_body_storage != nullptr);
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(dst_size >= sizeof(NcaHeader));
|
||||||
|
|
||||||
|
std::memcpy(dst, std::addressof(m_header), sizeof(NcaHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
GetDecompressorFunction NcaReader::GetDecompressor() const {
|
||||||
|
ASSERT(m_get_decompressor != nullptr);
|
||||||
|
return m_get_decompressor;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaHeader::EncryptionType NcaReader::GetEncryptionType() const {
|
||||||
|
return m_header_encryption_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result NcaReader::ReadHeader(NcaFsHeader* dst, s32 index) const {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(0 <= index && index < NcaHeader::FsCountMax);
|
||||||
|
|
||||||
|
const s64 offset = sizeof(NcaHeader) + sizeof(NcaFsHeader) * index;
|
||||||
|
m_header_storage->ReadObject(dst, offset);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaReader::GetHeaderSign1Valid() const {
|
||||||
|
return m_is_header_sign1_signature_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaReader::GetHeaderSign2(void* dst, size_t size) const {
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(size == NcaHeader::HeaderSignSize);
|
||||||
|
|
||||||
|
std::memcpy(dst, m_header.header_sign_2.data(), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result NcaFsHeaderReader::Initialize(const NcaReader& reader, s32 index) {
|
||||||
|
// Reset ourselves to uninitialized.
|
||||||
|
m_fs_index = -1;
|
||||||
|
|
||||||
|
// Read the header.
|
||||||
|
R_TRY(reader.ReadHeader(std::addressof(m_data), index));
|
||||||
|
|
||||||
|
// Set our index.
|
||||||
|
m_fs_index = index;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NcaFsHeaderReader::GetRawData(void* dst, size_t dst_size) const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
ASSERT(dst != nullptr);
|
||||||
|
ASSERT(dst_size >= sizeof(NcaFsHeader));
|
||||||
|
|
||||||
|
std::memcpy(dst, std::addressof(m_data), sizeof(NcaFsHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::HashData& NcaFsHeaderReader::GetHashData() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.hash_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaFsHeader::HashData& NcaFsHeaderReader::GetHashData() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.hash_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 NcaFsHeaderReader::GetVersion() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 NcaFsHeaderReader::GetFsIndex() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_fs_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::FsType NcaFsHeaderReader::GetFsType() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.fs_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::HashType NcaFsHeaderReader::GetHashType() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.hash_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::EncryptionType NcaFsHeaderReader::GetEncryptionType() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.encryption_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaPatchInfo& NcaFsHeaderReader::GetPatchInfo() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.patch_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaPatchInfo& NcaFsHeaderReader::GetPatchInfo() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.patch_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaAesCtrUpperIv NcaFsHeaderReader::GetAesCtrUpperIv() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.aes_ctr_upper_iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaFsHeaderReader::IsSkipLayerHashEncryption() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.IsSkipLayerHashEncryption();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result NcaFsHeaderReader::GetHashTargetOffset(s64* out) const {
|
||||||
|
ASSERT(out != nullptr);
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
R_RETURN(m_data.GetHashTargetOffset(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaFsHeaderReader::ExistsSparseLayer() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.sparse_info.generation != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaSparseInfo& NcaFsHeaderReader::GetSparseInfo() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.sparse_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaSparseInfo& NcaFsHeaderReader::GetSparseInfo() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.sparse_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaFsHeaderReader::ExistsCompressionLayer() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.compression_info.bucket.offset != 0 && m_data.compression_info.bucket.size != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaCompressionInfo& NcaFsHeaderReader::GetCompressionInfo() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.compression_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaCompressionInfo& NcaFsHeaderReader::GetCompressionInfo() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.compression_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaFsHeaderReader::ExistsPatchMetaHashLayer() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info.size != 0 && this->GetPatchInfo().HasIndirectTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaMetaDataHashDataInfo& NcaFsHeaderReader::GetPatchMetaDataHashDataInfo() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaMetaDataHashDataInfo& NcaFsHeaderReader::GetPatchMetaDataHashDataInfo() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::MetaDataHashType NcaFsHeaderReader::GetPatchMetaHashType() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NcaFsHeaderReader::ExistsSparseMetaHashLayer() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info.size != 0 && this->ExistsSparseLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaMetaDataHashDataInfo& NcaFsHeaderReader::GetSparseMetaDataHashDataInfo() {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NcaMetaDataHashDataInfo& NcaFsHeaderReader::GetSparseMetaDataHashDataInfo() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_data_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
NcaFsHeader::MetaDataHashType NcaFsHeaderReader::GetSparseMetaHashType() const {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
return m_data.meta_data_hash_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
61
core/fs/fssystem/fssystem_pooled_buffer.cpp
Normal file
61
core/fs/fssystem/fssystem_pooled_buffer.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/alignment.h"
|
||||||
|
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr size_t HeapBlockSize = BufferPoolAlignment;
|
||||||
|
static_assert(HeapBlockSize == 4_KiB);
|
||||||
|
|
||||||
|
// A heap block is 4KiB. An order is a power of two.
|
||||||
|
// This gives blocks of the order 32KiB, 512KiB, 4MiB.
|
||||||
|
constexpr s32 HeapOrderMax = 7;
|
||||||
|
constexpr s32 HeapOrderMaxForLarge = HeapOrderMax + 3;
|
||||||
|
|
||||||
|
constexpr size_t HeapAllocatableSizeMax = HeapBlockSize * (static_cast<size_t>(1) << HeapOrderMax);
|
||||||
|
constexpr size_t HeapAllocatableSizeMaxForLarge =
|
||||||
|
HeapBlockSize * (static_cast<size_t>(1) << HeapOrderMaxForLarge);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
size_t PooledBuffer::GetAllocatableSizeMaxCore(bool large) {
|
||||||
|
return large ? HeapAllocatableSizeMaxForLarge : HeapAllocatableSizeMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PooledBuffer::AllocateCore(size_t ideal_size, size_t required_size, bool large) {
|
||||||
|
// Ensure preconditions.
|
||||||
|
ASSERT(m_buffer == nullptr);
|
||||||
|
|
||||||
|
// Check that we can allocate this size.
|
||||||
|
ASSERT(required_size <= GetAllocatableSizeMaxCore(large));
|
||||||
|
|
||||||
|
const size_t target_size =
|
||||||
|
std::min(std::max(ideal_size, required_size), GetAllocatableSizeMaxCore(large));
|
||||||
|
|
||||||
|
// Dummy implementation for allocate.
|
||||||
|
if (target_size > 0) {
|
||||||
|
m_buffer =
|
||||||
|
reinterpret_cast<char*>(::operator new(target_size, std::align_val_t{HeapBlockSize}));
|
||||||
|
m_size = target_size;
|
||||||
|
|
||||||
|
// Ensure postconditions.
|
||||||
|
ASSERT(m_buffer != nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PooledBuffer::Shrink(size_t ideal_size) {
|
||||||
|
ASSERT(ideal_size <= GetAllocatableSizeMaxCore(true));
|
||||||
|
|
||||||
|
// Shrinking to zero means that we have no buffer.
|
||||||
|
if (ideal_size == 0) {
|
||||||
|
::operator delete(m_buffer, std::align_val_t{HeapBlockSize});
|
||||||
|
m_buffer = nullptr;
|
||||||
|
m_size = ideal_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
95
core/fs/fssystem/fssystem_pooled_buffer.h
Normal file
95
core/fs/fssystem/fssystem_pooled_buffer.h
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/literals.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
|
||||||
|
constexpr inline size_t BufferPoolAlignment = 4_KiB;
|
||||||
|
constexpr inline size_t BufferPoolWorkSize = 320;
|
||||||
|
|
||||||
|
class PooledBuffer {
|
||||||
|
YUZU_NON_COPYABLE(PooledBuffer);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor/Destructor.
|
||||||
|
constexpr PooledBuffer() : m_buffer(), m_size() {}
|
||||||
|
|
||||||
|
PooledBuffer(size_t ideal_size, size_t required_size) : m_buffer(), m_size() {
|
||||||
|
this->Allocate(ideal_size, required_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
~PooledBuffer() {
|
||||||
|
this->Deallocate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move and assignment.
|
||||||
|
explicit PooledBuffer(PooledBuffer&& rhs) : m_buffer(rhs.m_buffer), m_size(rhs.m_size) {
|
||||||
|
rhs.m_buffer = nullptr;
|
||||||
|
rhs.m_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PooledBuffer& operator=(PooledBuffer&& rhs) {
|
||||||
|
PooledBuffer(std::move(rhs)).Swap(*this);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocation API.
|
||||||
|
void Allocate(size_t ideal_size, size_t required_size) {
|
||||||
|
return this->AllocateCore(ideal_size, required_size, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AllocateParticularlyLarge(size_t ideal_size, size_t required_size) {
|
||||||
|
return this->AllocateCore(ideal_size, required_size, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shrink(size_t ideal_size);
|
||||||
|
|
||||||
|
void Deallocate() {
|
||||||
|
// Shrink the buffer to empty.
|
||||||
|
this->Shrink(0);
|
||||||
|
ASSERT(m_buffer == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* GetBuffer() const {
|
||||||
|
ASSERT(m_buffer != nullptr);
|
||||||
|
return m_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GetSize() const {
|
||||||
|
ASSERT(m_buffer != nullptr);
|
||||||
|
return m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static size_t GetAllocatableSizeMax() {
|
||||||
|
return GetAllocatableSizeMaxCore(false);
|
||||||
|
}
|
||||||
|
static size_t GetAllocatableParticularlyLargeSizeMax() {
|
||||||
|
return GetAllocatableSizeMaxCore(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static size_t GetAllocatableSizeMaxCore(bool large);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Swap(PooledBuffer& rhs) {
|
||||||
|
std::swap(m_buffer, rhs.m_buffer);
|
||||||
|
std::swap(m_size, rhs.m_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AllocateCore(size_t ideal_size, size_t required_size, bool large);
|
||||||
|
|
||||||
|
private:
|
||||||
|
char* m_buffer;
|
||||||
|
size_t m_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
39
core/fs/fssystem/fssystem_sparse_storage.cpp
Normal file
39
core/fs/fssystem/fssystem_sparse_storage.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_sparse_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
size_t SparseStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||||
|
// Validate preconditions.
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
ASSERT(buffer != nullptr);
|
||||||
|
|
||||||
|
// Allow zero size.
|
||||||
|
if (size == 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
SparseStorage* self = const_cast<SparseStorage*>(this);
|
||||||
|
|
||||||
|
if (self->GetEntryTable().IsEmpty()) {
|
||||||
|
BucketTree::Offsets table_offsets;
|
||||||
|
ASSERT(R_SUCCEEDED(self->GetEntryTable().GetOffsets(std::addressof(table_offsets))));
|
||||||
|
ASSERT(table_offsets.IsInclude(offset, size));
|
||||||
|
|
||||||
|
std::memset(buffer, 0, size);
|
||||||
|
} else {
|
||||||
|
self->OperatePerEntry<false, true>(
|
||||||
|
offset, size,
|
||||||
|
[=](VirtualFile storage, s64 data_offset, s64 cur_offset, s64 cur_size) -> Result {
|
||||||
|
storage->Read(reinterpret_cast<u8*>(buffer) + (cur_offset - offset),
|
||||||
|
static_cast<size_t>(cur_size), data_offset);
|
||||||
|
R_SUCCEED();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
72
core/fs/fssystem/fssystem_sparse_storage.h
Normal file
72
core/fs/fssystem/fssystem_sparse_storage.h
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_indirect_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class SparseStorage : public IndirectStorage {
|
||||||
|
YUZU_NON_COPYABLE(SparseStorage);
|
||||||
|
YUZU_NON_MOVEABLE(SparseStorage);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class ZeroStorage : public IReadOnlyStorage {
|
||||||
|
public:
|
||||||
|
ZeroStorage() {}
|
||||||
|
virtual ~ZeroStorage() {}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return std::numeric_limits<size_t>::max();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
ASSERT(buffer != nullptr || size == 0);
|
||||||
|
|
||||||
|
if (size > 0) {
|
||||||
|
std::memset(buffer, 0, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
SparseStorage() : IndirectStorage(), m_zero_storage(std::make_shared<ZeroStorage>()) {}
|
||||||
|
virtual ~SparseStorage() {}
|
||||||
|
|
||||||
|
using IndirectStorage::Initialize;
|
||||||
|
|
||||||
|
void Initialize(s64 end_offset) {
|
||||||
|
this->GetEntryTable().Initialize(NodeSize, end_offset);
|
||||||
|
this->SetZeroStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDataStorage(VirtualFile storage) {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
this->SetStorage(0, storage);
|
||||||
|
this->SetZeroStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void SetDataStorage(T storage, s64 offset, s64 size) {
|
||||||
|
ASSERT(this->IsInitialized());
|
||||||
|
|
||||||
|
this->SetStorage(0, storage, offset, size);
|
||||||
|
this->SetZeroStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetZeroStorage() {
|
||||||
|
return this->SetStorage(1, m_zero_storage, 0, std::numeric_limits<s64>::max());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_zero_storage;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
80
core/fs/fssystem/fssystem_switch_storage.h
Normal file
80
core/fs/fssystem/fssystem_switch_storage.h
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class RegionSwitchStorage : public IReadOnlyStorage {
|
||||||
|
YUZU_NON_COPYABLE(RegionSwitchStorage);
|
||||||
|
YUZU_NON_MOVEABLE(RegionSwitchStorage);
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct Region {
|
||||||
|
s64 offset;
|
||||||
|
s64 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
RegionSwitchStorage(VirtualFile&& i, VirtualFile&& o, Region r)
|
||||||
|
: m_inside_region_storage(std::move(i)), m_outside_region_storage(std::move(o)),
|
||||||
|
m_region(r) {}
|
||||||
|
|
||||||
|
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||||
|
// Process until we're done.
|
||||||
|
size_t processed = 0;
|
||||||
|
while (processed < size) {
|
||||||
|
// Process on the appropriate storage.
|
||||||
|
s64 cur_size = 0;
|
||||||
|
if (this->CheckRegions(std::addressof(cur_size), offset + processed,
|
||||||
|
size - processed)) {
|
||||||
|
m_inside_region_storage->Read(buffer + processed, cur_size, offset + processed);
|
||||||
|
} else {
|
||||||
|
m_outside_region_storage->Read(buffer + processed, cur_size, offset + processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance.
|
||||||
|
processed += cur_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetSize() const override {
|
||||||
|
return m_inside_region_storage->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool CheckRegions(s64* out_current_size, s64 offset, s64 size) const {
|
||||||
|
// Check if our region contains the access.
|
||||||
|
if (m_region.offset <= offset) {
|
||||||
|
if (offset < m_region.offset + m_region.size) {
|
||||||
|
if (m_region.offset + m_region.size <= offset + size) {
|
||||||
|
*out_current_size = m_region.offset + m_region.size - offset;
|
||||||
|
} else {
|
||||||
|
*out_current_size = size;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
*out_current_size = size;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (m_region.offset <= offset + size) {
|
||||||
|
*out_current_size = m_region.offset - offset;
|
||||||
|
} else {
|
||||||
|
*out_current_size = size;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile m_inside_region_storage;
|
||||||
|
VirtualFile m_outside_region_storage;
|
||||||
|
Region m_region;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
27
core/fs/fssystem/fssystem_utility.cpp
Normal file
27
core/fs/fssystem/fssystem_utility.cpp
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/file_sys/fssystem/fssystem_utility.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
void AddCounter(void* counter_, size_t counter_size, u64 value) {
|
||||||
|
u8* counter = static_cast<u8*>(counter_);
|
||||||
|
u64 remaining = value;
|
||||||
|
u8 carry = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < counter_size; i++) {
|
||||||
|
auto sum = counter[counter_size - 1 - i] + (remaining & 0xFF) + carry;
|
||||||
|
carry = static_cast<u8>(sum >> (sizeof(u8) * 8));
|
||||||
|
auto sum8 = static_cast<u8>(sum & 0xFF);
|
||||||
|
|
||||||
|
counter[counter_size - 1 - i] = sum8;
|
||||||
|
|
||||||
|
remaining >>= (sizeof(u8) * 8);
|
||||||
|
if (carry == 0 && remaining == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
12
core/fs/fssystem/fssystem_utility.h
Normal file
12
core/fs/fssystem/fssystem_utility.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
void AddCounter(void* counter, size_t counter_size, u64 value);
|
||||||
|
|
||||||
|
}
|
||||||
344
core/fs/ips_layer.cpp
Normal file
344
core/fs/ips_layer.cpp
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/hex_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/ips_layer.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class IPSFileType {
|
||||||
|
IPS,
|
||||||
|
IPS32,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array<std::pair<const char*, const char*>, 11> ESCAPE_CHARACTER_MAP{{
|
||||||
|
{"\\a", "\a"},
|
||||||
|
{"\\b", "\b"},
|
||||||
|
{"\\f", "\f"},
|
||||||
|
{"\\n", "\n"},
|
||||||
|
{"\\r", "\r"},
|
||||||
|
{"\\t", "\t"},
|
||||||
|
{"\\v", "\v"},
|
||||||
|
{"\\\\", "\\"},
|
||||||
|
{"\\\'", "\'"},
|
||||||
|
{"\\\"", "\""},
|
||||||
|
{"\\\?", "\?"},
|
||||||
|
}};
|
||||||
|
|
||||||
|
static IPSFileType IdentifyMagic(const std::vector<u8>& magic) {
|
||||||
|
if (magic.size() != 5) {
|
||||||
|
return IPSFileType::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::array<u8, 5> patch_magic{{'P', 'A', 'T', 'C', 'H'}};
|
||||||
|
if (std::equal(magic.begin(), magic.end(), patch_magic.begin())) {
|
||||||
|
return IPSFileType::IPS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::array<u8, 5> ips32_magic{{'I', 'P', 'S', '3', '2'}};
|
||||||
|
if (std::equal(magic.begin(), magic.end(), ips32_magic.begin())) {
|
||||||
|
return IPSFileType::IPS32;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IPSFileType::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsEOF(IPSFileType type, const std::vector<u8>& data) {
|
||||||
|
static constexpr std::array<u8, 3> eof{{'E', 'O', 'F'}};
|
||||||
|
if (type == IPSFileType::IPS && std::equal(data.begin(), data.end(), eof.begin())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::array<u8, 4> eeof{{'E', 'E', 'O', 'F'}};
|
||||||
|
return type == IPSFileType::IPS32 && std::equal(data.begin(), data.end(), eeof.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
|
||||||
|
if (in == nullptr || ips == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const auto type = IdentifyMagic(ips->ReadBytes(0x5));
|
||||||
|
if (type == IPSFileType::Error)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto in_data = in->ReadAllBytes();
|
||||||
|
if (in_data.size() == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4);
|
||||||
|
u64 offset = 5; // After header
|
||||||
|
while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) {
|
||||||
|
offset += temp.size();
|
||||||
|
if (IsEOF(type, temp)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 real_offset{};
|
||||||
|
if (type == IPSFileType::IPS32)
|
||||||
|
real_offset = (temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3];
|
||||||
|
else
|
||||||
|
real_offset = (temp[0] << 16) | (temp[1] << 8) | temp[2];
|
||||||
|
|
||||||
|
if (real_offset > in_data.size()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 data_size{};
|
||||||
|
if (ips->ReadObject(&data_size, offset) != sizeof(u16))
|
||||||
|
return nullptr;
|
||||||
|
data_size = Common::swap16(data_size);
|
||||||
|
offset += sizeof(u16);
|
||||||
|
|
||||||
|
if (data_size == 0) { // RLE
|
||||||
|
u16 rle_size{};
|
||||||
|
if (ips->ReadObject(&rle_size, offset) != sizeof(u16))
|
||||||
|
return nullptr;
|
||||||
|
rle_size = Common::swap16(rle_size);
|
||||||
|
offset += sizeof(u16);
|
||||||
|
|
||||||
|
const auto data = ips->ReadByte(offset++);
|
||||||
|
if (!data)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (real_offset + rle_size > in_data.size())
|
||||||
|
rle_size = static_cast<u16>(in_data.size() - real_offset);
|
||||||
|
std::memset(in_data.data() + real_offset, *data, rle_size);
|
||||||
|
} else { // Standard Patch
|
||||||
|
auto read = data_size;
|
||||||
|
if (real_offset + read > in_data.size())
|
||||||
|
read = static_cast<u16>(in_data.size() - real_offset);
|
||||||
|
if (ips->Read(in_data.data() + real_offset, read, offset) != data_size)
|
||||||
|
return nullptr;
|
||||||
|
offset += data_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEOF(type, temp)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
|
||||||
|
in->GetContainingDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IPSwitchCompiler::IPSwitchPatch {
|
||||||
|
std::string name;
|
||||||
|
bool enabled;
|
||||||
|
std::map<u32, std::vector<u8>> records;
|
||||||
|
};
|
||||||
|
|
||||||
|
IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) {
|
||||||
|
Parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
IPSwitchCompiler::~IPSwitchCompiler() = default;
|
||||||
|
|
||||||
|
std::array<u8, 32> IPSwitchCompiler::GetBuildID() const {
|
||||||
|
return nso_build_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IPSwitchCompiler::IsValid() const {
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool StartsWith(std::string_view base, std::string_view check) {
|
||||||
|
return base.size() >= check.size() && base.substr(0, check.size()) == check;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string EscapeStringSequences(std::string in) {
|
||||||
|
for (const auto& seq : ESCAPE_CHARACTER_MAP) {
|
||||||
|
for (auto index = in.find(seq.first); index != std::string::npos;
|
||||||
|
index = in.find(seq.first, index)) {
|
||||||
|
in.replace(index, std::strlen(seq.first), seq.second);
|
||||||
|
index += std::strlen(seq.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IPSwitchCompiler::ParseFlag(const std::string& line) {
|
||||||
|
if (StartsWith(line, "@flag offset_shift ")) {
|
||||||
|
// Offset Shift Flag
|
||||||
|
offset_shift = std::strtoll(line.substr(19).c_str(), nullptr, 0);
|
||||||
|
} else if (StartsWith(line, "@little-endian")) {
|
||||||
|
// Set values to read as little endian
|
||||||
|
is_little_endian = true;
|
||||||
|
} else if (StartsWith(line, "@big-endian")) {
|
||||||
|
// Set values to read as big endian
|
||||||
|
is_little_endian = false;
|
||||||
|
} else if (StartsWith(line, "@flag print_values")) {
|
||||||
|
// Force printing of applied values
|
||||||
|
print_values = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IPSwitchCompiler::Parse() {
|
||||||
|
const auto bytes = patch_text->ReadAllBytes();
|
||||||
|
std::stringstream s;
|
||||||
|
s.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||||
|
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string stream_line;
|
||||||
|
while (std::getline(s, stream_line)) {
|
||||||
|
// Remove a trailing \r
|
||||||
|
if (!stream_line.empty() && stream_line.back() == '\r')
|
||||||
|
stream_line.pop_back();
|
||||||
|
lines.push_back(std::move(stream_line));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
auto line = lines[i];
|
||||||
|
|
||||||
|
// Remove midline comments
|
||||||
|
std::size_t comment_index = std::string::npos;
|
||||||
|
bool within_string = false;
|
||||||
|
for (std::size_t k = 0; k < line.size(); ++k) {
|
||||||
|
if (line[k] == '\"' && (k > 0 && line[k - 1] != '\\')) {
|
||||||
|
within_string = !within_string;
|
||||||
|
} else if (line[k] == '\\' && (k < line.size() - 1 && line[k + 1] == '\\')) {
|
||||||
|
comment_index = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StartsWith(line, "//") && comment_index != std::string::npos) {
|
||||||
|
last_comment = line.substr(comment_index + 2);
|
||||||
|
line = line.substr(0, comment_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StartsWith(line, "@stop")) {
|
||||||
|
// Force stop
|
||||||
|
break;
|
||||||
|
} else if (StartsWith(line, "@nsobid-")) {
|
||||||
|
// NSO Build ID Specifier
|
||||||
|
const auto raw_build_id = fmt::format("{:0<64}", line.substr(8));
|
||||||
|
nso_build_id = Common::HexStringToArray<0x20>(raw_build_id);
|
||||||
|
} else if (StartsWith(line, "#")) {
|
||||||
|
// Mandatory Comment
|
||||||
|
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Forced output comment: {}",
|
||||||
|
patch_text->GetName(), line.substr(1));
|
||||||
|
} else if (StartsWith(line, "//")) {
|
||||||
|
// Normal Comment
|
||||||
|
last_comment = line.substr(2);
|
||||||
|
if (last_comment.find_first_not_of(' ') == std::string::npos)
|
||||||
|
continue;
|
||||||
|
if (last_comment.find_first_not_of(' ') != 0)
|
||||||
|
last_comment = last_comment.substr(last_comment.find_first_not_of(' '));
|
||||||
|
} else if (StartsWith(line, "@enabled") || StartsWith(line, "@disabled")) {
|
||||||
|
// Start of patch
|
||||||
|
const auto enabled = StartsWith(line, "@enabled");
|
||||||
|
if (i == 0)
|
||||||
|
return;
|
||||||
|
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Parsing patch '{}' ({})",
|
||||||
|
patch_text->GetName(), last_comment, line.substr(1));
|
||||||
|
|
||||||
|
IPSwitchPatch patch{last_comment, enabled, {}};
|
||||||
|
|
||||||
|
// Read rest of patch
|
||||||
|
while (true) {
|
||||||
|
if (i + 1 >= lines.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& patch_line = lines[++i];
|
||||||
|
|
||||||
|
// Start of new patch
|
||||||
|
if (StartsWith(patch_line, "@enabled") || StartsWith(patch_line, "@disabled")) {
|
||||||
|
--i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a flag
|
||||||
|
if (StartsWith(patch_line, "@")) {
|
||||||
|
ParseFlag(patch_line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11 - 8 hex digit offset + space + minimum two digit overwrite val
|
||||||
|
if (patch_line.length() < 11)
|
||||||
|
break;
|
||||||
|
auto offset = std::strtoul(patch_line.substr(0, 8).c_str(), nullptr, 16);
|
||||||
|
offset += static_cast<unsigned long>(offset_shift);
|
||||||
|
|
||||||
|
std::vector<u8> replace;
|
||||||
|
// 9 - first char of replacement val
|
||||||
|
if (patch_line[9] == '\"') {
|
||||||
|
// string replacement
|
||||||
|
auto end_index = patch_line.find('\"', 10);
|
||||||
|
if (end_index == std::string::npos || end_index < 10)
|
||||||
|
return;
|
||||||
|
while (patch_line[end_index - 1] == '\\') {
|
||||||
|
end_index = patch_line.find('\"', end_index + 1);
|
||||||
|
if (end_index == std::string::npos || end_index < 10)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value = patch_line.substr(10, end_index - 10);
|
||||||
|
value = EscapeStringSequences(value);
|
||||||
|
replace.reserve(value.size());
|
||||||
|
std::copy(value.begin(), value.end(), std::back_inserter(replace));
|
||||||
|
} else {
|
||||||
|
// hex replacement
|
||||||
|
const auto value =
|
||||||
|
patch_line.substr(9, patch_line.find_first_of(" /\r\n", 9) - 9);
|
||||||
|
replace = Common::HexStringToVector(value, is_little_endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (print_values) {
|
||||||
|
LOG_INFO(Loader,
|
||||||
|
"[IPSwitchCompiler ('{}')] - Patching value at offset 0x{:08X} "
|
||||||
|
"with byte string '{}'",
|
||||||
|
patch_text->GetName(), offset, Common::HexToString(replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
patch.records.insert_or_assign(static_cast<u32>(offset), std::move(replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
patches.push_back(std::move(patch));
|
||||||
|
} else if (StartsWith(line, "@")) {
|
||||||
|
ParseFlag(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const {
|
||||||
|
if (in == nullptr || !valid)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto in_data = in->ReadAllBytes();
|
||||||
|
|
||||||
|
for (const auto& patch : patches) {
|
||||||
|
if (!patch.enabled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (const auto& record : patch.records) {
|
||||||
|
if (record.first >= in_data.size())
|
||||||
|
continue;
|
||||||
|
auto replace_size = record.second.size();
|
||||||
|
if (record.first + replace_size > in_data.size())
|
||||||
|
replace_size = in_data.size() - record.first;
|
||||||
|
for (std::size_t i = 0; i < replace_size; ++i)
|
||||||
|
in_data[i + record.first] = record.second[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
|
||||||
|
in->GetContainingDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
43
core/fs/ips_layer.h
Normal file
43
core/fs/ips_layer.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips);
|
||||||
|
|
||||||
|
class IPSwitchCompiler {
|
||||||
|
public:
|
||||||
|
explicit IPSwitchCompiler(VirtualFile patch_text);
|
||||||
|
~IPSwitchCompiler();
|
||||||
|
|
||||||
|
std::array<u8, 0x20> GetBuildID() const;
|
||||||
|
bool IsValid() const;
|
||||||
|
VirtualFile Apply(const VirtualFile& in) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct IPSwitchPatch;
|
||||||
|
|
||||||
|
void ParseFlag(const std::string& flag);
|
||||||
|
void Parse();
|
||||||
|
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
VirtualFile patch_text;
|
||||||
|
std::vector<IPSwitchPatch> patches;
|
||||||
|
std::array<u8, 0x20> nso_build_id{};
|
||||||
|
bool is_little_endian = false;
|
||||||
|
s64 offset_shift = 0;
|
||||||
|
bool print_values = false;
|
||||||
|
std::string last_comment = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
230
core/fs/kernel_executable.cpp
Normal file
230
core/fs/kernel_executable.cpp
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "core/file_sys/kernel_executable.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u32 INI_MAX_KIPS = 0x50;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool DecompressBLZ(std::vector<u8>& data) {
|
||||||
|
if (data.size() < 0xC)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto data_size = data.size() - 0xC;
|
||||||
|
|
||||||
|
u32 compressed_size{};
|
||||||
|
u32 init_index{};
|
||||||
|
u32 additional_size{};
|
||||||
|
std::memcpy(&compressed_size, data.data() + data_size, sizeof(u32));
|
||||||
|
std::memcpy(&init_index, data.data() + data_size + 0x4, sizeof(u32));
|
||||||
|
std::memcpy(&additional_size, data.data() + data_size + 0x8, sizeof(u32));
|
||||||
|
|
||||||
|
const auto start_offset = data.size() - compressed_size;
|
||||||
|
data.resize(compressed_size + additional_size + start_offset);
|
||||||
|
|
||||||
|
std::size_t index = compressed_size - init_index;
|
||||||
|
std::size_t out_index = compressed_size + additional_size;
|
||||||
|
|
||||||
|
while (out_index > 0) {
|
||||||
|
--index;
|
||||||
|
auto control = data[index + start_offset];
|
||||||
|
for (size_t i = 0; i < 8; ++i) {
|
||||||
|
if (((control << i) & 0x80) > 0) {
|
||||||
|
if (index < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
index -= 2;
|
||||||
|
std::size_t segment_offset =
|
||||||
|
data[index + start_offset] | data[index + start_offset + 1] << 8;
|
||||||
|
std::size_t segment_size = ((segment_offset >> 12) & 0xF) + 3;
|
||||||
|
segment_offset &= 0xFFF;
|
||||||
|
segment_offset += 3;
|
||||||
|
|
||||||
|
if (out_index < segment_size)
|
||||||
|
segment_size = out_index;
|
||||||
|
|
||||||
|
if (out_index < segment_size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_index -= segment_size;
|
||||||
|
|
||||||
|
for (size_t j = 0; j < segment_size; ++j) {
|
||||||
|
if (out_index + j + segment_offset + start_offset >= data.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data[out_index + j + start_offset] =
|
||||||
|
data[out_index + j + segment_offset + start_offset];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (out_index < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
--out_index;
|
||||||
|
--index;
|
||||||
|
data[out_index + start_offset] = data[index + start_offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_index == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
KIP::KIP(const VirtualFile& file) : status(Loader::ResultStatus::Success) {
|
||||||
|
if (file == nullptr) {
|
||||||
|
status = Loader::ResultStatus::ErrorNullFile;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file->GetSize() < sizeof(KIPHeader) || file->ReadObject(&header) != sizeof(KIPHeader)) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadKIPHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.magic != Common::MakeMagic('K', 'I', 'P', '1')) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadKIPHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 offset = sizeof(KIPHeader);
|
||||||
|
for (std::size_t i = 0; i < header.sections.size(); ++i) {
|
||||||
|
auto compressed = file->ReadBytes(header.sections[i].compressed_size, offset);
|
||||||
|
offset += header.sections[i].compressed_size;
|
||||||
|
|
||||||
|
if (header.sections[i].compressed_size == 0 && header.sections[i].decompressed_size != 0) {
|
||||||
|
decompressed_sections[i] = std::vector<u8>(header.sections[i].decompressed_size);
|
||||||
|
} else if (header.sections[i].compressed_size == header.sections[i].decompressed_size) {
|
||||||
|
decompressed_sections[i] = std::move(compressed);
|
||||||
|
} else {
|
||||||
|
decompressed_sections[i] = compressed;
|
||||||
|
if (!DecompressBLZ(decompressed_sections[i])) {
|
||||||
|
status = Loader::ResultStatus::ErrorBLZDecompressionFailed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus KIP::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string KIP::GetName() const {
|
||||||
|
return Common::StringFromFixedZeroTerminatedBuffer(header.name.data(), header.name.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 KIP::GetTitleID() const {
|
||||||
|
return header.title_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> KIP::GetSectionDecompressed(u8 index) const {
|
||||||
|
return decompressed_sections[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KIP::Is64Bit() const {
|
||||||
|
return (header.flags & 0x8) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KIP::Is39BitAddressSpace() const {
|
||||||
|
return (header.flags & 0x10) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KIP::IsService() const {
|
||||||
|
return (header.flags & 0x20) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u32> KIP::GetKernelCapabilities() const {
|
||||||
|
return std::vector<u32>(header.capabilities.begin(), header.capabilities.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 KIP::GetMainThreadPriority() const {
|
||||||
|
return static_cast<s32>(header.main_thread_priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetMainThreadStackSize() const {
|
||||||
|
return header.sections[1].attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetMainThreadCpuCore() const {
|
||||||
|
return header.default_core;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<u8>& KIP::GetTextSection() const {
|
||||||
|
return decompressed_sections[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<u8>& KIP::GetRODataSection() const {
|
||||||
|
return decompressed_sections[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<u8>& KIP::GetDataSection() const {
|
||||||
|
return decompressed_sections[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetTextOffset() const {
|
||||||
|
return header.sections[0].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetRODataOffset() const {
|
||||||
|
return header.sections[1].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetDataOffset() const {
|
||||||
|
return header.sections[2].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetBSSSize() const {
|
||||||
|
return header.sections[3].decompressed_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 KIP::GetBSSOffset() const {
|
||||||
|
return header.sections[3].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
INI::INI(const VirtualFile& file) : status(Loader::ResultStatus::Success) {
|
||||||
|
if (file->GetSize() < sizeof(INIHeader) || file->ReadObject(&header) != sizeof(INIHeader)) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadINIHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.magic != Common::MakeMagic('I', 'N', 'I', '1')) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadINIHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.kip_count > INI_MAX_KIPS) {
|
||||||
|
status = Loader::ResultStatus::ErrorINITooManyKIPs;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 offset = sizeof(INIHeader);
|
||||||
|
for (std::size_t i = 0; i < header.kip_count; ++i) {
|
||||||
|
const auto kip_file =
|
||||||
|
std::make_shared<OffsetVfsFile>(file, file->GetSize() - offset, offset);
|
||||||
|
KIP kip(kip_file);
|
||||||
|
if (kip.GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
kips.push_back(std::move(kip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus INI::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<KIP>& INI::GetKIPs() const {
|
||||||
|
return kips;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
106
core/fs/kernel_executable.h
Normal file
106
core/fs/kernel_executable.h
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
struct KIPSectionHeader {
|
||||||
|
u32_le offset;
|
||||||
|
u32_le decompressed_size;
|
||||||
|
u32_le compressed_size;
|
||||||
|
u32_le attribute;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(KIPSectionHeader) == 0x10, "KIPSectionHeader has incorrect size.");
|
||||||
|
|
||||||
|
struct KIPHeader {
|
||||||
|
u32_le magic;
|
||||||
|
std::array<char, 0xC> name;
|
||||||
|
u64_le title_id;
|
||||||
|
u32_le process_category;
|
||||||
|
u8 main_thread_priority;
|
||||||
|
u8 default_core;
|
||||||
|
INSERT_PADDING_BYTES(1);
|
||||||
|
u8 flags;
|
||||||
|
std::array<KIPSectionHeader, 6> sections;
|
||||||
|
std::array<u32, 0x20> capabilities;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(KIPHeader) == 0x100, "KIPHeader has incorrect size.");
|
||||||
|
|
||||||
|
struct INIHeader {
|
||||||
|
u32_le magic;
|
||||||
|
u32_le size;
|
||||||
|
u32_le kip_count;
|
||||||
|
INSERT_PADDING_BYTES(0x4);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(INIHeader) == 0x10, "INIHeader has incorrect size.");
|
||||||
|
|
||||||
|
// Kernel Internal Process
|
||||||
|
class KIP {
|
||||||
|
public:
|
||||||
|
explicit KIP(const VirtualFile& file);
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
|
||||||
|
std::string GetName() const;
|
||||||
|
u64 GetTitleID() const;
|
||||||
|
std::vector<u8> GetSectionDecompressed(u8 index) const;
|
||||||
|
|
||||||
|
// Executable Flags
|
||||||
|
bool Is64Bit() const;
|
||||||
|
bool Is39BitAddressSpace() const;
|
||||||
|
bool IsService() const;
|
||||||
|
|
||||||
|
std::vector<u32> GetKernelCapabilities() const;
|
||||||
|
|
||||||
|
s32 GetMainThreadPriority() const;
|
||||||
|
u32 GetMainThreadStackSize() const;
|
||||||
|
u32 GetMainThreadCpuCore() const;
|
||||||
|
|
||||||
|
const std::vector<u8>& GetTextSection() const;
|
||||||
|
const std::vector<u8>& GetRODataSection() const;
|
||||||
|
const std::vector<u8>& GetDataSection() const;
|
||||||
|
|
||||||
|
u32 GetTextOffset() const;
|
||||||
|
u32 GetRODataOffset() const;
|
||||||
|
u32 GetDataOffset() const;
|
||||||
|
|
||||||
|
u32 GetBSSSize() const;
|
||||||
|
u32 GetBSSOffset() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Loader::ResultStatus status;
|
||||||
|
|
||||||
|
KIPHeader header{};
|
||||||
|
std::array<std::vector<u8>, 6> decompressed_sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
class INI {
|
||||||
|
public:
|
||||||
|
explicit INI(const VirtualFile& file);
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
|
||||||
|
const std::vector<KIP>& GetKIPs() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Loader::ResultStatus status;
|
||||||
|
|
||||||
|
INIHeader header{};
|
||||||
|
std::vector<KIP> kips;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
128
core/fs/nca_metadata.cpp
Normal file
128
core/fs/nca_metadata.cpp
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
CNMT::CNMT(VirtualFile file) {
|
||||||
|
if (file->ReadObject(&header) != sizeof(CNMTHeader))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If type is {Application, Update, AOC} has opt-header.
|
||||||
|
if (header.type >= TitleType::Application && header.type <= TitleType::AOC) {
|
||||||
|
if (file->ReadObject(&opt_header, sizeof(CNMTHeader)) != sizeof(OptionalHeader)) {
|
||||||
|
LOG_WARNING(Loader, "Failed to read optional header.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (u16 i = 0; i < header.number_content_entries; ++i) {
|
||||||
|
auto& next = content_records.emplace_back(ContentRecord{});
|
||||||
|
if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(ContentRecord) +
|
||||||
|
header.table_offset) != sizeof(ContentRecord)) {
|
||||||
|
content_records.erase(content_records.end() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (u16 i = 0; i < header.number_meta_entries; ++i) {
|
||||||
|
auto& next = meta_records.emplace_back(MetaRecord{});
|
||||||
|
if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(MetaRecord) +
|
||||||
|
header.table_offset) != sizeof(MetaRecord)) {
|
||||||
|
meta_records.erase(meta_records.end() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CNMT::CNMT(CNMTHeader header_, OptionalHeader opt_header_,
|
||||||
|
std::vector<ContentRecord> content_records_, std::vector<MetaRecord> meta_records_)
|
||||||
|
: header(std::move(header_)), opt_header(std::move(opt_header_)),
|
||||||
|
content_records(std::move(content_records_)), meta_records(std::move(meta_records_)) {}
|
||||||
|
|
||||||
|
CNMT::~CNMT() = default;
|
||||||
|
|
||||||
|
const CNMTHeader& CNMT::GetHeader() const {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 CNMT::GetTitleID() const {
|
||||||
|
return header.title_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 CNMT::GetTitleVersion() const {
|
||||||
|
return header.title_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleType CNMT::GetType() const {
|
||||||
|
return header.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<ContentRecord>& CNMT::GetContentRecords() const {
|
||||||
|
return content_records;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<MetaRecord>& CNMT::GetMetaRecords() const {
|
||||||
|
return meta_records;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CNMT::UnionRecords(const CNMT& other) {
|
||||||
|
bool change = false;
|
||||||
|
for (const auto& rec : other.content_records) {
|
||||||
|
const auto iter = std::find_if(content_records.begin(), content_records.end(),
|
||||||
|
[&rec](const ContentRecord& r) {
|
||||||
|
return r.nca_id == rec.nca_id && r.type == rec.type;
|
||||||
|
});
|
||||||
|
if (iter == content_records.end()) {
|
||||||
|
content_records.emplace_back(rec);
|
||||||
|
++header.number_content_entries;
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& rec : other.meta_records) {
|
||||||
|
const auto iter =
|
||||||
|
std::find_if(meta_records.begin(), meta_records.end(), [&rec](const MetaRecord& r) {
|
||||||
|
return r.title_id == rec.title_id && r.title_version == rec.title_version &&
|
||||||
|
r.type == rec.type;
|
||||||
|
});
|
||||||
|
if (iter == meta_records.end()) {
|
||||||
|
meta_records.emplace_back(rec);
|
||||||
|
++header.number_meta_entries;
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> CNMT::Serialize() const {
|
||||||
|
const bool has_opt_header =
|
||||||
|
header.type >= TitleType::Application && header.type <= TitleType::AOC;
|
||||||
|
const auto dead_zone = header.table_offset + sizeof(CNMTHeader);
|
||||||
|
std::vector<u8> out(
|
||||||
|
std::max(sizeof(CNMTHeader) + (has_opt_header ? sizeof(OptionalHeader) : 0), dead_zone) +
|
||||||
|
content_records.size() * sizeof(ContentRecord) + meta_records.size() * sizeof(MetaRecord));
|
||||||
|
memcpy(out.data(), &header, sizeof(CNMTHeader));
|
||||||
|
|
||||||
|
// Optional Header
|
||||||
|
if (has_opt_header) {
|
||||||
|
memcpy(out.data() + sizeof(CNMTHeader), &opt_header, sizeof(OptionalHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
u64_le offset = header.table_offset;
|
||||||
|
|
||||||
|
for (const auto& rec : content_records) {
|
||||||
|
memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(ContentRecord));
|
||||||
|
offset += sizeof(ContentRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& rec : meta_records) {
|
||||||
|
memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(MetaRecord));
|
||||||
|
offset += sizeof(MetaRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
113
core/fs/nca_metadata.h
Normal file
113
core/fs/nca_metadata.h
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
class CNMT;
|
||||||
|
|
||||||
|
struct CNMTHeader;
|
||||||
|
struct OptionalHeader;
|
||||||
|
|
||||||
|
enum class TitleType : u8 {
|
||||||
|
SystemProgram = 0x01,
|
||||||
|
SystemDataArchive = 0x02,
|
||||||
|
SystemUpdate = 0x03,
|
||||||
|
FirmwarePackageA = 0x04,
|
||||||
|
FirmwarePackageB = 0x05,
|
||||||
|
Application = 0x80,
|
||||||
|
Update = 0x81,
|
||||||
|
AOC = 0x82,
|
||||||
|
DeltaTitle = 0x83,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ContentRecordType : u8 {
|
||||||
|
Meta = 0,
|
||||||
|
Program = 1,
|
||||||
|
Data = 2,
|
||||||
|
Control = 3,
|
||||||
|
HtmlDocument = 4,
|
||||||
|
LegalInformation = 5,
|
||||||
|
DeltaFragment = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ContentRecord {
|
||||||
|
std::array<u8, 0x20> hash;
|
||||||
|
std::array<u8, 0x10> nca_id;
|
||||||
|
std::array<u8, 0x6> size;
|
||||||
|
ContentRecordType type;
|
||||||
|
INSERT_PADDING_BYTES(1);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(ContentRecord) == 0x38, "ContentRecord has incorrect size.");
|
||||||
|
|
||||||
|
constexpr ContentRecord EMPTY_META_CONTENT_RECORD{{}, {}, {}, ContentRecordType::Meta, {}};
|
||||||
|
|
||||||
|
struct MetaRecord {
|
||||||
|
u64_le title_id;
|
||||||
|
u32_le title_version;
|
||||||
|
TitleType type;
|
||||||
|
u8 install_byte;
|
||||||
|
INSERT_PADDING_BYTES(2);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MetaRecord) == 0x10, "MetaRecord has incorrect size.");
|
||||||
|
|
||||||
|
struct OptionalHeader {
|
||||||
|
u64_le title_id;
|
||||||
|
u64_le minimum_version;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(OptionalHeader) == 0x10, "OptionalHeader has incorrect size.");
|
||||||
|
|
||||||
|
struct CNMTHeader {
|
||||||
|
u64_le title_id;
|
||||||
|
u32_le title_version;
|
||||||
|
TitleType type;
|
||||||
|
u8 reserved;
|
||||||
|
u16_le table_offset;
|
||||||
|
u16_le number_content_entries;
|
||||||
|
u16_le number_meta_entries;
|
||||||
|
u8 attributes;
|
||||||
|
std::array<u8, 2> reserved2;
|
||||||
|
u8 is_committed;
|
||||||
|
u32_le required_download_system_version;
|
||||||
|
std::array<u8, 4> reserved3;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CNMTHeader) == 0x20, "CNMTHeader has incorrect size.");
|
||||||
|
|
||||||
|
// A class representing the format used by NCA metadata files, typically named {}.cnmt.nca or
|
||||||
|
// meta0.ncd. These describe which NCA's belong with which titles in the registered cache.
|
||||||
|
class CNMT {
|
||||||
|
public:
|
||||||
|
explicit CNMT(VirtualFile file);
|
||||||
|
CNMT(CNMTHeader header_, OptionalHeader opt_header_,
|
||||||
|
std::vector<ContentRecord> content_records_, std::vector<MetaRecord> meta_records_);
|
||||||
|
~CNMT();
|
||||||
|
|
||||||
|
const CNMTHeader& GetHeader() const;
|
||||||
|
u64 GetTitleID() const;
|
||||||
|
u32 GetTitleVersion() const;
|
||||||
|
TitleType GetType() const;
|
||||||
|
|
||||||
|
const std::vector<ContentRecord>& GetContentRecords() const;
|
||||||
|
const std::vector<MetaRecord>& GetMetaRecords() const;
|
||||||
|
|
||||||
|
bool UnionRecords(const CNMT& other);
|
||||||
|
std::vector<u8> Serialize() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
CNMTHeader header;
|
||||||
|
OptionalHeader opt_header;
|
||||||
|
std::vector<ContentRecord> content_records;
|
||||||
|
std::vector<MetaRecord> meta_records;
|
||||||
|
|
||||||
|
// TODO(DarkLordZach): According to switchbrew, for Patch-type there is additional data
|
||||||
|
// after the table. This is not documented, unfortunately.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
116
core/fs/partition_filesystem.cpp
Normal file
116
core/fs/partition_filesystem.cpp
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/file_sys/partition_filesystem.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
bool PartitionFilesystem::Header::HasValidMagicValue() const {
|
||||||
|
return magic == Common::MakeMagic('H', 'F', 'S', '0') ||
|
||||||
|
magic == Common::MakeMagic('P', 'F', 'S', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
PartitionFilesystem::PartitionFilesystem(VirtualFile file) {
|
||||||
|
// At least be as large as the header
|
||||||
|
if (file->GetSize() < sizeof(Header)) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadPFSHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cartridges, HFSs can get very large, so we need to calculate the size up to
|
||||||
|
// the actual content itself instead of just blindly reading in the entire file.
|
||||||
|
if (sizeof(Header) != file->ReadObject(&pfs_header)) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadPFSHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pfs_header.HasValidMagicValue()) {
|
||||||
|
status = Loader::ResultStatus::ErrorBadPFSHeader;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_hfs = pfs_header.magic == Common::MakeMagic('H', 'F', 'S', '0');
|
||||||
|
|
||||||
|
std::size_t entry_size = is_hfs ? sizeof(HFSEntry) : sizeof(PFSEntry);
|
||||||
|
std::size_t metadata_size =
|
||||||
|
sizeof(Header) + (pfs_header.num_entries * entry_size) + pfs_header.strtab_size;
|
||||||
|
|
||||||
|
// Actually read in now...
|
||||||
|
std::vector<u8> file_data = file->ReadBytes(metadata_size);
|
||||||
|
const std::size_t total_size = file_data.size();
|
||||||
|
file_data.push_back(0);
|
||||||
|
|
||||||
|
if (total_size != metadata_size) {
|
||||||
|
status = Loader::ResultStatus::ErrorIncorrectPFSFileSize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t entries_offset = sizeof(Header);
|
||||||
|
std::size_t strtab_offset = entries_offset + (pfs_header.num_entries * entry_size);
|
||||||
|
content_offset = strtab_offset + pfs_header.strtab_size;
|
||||||
|
for (u16 i = 0; i < pfs_header.num_entries; i++) {
|
||||||
|
FSEntry entry;
|
||||||
|
|
||||||
|
memcpy(&entry, &file_data[entries_offset + (i * entry_size)], sizeof(FSEntry));
|
||||||
|
std::string name(
|
||||||
|
reinterpret_cast<const char*>(&file_data[strtab_offset + entry.strtab_offset]));
|
||||||
|
|
||||||
|
offsets.insert_or_assign(name, content_offset + entry.offset);
|
||||||
|
sizes.insert_or_assign(name, entry.size);
|
||||||
|
|
||||||
|
pfs_files.emplace_back(std::make_shared<OffsetVfsFile>(
|
||||||
|
file, entry.size, content_offset + entry.offset, std::move(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
status = Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
PartitionFilesystem::~PartitionFilesystem() = default;
|
||||||
|
|
||||||
|
Loader::ResultStatus PartitionFilesystem::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, u64> PartitionFilesystem::GetFileOffsets() const {
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, u64> PartitionFilesystem::GetFileSizes() const {
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualFile> PartitionFilesystem::GetFiles() const {
|
||||||
|
return pfs_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualDir> PartitionFilesystem::GetSubdirectories() const {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PartitionFilesystem::GetName() const {
|
||||||
|
return is_hfs ? "HFS0" : "PFS0";
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir PartitionFilesystem::GetParentDirectory() const {
|
||||||
|
// TODO(DarkLordZach): Add support for nested containers.
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PartitionFilesystem::PrintDebugInfo() const {
|
||||||
|
LOG_DEBUG(Service_FS, "Magic: {:.4}", pfs_header.magic);
|
||||||
|
LOG_DEBUG(Service_FS, "Files: {}", pfs_header.num_entries);
|
||||||
|
for (u32 i = 0; i < pfs_header.num_entries; i++) {
|
||||||
|
LOG_DEBUG(Service_FS, " > File {}: {} (0x{:X} bytes)", i,
|
||||||
|
pfs_files[i]->GetName(), pfs_files[i]->GetSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
91
core/fs/partition_filesystem.h
Normal file
91
core/fs/partition_filesystem.h
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper which implements an interface to parse PFS/HFS filesystems.
|
||||||
|
* Data can either be loaded from a file path or data with an offset into it.
|
||||||
|
*/
|
||||||
|
class PartitionFilesystem : public ReadOnlyVfsDirectory {
|
||||||
|
public:
|
||||||
|
explicit PartitionFilesystem(VirtualFile file);
|
||||||
|
~PartitionFilesystem() override;
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
|
||||||
|
std::map<std::string, u64> GetFileOffsets() const;
|
||||||
|
std::map<std::string, u64> GetFileSizes() const;
|
||||||
|
|
||||||
|
std::vector<VirtualFile> GetFiles() const override;
|
||||||
|
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||||
|
std::string GetName() const override;
|
||||||
|
VirtualDir GetParentDirectory() const override;
|
||||||
|
void PrintDebugInfo() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Header {
|
||||||
|
u32_le magic;
|
||||||
|
u32_le num_entries;
|
||||||
|
u32_le strtab_size;
|
||||||
|
INSERT_PADDING_BYTES(0x4);
|
||||||
|
|
||||||
|
bool HasValidMagicValue() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(Header) == 0x10, "PFS/HFS header structure size is wrong");
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct FSEntry {
|
||||||
|
u64_le offset;
|
||||||
|
u64_le size;
|
||||||
|
u32_le strtab_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(FSEntry) == 0x14, "FS entry structure size is wrong");
|
||||||
|
|
||||||
|
struct PFSEntry {
|
||||||
|
FSEntry fs_entry;
|
||||||
|
INSERT_PADDING_BYTES(0x4);
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(PFSEntry) == 0x18, "PFS entry structure size is wrong");
|
||||||
|
|
||||||
|
struct HFSEntry {
|
||||||
|
FSEntry fs_entry;
|
||||||
|
u32_le hash_region_size;
|
||||||
|
INSERT_PADDING_BYTES(0x8);
|
||||||
|
std::array<char, 0x20> hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(HFSEntry) == 0x40, "HFS entry structure size is wrong");
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
Loader::ResultStatus status{};
|
||||||
|
|
||||||
|
Header pfs_header{};
|
||||||
|
bool is_hfs = false;
|
||||||
|
std::size_t content_offset = 0;
|
||||||
|
|
||||||
|
std::map<std::string, u64> offsets;
|
||||||
|
std::map<std::string, u64> sizes;
|
||||||
|
|
||||||
|
std::vector<VirtualFile> pfs_files;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
696
core/fs/patch_manager.cpp
Normal file
696
core/fs/patch_manager.cpp
Normal file
|
|
@ -0,0 +1,696 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "common/hex_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/settings.h"
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/file_sys/common_funcs.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/control_metadata.h"
|
||||||
|
#include "core/file_sys/ips_layer.h"
|
||||||
|
#include "core/file_sys/patch_manager.h"
|
||||||
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_cached.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_layered.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
#include "core/hle/service/ns/language.h"
|
||||||
|
#include "core/hle/service/set/settings_server.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
#include "core/loader/nso.h"
|
||||||
|
#include "core/memory/cheat_engine.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr u32 SINGLE_BYTE_MODULUS = 0x100;
|
||||||
|
|
||||||
|
constexpr std::array<const char*, 14> EXEFS_FILE_NAMES{
|
||||||
|
"main", "main.npdm", "rtld", "sdk", "subsdk0", "subsdk1", "subsdk2",
|
||||||
|
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9",
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TitleVersionFormat : u8 {
|
||||||
|
ThreeElements, ///< vX.Y.Z
|
||||||
|
FourElements, ///< vX.Y.Z.W
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string FormatTitleVersion(u32 version,
|
||||||
|
TitleVersionFormat format = TitleVersionFormat::ThreeElements) {
|
||||||
|
std::array<u8, sizeof(u32)> bytes{};
|
||||||
|
bytes[0] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
|
||||||
|
for (std::size_t i = 1; i < bytes.size(); ++i) {
|
||||||
|
version /= SINGLE_BYTE_MODULUS;
|
||||||
|
bytes[i] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == TitleVersionFormat::FourElements) {
|
||||||
|
return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
|
||||||
|
}
|
||||||
|
return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a directory with name matching name case-insensitive. Returns nullptr if directory
|
||||||
|
// doesn't have a directory with name.
|
||||||
|
VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return dir->GetSubdirectory(name);
|
||||||
|
#else
|
||||||
|
const auto subdirs = dir->GetSubdirectories();
|
||||||
|
for (const auto& subdir : subdirs) {
|
||||||
|
std::string dir_name = Common::ToLower(subdir->GetName());
|
||||||
|
if (dir_name == name) {
|
||||||
|
return subdir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder(
|
||||||
|
u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) {
|
||||||
|
const auto build_id_raw = Common::HexToString(build_id_, upper);
|
||||||
|
const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
|
||||||
|
const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
|
||||||
|
|
||||||
|
if (file == nullptr) {
|
||||||
|
LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
|
||||||
|
title_id, build_id);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> data(file->GetSize());
|
||||||
|
if (file->Read(data.data(), data.size()) != data.size()) {
|
||||||
|
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
|
||||||
|
title_id, build_id);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Core::Memory::TextCheatParser parser;
|
||||||
|
return parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
|
||||||
|
if (to.empty()) {
|
||||||
|
to += with;
|
||||||
|
} else {
|
||||||
|
to += ", ";
|
||||||
|
to += with;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
|
||||||
|
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
|
||||||
|
}
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
PatchManager::PatchManager(u64 title_id_,
|
||||||
|
const Service::FileSystem::FileSystemController& fs_controller_,
|
||||||
|
const ContentProvider& content_provider_)
|
||||||
|
: title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {}
|
||||||
|
|
||||||
|
PatchManager::~PatchManager() = default;
|
||||||
|
|
||||||
|
u64 PatchManager::GetTitleID() const {
|
||||||
|
return title_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
||||||
|
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
|
||||||
|
|
||||||
|
if (exefs == nullptr)
|
||||||
|
return exefs;
|
||||||
|
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
const auto update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
|
||||||
|
// Game Updates
|
||||||
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
|
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
||||||
|
|
||||||
|
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
|
||||||
|
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
|
||||||
|
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
||||||
|
exefs = update->GetExeFS();
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayeredExeFS
|
||||||
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
||||||
|
|
||||||
|
std::vector<VirtualDir> patch_dirs = {sdmc_load_dir};
|
||||||
|
if (load_dir != nullptr) {
|
||||||
|
const auto load_patch_dirs = load_dir->GetSubdirectories();
|
||||||
|
patch_dirs.insert(patch_dirs.end(), load_patch_dirs.begin(), load_patch_dirs.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||||
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
||||||
|
|
||||||
|
std::vector<VirtualDir> layers;
|
||||||
|
layers.reserve(patch_dirs.size() + 1);
|
||||||
|
for (const auto& subdir : patch_dirs) {
|
||||||
|
if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
|
||||||
|
if (exefs_dir != nullptr)
|
||||||
|
layers.push_back(std::move(exefs_dir));
|
||||||
|
}
|
||||||
|
layers.push_back(exefs);
|
||||||
|
|
||||||
|
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
|
||||||
|
if (layered != nullptr) {
|
||||||
|
LOG_INFO(Loader, " ExeFS: LayeredExeFS patches applied successfully");
|
||||||
|
exefs = std::move(layered);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.dump_exefs) {
|
||||||
|
LOG_INFO(Loader, "Dumping ExeFS for title_id={:016X}", title_id);
|
||||||
|
const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
|
||||||
|
if (dump_dir != nullptr) {
|
||||||
|
const auto exefs_dir = GetOrCreateDirectoryRelative(dump_dir, "/exefs");
|
||||||
|
VfsRawCopyD(exefs, exefs_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
|
||||||
|
const std::string& build_id) const {
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
const auto nso_build_id = fmt::format("{:0<64}", build_id);
|
||||||
|
|
||||||
|
std::vector<VirtualFile> out;
|
||||||
|
out.reserve(patch_dirs.size());
|
||||||
|
for (const auto& subdir : patch_dirs) {
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
|
||||||
|
if (exefs_dir != nullptr) {
|
||||||
|
for (const auto& file : exefs_dir->GetFiles()) {
|
||||||
|
if (file->GetExtension() == "ips") {
|
||||||
|
auto name = file->GetName();
|
||||||
|
|
||||||
|
const auto this_build_id =
|
||||||
|
fmt::format("{:0<64}", name.substr(0, name.find('.')));
|
||||||
|
if (nso_build_id == this_build_id)
|
||||||
|
out.push_back(file);
|
||||||
|
} else if (file->GetExtension() == "pchtxt") {
|
||||||
|
IPSwitchCompiler compiler{file};
|
||||||
|
if (!compiler.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto this_build_id = Common::HexToString(compiler.GetBuildID());
|
||||||
|
if (nso_build_id == this_build_id)
|
||||||
|
out.push_back(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::string& name) const {
|
||||||
|
if (nso.size() < sizeof(Loader::NSOHeader)) {
|
||||||
|
return nso;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::NSOHeader header;
|
||||||
|
std::memcpy(&header, nso.data(), sizeof(header));
|
||||||
|
|
||||||
|
if (header.magic != Common::MakeMagic('N', 'S', 'O', '0')) {
|
||||||
|
return nso;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto build_id_raw = Common::HexToString(header.build_id);
|
||||||
|
const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
|
||||||
|
|
||||||
|
if (Settings::values.dump_nso) {
|
||||||
|
LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id,
|
||||||
|
title_id);
|
||||||
|
const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
|
||||||
|
if (dump_dir != nullptr) {
|
||||||
|
const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso");
|
||||||
|
const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id));
|
||||||
|
|
||||||
|
file->Resize(nso.size());
|
||||||
|
file->WriteBytes(nso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id);
|
||||||
|
|
||||||
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
if (load_dir == nullptr) {
|
||||||
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
||||||
|
return nso;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto patch_dirs = load_dir->GetSubdirectories();
|
||||||
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||||
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
||||||
|
const auto patches = CollectPatches(patch_dirs, build_id);
|
||||||
|
|
||||||
|
auto out = nso;
|
||||||
|
for (const auto& patch_file : patches) {
|
||||||
|
if (patch_file->GetExtension() == "ips") {
|
||||||
|
LOG_INFO(Loader, " - Applying IPS patch from mod \"{}\"",
|
||||||
|
patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
|
||||||
|
const auto patched = PatchIPS(std::make_shared<VectorVfsFile>(out), patch_file);
|
||||||
|
if (patched != nullptr)
|
||||||
|
out = patched->ReadAllBytes();
|
||||||
|
} else if (patch_file->GetExtension() == "pchtxt") {
|
||||||
|
LOG_INFO(Loader, " - Applying IPSwitch patch from mod \"{}\"",
|
||||||
|
patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
|
||||||
|
const IPSwitchCompiler compiler{patch_file};
|
||||||
|
const auto patched = compiler.Apply(std::make_shared<VectorVfsFile>(out));
|
||||||
|
if (patched != nullptr)
|
||||||
|
out = patched->ReadAllBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.size() < sizeof(Loader::NSOHeader)) {
|
||||||
|
return nso;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(out.data(), &header, sizeof(header));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) const {
|
||||||
|
const auto build_id_raw = Common::HexToString(build_id_);
|
||||||
|
const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
|
||||||
|
|
||||||
|
LOG_INFO(Loader, "Querying NSO patch existence for build_id={}, name={}", build_id, name);
|
||||||
|
|
||||||
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
if (load_dir == nullptr) {
|
||||||
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto patch_dirs = load_dir->GetSubdirectories();
|
||||||
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||||
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
||||||
|
|
||||||
|
return !CollectPatches(patch_dirs, build_id).empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(
|
||||||
|
const BuildID& build_id_) const {
|
||||||
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
if (load_dir == nullptr) {
|
||||||
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
auto patch_dirs = load_dir->GetSubdirectories();
|
||||||
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||||
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
||||||
|
|
||||||
|
std::vector<Core::Memory::CheatEntry> out;
|
||||||
|
for (const auto& subdir : patch_dirs) {
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats");
|
||||||
|
if (cheats_dir != nullptr) {
|
||||||
|
if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
|
||||||
|
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
|
||||||
|
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type,
|
||||||
|
const Service::FileSystem::FileSystemController& fs_controller) {
|
||||||
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
||||||
|
if ((type != ContentRecordType::Program && type != ContentRecordType::Data &&
|
||||||
|
type != ContentRecordType::HtmlDocument) ||
|
||||||
|
(load_dir == nullptr && sdmc_load_dir == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
std::vector<VirtualDir> patch_dirs = load_dir->GetSubdirectories();
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), "SDMC") == disabled.cend()) {
|
||||||
|
patch_dirs.push_back(sdmc_load_dir);
|
||||||
|
}
|
||||||
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||||
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
||||||
|
|
||||||
|
std::vector<VirtualDir> layers;
|
||||||
|
std::vector<VirtualDir> layers_ext;
|
||||||
|
layers.reserve(patch_dirs.size() + 1);
|
||||||
|
layers_ext.reserve(patch_dirs.size() + 1);
|
||||||
|
for (const auto& subdir : patch_dirs) {
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs");
|
||||||
|
if (romfs_dir != nullptr)
|
||||||
|
layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(romfs_dir)));
|
||||||
|
|
||||||
|
auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext");
|
||||||
|
if (ext_dir != nullptr)
|
||||||
|
layers_ext.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(ext_dir)));
|
||||||
|
|
||||||
|
if (type == ContentRecordType::HtmlDocument) {
|
||||||
|
auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html");
|
||||||
|
if (manual_dir != nullptr)
|
||||||
|
layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(manual_dir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there are no layers to apply, return early as there is no need to rebuild the RomFS
|
||||||
|
if (layers.empty() && layers_ext.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extracted = ExtractRomFS(romfs);
|
||||||
|
if (extracted == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.emplace_back(std::move(extracted));
|
||||||
|
|
||||||
|
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
|
||||||
|
if (layered == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
|
||||||
|
|
||||||
|
auto packed = CreateRomFS(std::move(layered), std::move(layered_ext));
|
||||||
|
if (packed == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Loader, " RomFS: LayeredFS patches applied successfully");
|
||||||
|
romfs = std::move(packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs,
|
||||||
|
ContentRecordType type, VirtualFile packed_update_raw,
|
||||||
|
bool apply_layeredfs) const {
|
||||||
|
const auto log_string = fmt::format("Patching RomFS for title_id={:016X}, type={:02X}",
|
||||||
|
title_id, static_cast<u8>(type));
|
||||||
|
if (type == ContentRecordType::Program || type == ContentRecordType::Data) {
|
||||||
|
LOG_INFO(Loader, "{}", log_string);
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG(Loader, "{}", log_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto romfs = base_romfs;
|
||||||
|
|
||||||
|
// Game Updates
|
||||||
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
|
const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
|
||||||
|
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
const auto update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
|
||||||
|
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
|
||||||
|
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
|
||||||
|
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||||
|
new_nca->GetRomFS() != nullptr) {
|
||||||
|
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
|
||||||
|
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
||||||
|
romfs = new_nca->GetRomFS();
|
||||||
|
const auto version =
|
||||||
|
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0));
|
||||||
|
}
|
||||||
|
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
|
||||||
|
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
|
||||||
|
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||||
|
new_nca->GetRomFS() != nullptr) {
|
||||||
|
LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully");
|
||||||
|
romfs = new_nca->GetRomFS();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayeredFS
|
||||||
|
if (apply_layeredfs) {
|
||||||
|
ApplyLayeredFS(romfs, title_id, type, fs_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
return romfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||||
|
if (title_id == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Patch> out;
|
||||||
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
|
||||||
|
// Game Updates
|
||||||
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
|
PatchManager update{update_tid, fs_controller, content_provider};
|
||||||
|
const auto metadata = update.GetControlMetadata();
|
||||||
|
const auto& nacp = metadata.first;
|
||||||
|
|
||||||
|
const auto update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
.name = "Update",
|
||||||
|
.version = "",
|
||||||
|
.type = PatchType::Update,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = title_id};
|
||||||
|
|
||||||
|
if (nacp != nullptr) {
|
||||||
|
update_patch.version = nacp->GetVersionString();
|
||||||
|
out.push_back(update_patch);
|
||||||
|
} else {
|
||||||
|
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
||||||
|
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
||||||
|
if (meta_ver.value_or(0) == 0) {
|
||||||
|
out.push_back(update_patch);
|
||||||
|
} else {
|
||||||
|
update_patch.version = FormatTitleVersion(*meta_ver);
|
||||||
|
out.push_back(update_patch);
|
||||||
|
}
|
||||||
|
} else if (update_raw != nullptr) {
|
||||||
|
update_patch.version = "PACKED";
|
||||||
|
out.push_back(update_patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// General Mods (LayeredFS and IPS)
|
||||||
|
const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||||
|
if (mod_dir != nullptr) {
|
||||||
|
for (const auto& mod : mod_dir->GetSubdirectories()) {
|
||||||
|
std::string types;
|
||||||
|
|
||||||
|
const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs");
|
||||||
|
if (IsDirValidAndNonEmpty(exefs_dir)) {
|
||||||
|
bool ips = false;
|
||||||
|
bool ipswitch = false;
|
||||||
|
bool layeredfs = false;
|
||||||
|
|
||||||
|
for (const auto& file : exefs_dir->GetFiles()) {
|
||||||
|
if (file->GetExtension() == "ips") {
|
||||||
|
ips = true;
|
||||||
|
} else if (file->GetExtension() == "pchtxt") {
|
||||||
|
ipswitch = true;
|
||||||
|
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
|
||||||
|
file->GetName()) != EXEFS_FILE_NAMES.end()) {
|
||||||
|
layeredfs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ips)
|
||||||
|
AppendCommaIfNotEmpty(types, "IPS");
|
||||||
|
if (ipswitch)
|
||||||
|
AppendCommaIfNotEmpty(types, "IPSwitch");
|
||||||
|
if (layeredfs)
|
||||||
|
AppendCommaIfNotEmpty(types, "LayeredExeFS");
|
||||||
|
}
|
||||||
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")))
|
||||||
|
AppendCommaIfNotEmpty(types, "LayeredFS");
|
||||||
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats")))
|
||||||
|
AppendCommaIfNotEmpty(types, "Cheats");
|
||||||
|
|
||||||
|
if (types.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto mod_disabled =
|
||||||
|
std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
|
||||||
|
out.push_back({.enabled = !mod_disabled,
|
||||||
|
.name = mod->GetName(),
|
||||||
|
.version = types,
|
||||||
|
.type = PatchType::Mod,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = title_id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDMC mod directory (RomFS LayeredFS)
|
||||||
|
const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
||||||
|
if (sdmc_mod_dir != nullptr) {
|
||||||
|
std::string types;
|
||||||
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs"))) {
|
||||||
|
AppendCommaIfNotEmpty(types, "LayeredExeFS");
|
||||||
|
}
|
||||||
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs"))) {
|
||||||
|
AppendCommaIfNotEmpty(types, "LayeredFS");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!types.empty()) {
|
||||||
|
const auto mod_disabled =
|
||||||
|
std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
|
||||||
|
out.push_back({.enabled = !mod_disabled,
|
||||||
|
.name = "SDMC",
|
||||||
|
.version = types,
|
||||||
|
.type = PatchType::Mod,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = title_id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DLC
|
||||||
|
const auto dlc_entries =
|
||||||
|
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
|
||||||
|
std::vector<ContentProviderEntry> dlc_match;
|
||||||
|
dlc_match.reserve(dlc_entries.size());
|
||||||
|
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
|
||||||
|
[this](const ContentProviderEntry& entry) {
|
||||||
|
return GetBaseTitleID(entry.title_id) == title_id &&
|
||||||
|
content_provider.GetEntry(entry)->GetStatus() ==
|
||||||
|
Loader::ResultStatus::Success;
|
||||||
|
});
|
||||||
|
if (!dlc_match.empty()) {
|
||||||
|
// Ensure sorted so DLC IDs show in order.
|
||||||
|
std::sort(dlc_match.begin(), dlc_match.end());
|
||||||
|
|
||||||
|
std::string list;
|
||||||
|
for (size_t i = 0; i < dlc_match.size() - 1; ++i)
|
||||||
|
list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF);
|
||||||
|
|
||||||
|
list += fmt::format("{}", dlc_match.back().title_id & 0x7FF);
|
||||||
|
|
||||||
|
const auto dlc_disabled =
|
||||||
|
std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
|
||||||
|
out.push_back({.enabled = !dlc_disabled,
|
||||||
|
.name = "DLC",
|
||||||
|
.version = std::move(list),
|
||||||
|
.type = PatchType::DLC,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = dlc_match.back().title_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<u32> PatchManager::GetGameVersion() const {
|
||||||
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
|
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
||||||
|
return content_provider.GetEntryVersion(update_tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content_provider.GetEntryVersion(title_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchManager::Metadata PatchManager::GetControlMetadata() const {
|
||||||
|
const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
|
||||||
|
if (base_control_nca == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseControlNCA(*base_control_nca);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const {
|
||||||
|
const auto base_romfs = nca.GetRomFS();
|
||||||
|
if (base_romfs == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto romfs = PatchRomFS(&nca, base_romfs, ContentRecordType::Control);
|
||||||
|
if (romfs == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto extracted = ExtractRomFS(romfs);
|
||||||
|
if (extracted == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (nacp_file == nullptr) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file);
|
||||||
|
|
||||||
|
// Get language code from settings
|
||||||
|
const auto language_code = Service::Set::GetLanguageCodeFromIndex(
|
||||||
|
static_cast<u32>(Settings::values.language_index.GetValue()));
|
||||||
|
|
||||||
|
// Convert to application language and get priority list
|
||||||
|
const auto application_language =
|
||||||
|
Service::NS::ConvertToApplicationLanguage(language_code)
|
||||||
|
.value_or(Service::NS::ApplicationLanguage::AmericanEnglish);
|
||||||
|
const auto language_priority_list =
|
||||||
|
Service::NS::GetApplicationLanguagePriorityList(application_language);
|
||||||
|
|
||||||
|
// Convert to language names
|
||||||
|
auto priority_language_names = FileSys::LANGUAGE_NAMES; // Copy
|
||||||
|
if (language_priority_list) {
|
||||||
|
for (size_t i = 0; i < priority_language_names.size(); ++i) {
|
||||||
|
// Relies on FileSys::LANGUAGE_NAMES being in the same order as
|
||||||
|
// Service::NS::ApplicationLanguage
|
||||||
|
const auto language_index = static_cast<u8>(language_priority_list->at(i));
|
||||||
|
|
||||||
|
if (language_index < FileSys::LANGUAGE_NAMES.size()) {
|
||||||
|
priority_language_names[i] = FileSys::LANGUAGE_NAMES[language_index];
|
||||||
|
} else {
|
||||||
|
// Not a catastrophe, unlikely to happen
|
||||||
|
LOG_WARNING(Loader, "Invalid language index {}", language_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first matching icon
|
||||||
|
VirtualFile icon_file;
|
||||||
|
for (const auto& language : priority_language_names) {
|
||||||
|
icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat"));
|
||||||
|
if (icon_file != nullptr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {std::move(nacp), icon_file};
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
103
core/fs/patch_manager.h
Normal file
103
core/fs/patch_manager.h
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
#include "core/memory/dmnt_cheat_types.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Service::FileSystem {
|
||||||
|
class FileSystemController;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class ContentProvider;
|
||||||
|
class NCA;
|
||||||
|
class NACP;
|
||||||
|
|
||||||
|
enum class PatchType { Update, DLC, Mod };
|
||||||
|
|
||||||
|
struct Patch {
|
||||||
|
bool enabled;
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
PatchType type;
|
||||||
|
u64 program_id;
|
||||||
|
u64 title_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A centralized class to manage patches to games.
|
||||||
|
class PatchManager {
|
||||||
|
public:
|
||||||
|
using BuildID = std::array<u8, 0x20>;
|
||||||
|
using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>;
|
||||||
|
|
||||||
|
explicit PatchManager(u64 title_id_,
|
||||||
|
const Service::FileSystem::FileSystemController& fs_controller_,
|
||||||
|
const ContentProvider& content_provider_);
|
||||||
|
~PatchManager();
|
||||||
|
|
||||||
|
[[nodiscard]] u64 GetTitleID() const;
|
||||||
|
|
||||||
|
// Currently tracked ExeFS patches:
|
||||||
|
// - Game Updates
|
||||||
|
[[nodiscard]] VirtualDir PatchExeFS(VirtualDir exefs) const;
|
||||||
|
|
||||||
|
// Currently tracked NSO patches:
|
||||||
|
// - IPS
|
||||||
|
// - IPSwitch
|
||||||
|
[[nodiscard]] std::vector<u8> PatchNSO(const std::vector<u8>& nso,
|
||||||
|
const std::string& name) const;
|
||||||
|
|
||||||
|
// Checks to see if PatchNSO() will have any effect given the NSO's build ID.
|
||||||
|
// Used to prevent expensive copies in NSO loader.
|
||||||
|
[[nodiscard]] bool HasNSOPatch(const BuildID& build_id, std::string_view name) const;
|
||||||
|
|
||||||
|
// Creates a CheatList object with all
|
||||||
|
[[nodiscard]] std::vector<Core::Memory::CheatEntry> CreateCheatList(
|
||||||
|
const BuildID& build_id) const;
|
||||||
|
|
||||||
|
// Currently tracked RomFS patches:
|
||||||
|
// - Game Updates
|
||||||
|
// - LayeredFS
|
||||||
|
[[nodiscard]] VirtualFile PatchRomFS(const NCA* base_nca, VirtualFile base_romfs,
|
||||||
|
ContentRecordType type = ContentRecordType::Program,
|
||||||
|
VirtualFile packed_update_raw = nullptr,
|
||||||
|
bool apply_layeredfs = true) const;
|
||||||
|
|
||||||
|
// Returns a vector of patches
|
||||||
|
[[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const;
|
||||||
|
|
||||||
|
// If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails,
|
||||||
|
// it will fallback to the Meta-type NCA of the base game. If that fails, the result will be
|
||||||
|
// std::nullopt
|
||||||
|
[[nodiscard]] std::optional<u32> GetGameVersion() const;
|
||||||
|
|
||||||
|
// Given title_id of the program, attempts to get the control data of the update and parse
|
||||||
|
// it, falling back to the base control data.
|
||||||
|
[[nodiscard]] Metadata GetControlMetadata() const;
|
||||||
|
|
||||||
|
// Version of GetControlMetadata that takes an arbitrary NCA
|
||||||
|
[[nodiscard]] Metadata ParseControlNCA(const NCA& nca) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs,
|
||||||
|
const std::string& build_id) const;
|
||||||
|
|
||||||
|
u64 title_id;
|
||||||
|
const Service::FileSystem::FileSystemController& fs_controller;
|
||||||
|
const ContentProvider& content_provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
220
core/fs/program_metadata.cpp
Normal file
220
core/fs/program_metadata.cpp
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "core/file_sys/program_metadata.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
ProgramMetadata::ProgramMetadata() = default;
|
||||||
|
|
||||||
|
ProgramMetadata::~ProgramMetadata() = default;
|
||||||
|
|
||||||
|
Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
|
||||||
|
const std::size_t total_size = file->GetSize();
|
||||||
|
if (total_size < sizeof(Header)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadNPDMHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeof(Header) != file->ReadObject(&npdm_header)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadNPDMHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeof(AcidHeader) != file->ReadObject(&acid_header, npdm_header.acid_offset)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadACIDHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeof(AciHeader) != file->ReadObject(&aci_header, npdm_header.aci_offset)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadACIHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load acid_file_access per-component instead of the entire struct, since this struct does not
|
||||||
|
// reflect the layout of the real data.
|
||||||
|
std::size_t current_offset = acid_header.fac_offset;
|
||||||
|
if (sizeof(FileAccessControl::version) != file->ReadBytes(&acid_file_access.version,
|
||||||
|
sizeof(FileAccessControl::version),
|
||||||
|
current_offset)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessControl;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessControl::permissions) !=
|
||||||
|
file->ReadBytes(&acid_file_access.permissions, sizeof(FileAccessControl::permissions),
|
||||||
|
current_offset += sizeof(FileAccessControl::version) + 3)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessControl;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessControl::unknown) !=
|
||||||
|
file->ReadBytes(&acid_file_access.unknown, sizeof(FileAccessControl::unknown),
|
||||||
|
current_offset + sizeof(FileAccessControl::permissions))) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load aci_file_access per-component instead of the entire struct, same as acid_file_access
|
||||||
|
current_offset = aci_header.fah_offset;
|
||||||
|
if (sizeof(FileAccessHeader::version) != file->ReadBytes(&aci_file_access.version,
|
||||||
|
sizeof(FileAccessHeader::version),
|
||||||
|
current_offset)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessHeader::permissions) !=
|
||||||
|
file->ReadBytes(&aci_file_access.permissions, sizeof(FileAccessHeader::permissions),
|
||||||
|
current_offset += sizeof(FileAccessHeader::version) + 3)) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessHeader::unk_offset) !=
|
||||||
|
file->ReadBytes(&aci_file_access.unk_offset, sizeof(FileAccessHeader::unk_offset),
|
||||||
|
current_offset += sizeof(FileAccessHeader::permissions))) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessHeader::unk_size) !=
|
||||||
|
file->ReadBytes(&aci_file_access.unk_size, sizeof(FileAccessHeader::unk_size),
|
||||||
|
current_offset += sizeof(FileAccessHeader::unk_offset))) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessHeader::unk_offset_2) !=
|
||||||
|
file->ReadBytes(&aci_file_access.unk_offset_2, sizeof(FileAccessHeader::unk_offset_2),
|
||||||
|
current_offset += sizeof(FileAccessHeader::unk_size))) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
if (sizeof(FileAccessHeader::unk_size_2) !=
|
||||||
|
file->ReadBytes(&aci_file_access.unk_size_2, sizeof(FileAccessHeader::unk_size_2),
|
||||||
|
current_offset + sizeof(FileAccessHeader::unk_offset_2))) {
|
||||||
|
return Loader::ResultStatus::ErrorBadFileAccessHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
aci_kernel_capabilities.resize(aci_header.kac_size / sizeof(u32));
|
||||||
|
const u64 read_size = aci_header.kac_size;
|
||||||
|
const u64 read_offset = npdm_header.aci_offset + aci_header.kac_offset;
|
||||||
|
if (file->ReadBytes(aci_kernel_capabilities.data(), read_size, read_offset) != read_size) {
|
||||||
|
return Loader::ResultStatus::ErrorBadKernelCapabilityDescriptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus ProgramMetadata::Reload(VirtualFile file) {
|
||||||
|
const u64 original_program_id = aci_header.title_id;
|
||||||
|
SCOPE_EXIT {
|
||||||
|
aci_header.title_id = original_program_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
return this->Load(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*static*/ ProgramMetadata ProgramMetadata::GetDefault() {
|
||||||
|
// Allow use of cores 0~3 and thread priorities 16~63.
|
||||||
|
constexpr u32 default_thread_info_capability = 0x30043F7;
|
||||||
|
|
||||||
|
ProgramMetadata result;
|
||||||
|
|
||||||
|
result.LoadManual(
|
||||||
|
true /*is_64_bit*/, FileSys::ProgramAddressSpaceType::Is39Bit /*address_space*/,
|
||||||
|
0x2c /*main_thread_prio*/, 0 /*main_thread_core*/, 0x100000 /*main_thread_stack_size*/,
|
||||||
|
0 /*title_id*/, 0xFFFFFFFFFFFFFFFF /*filesystem_permissions*/, 0 /*system_resource_size*/,
|
||||||
|
{default_thread_info_capability} /*capabilities*/);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramMetadata::LoadManual(bool is_64_bit, ProgramAddressSpaceType address_space,
|
||||||
|
s32 main_thread_prio, u32 main_thread_core,
|
||||||
|
u32 main_thread_stack_size, u64 title_id,
|
||||||
|
u64 filesystem_permissions, u32 system_resource_size,
|
||||||
|
KernelCapabilityDescriptors capabilities) {
|
||||||
|
npdm_header.has_64_bit_instructions.Assign(is_64_bit);
|
||||||
|
npdm_header.address_space_type.Assign(address_space);
|
||||||
|
npdm_header.main_thread_priority = static_cast<u8>(main_thread_prio);
|
||||||
|
npdm_header.main_thread_cpu = static_cast<u8>(main_thread_core);
|
||||||
|
npdm_header.main_stack_size = main_thread_stack_size;
|
||||||
|
aci_header.title_id = title_id;
|
||||||
|
aci_file_access.permissions = filesystem_permissions;
|
||||||
|
npdm_header.system_resource_size = system_resource_size;
|
||||||
|
aci_kernel_capabilities = std::move(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProgramMetadata::Is64BitProgram() const {
|
||||||
|
return npdm_header.has_64_bit_instructions.As<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramAddressSpaceType ProgramMetadata::GetAddressSpaceType() const {
|
||||||
|
return npdm_header.address_space_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 ProgramMetadata::GetMainThreadPriority() const {
|
||||||
|
return npdm_header.main_thread_priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 ProgramMetadata::GetMainThreadCore() const {
|
||||||
|
return npdm_header.main_thread_cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ProgramMetadata::GetMainThreadStackSize() const {
|
||||||
|
return npdm_header.main_stack_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ProgramMetadata::GetTitleID() const {
|
||||||
|
return aci_header.title_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ProgramMetadata::GetFilesystemPermissions() const {
|
||||||
|
return aci_file_access.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ProgramMetadata::GetSystemResourceSize() const {
|
||||||
|
return npdm_header.system_resource_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
PoolPartition ProgramMetadata::GetPoolPartition() const {
|
||||||
|
return acid_header.pool_partition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgramMetadata::KernelCapabilityDescriptors& ProgramMetadata::GetKernelCapabilities() const {
|
||||||
|
return aci_kernel_capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramMetadata::Print() const {
|
||||||
|
LOG_DEBUG(Service_FS, "Magic: {:.4}", npdm_header.magic.data());
|
||||||
|
LOG_DEBUG(Service_FS, "Main thread priority: 0x{:02X}", npdm_header.main_thread_priority);
|
||||||
|
LOG_DEBUG(Service_FS, "Main thread core: {}", npdm_header.main_thread_cpu);
|
||||||
|
LOG_DEBUG(Service_FS, "Main thread stack size: 0x{:X} bytes", npdm_header.main_stack_size);
|
||||||
|
LOG_DEBUG(Service_FS, "Process category: {}", npdm_header.process_category);
|
||||||
|
LOG_DEBUG(Service_FS, "Flags: 0x{:02X}", npdm_header.flags);
|
||||||
|
LOG_DEBUG(Service_FS, " > 64-bit instructions: {}",
|
||||||
|
npdm_header.has_64_bit_instructions ? "YES" : "NO");
|
||||||
|
|
||||||
|
const char* address_space = "Unknown";
|
||||||
|
switch (npdm_header.address_space_type) {
|
||||||
|
case ProgramAddressSpaceType::Is36Bit:
|
||||||
|
address_space = "64-bit (36-bit address space)";
|
||||||
|
break;
|
||||||
|
case ProgramAddressSpaceType::Is39Bit:
|
||||||
|
address_space = "64-bit (39-bit address space)";
|
||||||
|
break;
|
||||||
|
case ProgramAddressSpaceType::Is32Bit:
|
||||||
|
address_space = "32-bit";
|
||||||
|
break;
|
||||||
|
case ProgramAddressSpaceType::Is32BitNoMap:
|
||||||
|
address_space = "32-bit (no map region)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, " > Address space: {}\n", address_space);
|
||||||
|
|
||||||
|
// Begin ACID printing (potential perms, signed)
|
||||||
|
LOG_DEBUG(Service_FS, "Magic: {:.4}", acid_header.magic.data());
|
||||||
|
LOG_DEBUG(Service_FS, "Flags: 0x{:02X}", acid_header.flags);
|
||||||
|
LOG_DEBUG(Service_FS, " > Is Retail: {}", acid_header.production_flag ? "YES" : "NO");
|
||||||
|
LOG_DEBUG(Service_FS, "Title ID Min: 0x{:016X}", acid_header.title_id_min);
|
||||||
|
LOG_DEBUG(Service_FS, "Title ID Max: 0x{:016X}", acid_header.title_id_max);
|
||||||
|
LOG_DEBUG(Service_FS, "Filesystem Access: 0x{:016X}\n", acid_file_access.permissions);
|
||||||
|
|
||||||
|
// Begin ACI0 printing (actual perms, unsigned)
|
||||||
|
LOG_DEBUG(Service_FS, "Magic: {:.4}", aci_header.magic.data());
|
||||||
|
LOG_DEBUG(Service_FS, "Title ID: 0x{:016X}", aci_header.title_id);
|
||||||
|
LOG_DEBUG(Service_FS, "Filesystem Access: 0x{:016X}\n", aci_file_access.permissions);
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
189
core/fs/program_metadata.h
Normal file
189
core/fs/program_metadata.h
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/bit_field.h"
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
enum class ProgramAddressSpaceType : u8 {
|
||||||
|
Is32Bit = 0,
|
||||||
|
Is36Bit = 1,
|
||||||
|
Is32BitNoMap = 2,
|
||||||
|
Is39Bit = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ProgramFilePermission : u64 {
|
||||||
|
MountContent = 1ULL << 0,
|
||||||
|
SaveDataBackup = 1ULL << 5,
|
||||||
|
SdCard = 1ULL << 21,
|
||||||
|
Calibration = 1ULL << 34,
|
||||||
|
Bit62 = 1ULL << 62,
|
||||||
|
Everything = 1ULL << 63,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PoolPartition : u32 {
|
||||||
|
Application = 0,
|
||||||
|
Applet = 1,
|
||||||
|
System = 2,
|
||||||
|
SystemNonSecure = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper which implements an interface to parse Program Description Metadata (NPDM)
|
||||||
|
* Data can either be loaded from a file path or with data and an offset into it.
|
||||||
|
*/
|
||||||
|
class ProgramMetadata {
|
||||||
|
public:
|
||||||
|
using KernelCapabilityDescriptors = std::vector<u32>;
|
||||||
|
|
||||||
|
ProgramMetadata();
|
||||||
|
~ProgramMetadata();
|
||||||
|
|
||||||
|
ProgramMetadata(const ProgramMetadata&) = default;
|
||||||
|
ProgramMetadata& operator=(const ProgramMetadata&) = default;
|
||||||
|
|
||||||
|
ProgramMetadata(ProgramMetadata&&) = default;
|
||||||
|
ProgramMetadata& operator=(ProgramMetadata&&) = default;
|
||||||
|
|
||||||
|
/// Gets a default ProgramMetadata configuration, should only be used for homebrew formats where
|
||||||
|
/// we do not have an NPDM file
|
||||||
|
static ProgramMetadata GetDefault();
|
||||||
|
|
||||||
|
Loader::ResultStatus Load(VirtualFile file);
|
||||||
|
Loader::ResultStatus Reload(VirtualFile file);
|
||||||
|
|
||||||
|
/// Load from parameters instead of NPDM file, used for KIP
|
||||||
|
void LoadManual(bool is_64_bit, ProgramAddressSpaceType address_space, s32 main_thread_prio,
|
||||||
|
u32 main_thread_core, u32 main_thread_stack_size, u64 title_id,
|
||||||
|
u64 filesystem_permissions, u32 system_resource_size,
|
||||||
|
KernelCapabilityDescriptors capabilities);
|
||||||
|
|
||||||
|
bool Is64BitProgram() const;
|
||||||
|
ProgramAddressSpaceType GetAddressSpaceType() const;
|
||||||
|
u8 GetMainThreadPriority() const;
|
||||||
|
u8 GetMainThreadCore() const;
|
||||||
|
u32 GetMainThreadStackSize() const;
|
||||||
|
u64 GetTitleID() const;
|
||||||
|
u64 GetFilesystemPermissions() const;
|
||||||
|
u32 GetSystemResourceSize() const;
|
||||||
|
PoolPartition GetPoolPartition() const;
|
||||||
|
const KernelCapabilityDescriptors& GetKernelCapabilities() const;
|
||||||
|
const std::array<u8, 0x10>& GetName() const {
|
||||||
|
return npdm_header.application_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Print() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Header {
|
||||||
|
std::array<char, 4> magic;
|
||||||
|
std::array<u8, 8> reserved;
|
||||||
|
union {
|
||||||
|
u8 flags;
|
||||||
|
|
||||||
|
BitField<0, 1, u8> has_64_bit_instructions;
|
||||||
|
BitField<1, 3, ProgramAddressSpaceType> address_space_type;
|
||||||
|
BitField<4, 4, u8> reserved_2;
|
||||||
|
};
|
||||||
|
u8 reserved_3;
|
||||||
|
u8 main_thread_priority;
|
||||||
|
u8 main_thread_cpu;
|
||||||
|
std::array<u8, 4> reserved_4;
|
||||||
|
u32_le system_resource_size;
|
||||||
|
u32_le process_category;
|
||||||
|
u32_le main_stack_size;
|
||||||
|
std::array<u8, 0x10> application_name;
|
||||||
|
std::array<u8, 0x40> reserved_5;
|
||||||
|
u32_le aci_offset;
|
||||||
|
u32_le aci_size;
|
||||||
|
u32_le acid_offset;
|
||||||
|
u32_le acid_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(Header) == 0x80, "NPDM header structure size is wrong");
|
||||||
|
|
||||||
|
struct AcidHeader {
|
||||||
|
std::array<u8, 0x100> signature;
|
||||||
|
std::array<u8, 0x100> nca_modulus;
|
||||||
|
std::array<char, 4> magic;
|
||||||
|
u32_le nca_size;
|
||||||
|
std::array<u8, 0x4> reserved;
|
||||||
|
union {
|
||||||
|
u32 flags;
|
||||||
|
|
||||||
|
BitField<0, 1, u32> production_flag;
|
||||||
|
BitField<1, 1, u32> unqualified_approval;
|
||||||
|
BitField<2, 4, PoolPartition> pool_partition;
|
||||||
|
};
|
||||||
|
u64_le title_id_min;
|
||||||
|
u64_le title_id_max;
|
||||||
|
u32_le fac_offset;
|
||||||
|
u32_le fac_size;
|
||||||
|
u32_le sac_offset;
|
||||||
|
u32_le sac_size;
|
||||||
|
u32_le kac_offset;
|
||||||
|
u32_le kac_size;
|
||||||
|
INSERT_PADDING_BYTES(0x8);
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(AcidHeader) == 0x240, "ACID header structure size is wrong");
|
||||||
|
|
||||||
|
struct AciHeader {
|
||||||
|
std::array<char, 4> magic;
|
||||||
|
std::array<u8, 0xC> reserved;
|
||||||
|
u64_le title_id;
|
||||||
|
INSERT_PADDING_BYTES(0x8);
|
||||||
|
u32_le fah_offset;
|
||||||
|
u32_le fah_size;
|
||||||
|
u32_le sac_offset;
|
||||||
|
u32_le sac_size;
|
||||||
|
u32_le kac_offset;
|
||||||
|
u32_le kac_size;
|
||||||
|
INSERT_PADDING_BYTES(0x8);
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(AciHeader) == 0x40, "ACI0 header structure size is wrong");
|
||||||
|
|
||||||
|
// FileAccessControl and FileAccessHeader need loaded per-component: this layout does not
|
||||||
|
// reflect the real layout to avoid reference binding to misaligned addresses
|
||||||
|
struct FileAccessControl {
|
||||||
|
u8 version;
|
||||||
|
// 3 padding bytes
|
||||||
|
u64_le permissions;
|
||||||
|
std::array<u8, 0x20> unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileAccessHeader {
|
||||||
|
u8 version;
|
||||||
|
// 3 padding bytes
|
||||||
|
u64_le permissions;
|
||||||
|
u32_le unk_offset;
|
||||||
|
u32_le unk_size;
|
||||||
|
u32_le unk_offset_2;
|
||||||
|
u32_le unk_size_2;
|
||||||
|
};
|
||||||
|
|
||||||
|
Header npdm_header{};
|
||||||
|
AciHeader aci_header{};
|
||||||
|
AcidHeader acid_header{};
|
||||||
|
|
||||||
|
FileAccessControl acid_file_access{};
|
||||||
|
FileAccessHeader aci_file_access{};
|
||||||
|
|
||||||
|
KernelCapabilityDescriptors aci_kernel_capabilities{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
1036
core/fs/registered_cache.cpp
Normal file
1036
core/fs/registered_cache.cpp
Normal file
File diff suppressed because it is too large
Load diff
265
core/fs/registered_cache.h
Normal file
265
core/fs/registered_cache.h
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <boost/container/flat_map.hpp>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
class CNMT;
|
||||||
|
class NCA;
|
||||||
|
class NSP;
|
||||||
|
class XCI;
|
||||||
|
|
||||||
|
enum class ContentRecordType : u8;
|
||||||
|
enum class NCAContentType : u8;
|
||||||
|
enum class TitleType : u8;
|
||||||
|
|
||||||
|
struct ContentRecord;
|
||||||
|
struct CNMTHeader;
|
||||||
|
struct MetaRecord;
|
||||||
|
class RegisteredCache;
|
||||||
|
|
||||||
|
using NcaID = std::array<u8, 0x10>;
|
||||||
|
using ContentProviderParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
|
||||||
|
using VfsCopyFunction = std::function<bool(const VirtualFile&, const VirtualFile&, size_t)>;
|
||||||
|
|
||||||
|
enum class InstallResult {
|
||||||
|
Success,
|
||||||
|
OverwriteExisting,
|
||||||
|
ErrorAlreadyExists,
|
||||||
|
ErrorCopyFailed,
|
||||||
|
ErrorMetaFailed,
|
||||||
|
ErrorBaseInstall,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ContentProviderEntry {
|
||||||
|
u64 title_id;
|
||||||
|
ContentRecordType type;
|
||||||
|
|
||||||
|
std::string DebugInfo() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
|
||||||
|
return base_title_id | 0x800;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentRecordType GetCRTypeFromNCAType(NCAContentType type);
|
||||||
|
|
||||||
|
// boost flat_map requires operator< for O(log(n)) lookups.
|
||||||
|
bool operator<(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
|
||||||
|
|
||||||
|
// std unique requires operator== to identify duplicates.
|
||||||
|
bool operator==(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
|
||||||
|
bool operator!=(const ContentProviderEntry& lhs, const ContentProviderEntry& rhs);
|
||||||
|
|
||||||
|
class ContentProvider {
|
||||||
|
public:
|
||||||
|
virtual ~ContentProvider();
|
||||||
|
|
||||||
|
virtual void Refresh() = 0;
|
||||||
|
|
||||||
|
virtual bool HasEntry(u64 title_id, ContentRecordType type) const = 0;
|
||||||
|
bool HasEntry(ContentProviderEntry entry) const;
|
||||||
|
|
||||||
|
virtual std::optional<u32> GetEntryVersion(u64 title_id) const = 0;
|
||||||
|
|
||||||
|
virtual VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const = 0;
|
||||||
|
VirtualFile GetEntryUnparsed(ContentProviderEntry entry) const;
|
||||||
|
|
||||||
|
virtual VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const = 0;
|
||||||
|
VirtualFile GetEntryRaw(ContentProviderEntry entry) const;
|
||||||
|
|
||||||
|
virtual std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const = 0;
|
||||||
|
std::unique_ptr<NCA> GetEntry(ContentProviderEntry entry) const;
|
||||||
|
|
||||||
|
virtual std::vector<ContentProviderEntry> ListEntries() const;
|
||||||
|
|
||||||
|
// If a parameter is not std::nullopt, it will be filtered for from all entries.
|
||||||
|
virtual std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
||||||
|
std::optional<u64> title_id = {}) const = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// A single instance of KeyManager to be used by GetEntry()
|
||||||
|
Core::Crypto::KeyManager& keys = Core::Crypto::KeyManager::Instance();
|
||||||
|
};
|
||||||
|
|
||||||
|
class PlaceholderCache {
|
||||||
|
public:
|
||||||
|
explicit PlaceholderCache(VirtualDir dir);
|
||||||
|
|
||||||
|
bool Create(const NcaID& id, u64 size) const;
|
||||||
|
bool Delete(const NcaID& id) const;
|
||||||
|
bool Exists(const NcaID& id) const;
|
||||||
|
bool Write(const NcaID& id, u64 offset, const std::vector<u8>& data) const;
|
||||||
|
bool Register(RegisteredCache* cache, const NcaID& placeholder, const NcaID& install) const;
|
||||||
|
bool CleanAll() const;
|
||||||
|
std::optional<std::array<u8, 0x10>> GetRightsID(const NcaID& id) const;
|
||||||
|
u64 Size(const NcaID& id) const;
|
||||||
|
bool SetSize(const NcaID& id, u64 new_size) const;
|
||||||
|
std::vector<NcaID> List() const;
|
||||||
|
|
||||||
|
static NcaID Generate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualDir dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A class that catalogues NCAs in the registered directory structure.
|
||||||
|
* Nintendo's registered format follows this structure:
|
||||||
|
*
|
||||||
|
* Root
|
||||||
|
* | 000000XX <- XX is the ____ two digits of the NcaID
|
||||||
|
* | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder)
|
||||||
|
* | 00
|
||||||
|
* | 01 <- Actual content split along 4GB boundaries. (optional)
|
||||||
|
*
|
||||||
|
* (This impl also supports substituting the nca dir for an nca file, as that's more convenient
|
||||||
|
* when 4GB splitting can be ignored.)
|
||||||
|
*/
|
||||||
|
class RegisteredCache : public ContentProvider {
|
||||||
|
friend class PlaceholderCache;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Parsing function defines the conversion from raw file to NCA. If there are other steps
|
||||||
|
// besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom
|
||||||
|
// parsing function.
|
||||||
|
explicit RegisteredCache(
|
||||||
|
VirtualDir dir, ContentProviderParsingFunction parsing_function =
|
||||||
|
[](const VirtualFile& file, const NcaID& id) { return file; });
|
||||||
|
~RegisteredCache() override;
|
||||||
|
|
||||||
|
void Refresh() override;
|
||||||
|
|
||||||
|
bool HasEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
|
||||||
|
std::optional<u32> GetEntryVersion(u64 title_id) const override;
|
||||||
|
|
||||||
|
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
|
||||||
|
|
||||||
|
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
|
||||||
|
|
||||||
|
std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
|
||||||
|
// If a parameter is not std::nullopt, it will be filtered for from all entries.
|
||||||
|
std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
||||||
|
std::optional<u64> title_id = {}) const override;
|
||||||
|
|
||||||
|
// Raw copies all the ncas from the xci/nsp to the csache. Does some quick checks to make sure
|
||||||
|
// there is a meta NCA and all of them are accessible.
|
||||||
|
InstallResult InstallEntry(const XCI& xci, bool overwrite_if_exists = false,
|
||||||
|
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||||
|
InstallResult InstallEntry(const NSP& nsp, bool overwrite_if_exists = false,
|
||||||
|
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||||
|
|
||||||
|
// Due to the fact that we must use Meta-type NCAs to determine the existence of files, this
|
||||||
|
// poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a
|
||||||
|
// dir inside the NAND called 'yuzu_meta' and store the raw CNMT there.
|
||||||
|
// TODO(DarkLordZach): Author real meta-type NCAs and install those.
|
||||||
|
InstallResult InstallEntry(const NCA& nca, TitleType type, bool overwrite_if_exists = false,
|
||||||
|
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||||
|
|
||||||
|
InstallResult InstallEntry(const NCA& nca, const CNMTHeader& base_header,
|
||||||
|
const ContentRecord& base_record, bool overwrite_if_exists = false,
|
||||||
|
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||||
|
|
||||||
|
// Removes an existing entry based on title id
|
||||||
|
bool RemoveExistingEntry(u64 title_id) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename T>
|
||||||
|
void IterateAllMetadata(std::vector<T>& out,
|
||||||
|
std::function<T(const CNMT&, const ContentRecord&)> proc,
|
||||||
|
std::function<bool(const CNMT&, const ContentRecord&)> filter) const;
|
||||||
|
std::vector<NcaID> AccumulateFiles() const;
|
||||||
|
void ProcessFiles(const std::vector<NcaID>& ids);
|
||||||
|
void AccumulateYuzuMeta();
|
||||||
|
std::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const;
|
||||||
|
VirtualFile GetFileAtID(NcaID id) const;
|
||||||
|
VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& open_dir, std::string_view path) const;
|
||||||
|
InstallResult RawInstallNCA(const NCA& nca, const VfsCopyFunction& copy,
|
||||||
|
bool overwrite_if_exists, std::optional<NcaID> override_id = {});
|
||||||
|
bool RawInstallYuzuMeta(const CNMT& cnmt);
|
||||||
|
|
||||||
|
VirtualDir dir;
|
||||||
|
ContentProviderParsingFunction parser;
|
||||||
|
|
||||||
|
// maps tid -> NcaID of meta
|
||||||
|
std::map<u64, NcaID> meta_id;
|
||||||
|
// maps tid -> meta
|
||||||
|
std::map<u64, CNMT> meta;
|
||||||
|
// maps tid -> meta for CNMT in yuzu_meta
|
||||||
|
std::map<u64, CNMT> yuzu_meta;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ContentProviderUnionSlot {
|
||||||
|
SysNAND, ///< System NAND
|
||||||
|
UserNAND, ///< User NAND
|
||||||
|
SDMC, ///< SD Card
|
||||||
|
FrontendManual, ///< Frontend-defined game list or similar
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||||
|
class ContentProviderUnion : public ContentProvider {
|
||||||
|
public:
|
||||||
|
~ContentProviderUnion() override;
|
||||||
|
|
||||||
|
void SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider);
|
||||||
|
void ClearSlot(ContentProviderUnionSlot slot);
|
||||||
|
|
||||||
|
void Refresh() override;
|
||||||
|
bool HasEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::optional<u32> GetEntryVersion(u64 title_id) const override;
|
||||||
|
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
|
||||||
|
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
|
std::optional<u64> title_id) const override;
|
||||||
|
|
||||||
|
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
|
||||||
|
std::optional<ContentProviderUnionSlot> origin = {},
|
||||||
|
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
||||||
|
std::optional<u64> title_id = {}) const;
|
||||||
|
|
||||||
|
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id,
|
||||||
|
ContentRecordType type) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<ContentProviderUnionSlot, ContentProvider*> providers;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ManualContentProvider : public ContentProvider {
|
||||||
|
public:
|
||||||
|
~ManualContentProvider() override;
|
||||||
|
|
||||||
|
void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
|
VirtualFile file);
|
||||||
|
void ClearAllEntries();
|
||||||
|
|
||||||
|
void Refresh() override;
|
||||||
|
bool HasEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::optional<u32> GetEntryVersion(u64 title_id) const override;
|
||||||
|
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
|
||||||
|
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
|
std::optional<u64> title_id) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
167
core/fs/romfs.cpp
Normal file
167
core/fs/romfs.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
#include "core/file_sys/fsmitm_romfsbuild.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_cached.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_concat.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_offset.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_vector.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
namespace {
|
||||||
|
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
struct TableLocation {
|
||||||
|
u64_le offset;
|
||||||
|
u64_le size;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(TableLocation) == 0x10, "TableLocation has incorrect size.");
|
||||||
|
|
||||||
|
struct RomFSHeader {
|
||||||
|
u64_le header_size;
|
||||||
|
TableLocation directory_hash;
|
||||||
|
TableLocation directory_meta;
|
||||||
|
TableLocation file_hash;
|
||||||
|
TableLocation file_meta;
|
||||||
|
u64_le data_offset;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RomFSHeader) == 0x50, "RomFSHeader has incorrect size.");
|
||||||
|
|
||||||
|
struct DirectoryEntry {
|
||||||
|
u32_le parent;
|
||||||
|
u32_le sibling;
|
||||||
|
u32_le child_dir;
|
||||||
|
u32_le child_file;
|
||||||
|
u32_le hash;
|
||||||
|
u32_le name_length;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(DirectoryEntry) == 0x18, "DirectoryEntry has incorrect size.");
|
||||||
|
|
||||||
|
struct FileEntry {
|
||||||
|
u32_le parent;
|
||||||
|
u32_le sibling;
|
||||||
|
u64_le offset;
|
||||||
|
u64_le size;
|
||||||
|
u32_le hash;
|
||||||
|
u32_le name_length;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(FileEntry) == 0x20, "FileEntry has incorrect size.");
|
||||||
|
|
||||||
|
struct RomFSTraversalContext {
|
||||||
|
RomFSHeader header;
|
||||||
|
VirtualFile file;
|
||||||
|
std::vector<u8> directory_meta;
|
||||||
|
std::vector<u8> file_meta;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename EntryType, auto Member>
|
||||||
|
std::pair<EntryType, std::string> GetEntry(const RomFSTraversalContext& ctx, size_t offset) {
|
||||||
|
const size_t entry_end = offset + sizeof(EntryType);
|
||||||
|
const std::vector<u8>& vec = ctx.*Member;
|
||||||
|
const size_t size = vec.size();
|
||||||
|
const u8* data = vec.data();
|
||||||
|
EntryType entry{};
|
||||||
|
|
||||||
|
if (entry_end > size) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::memcpy(&entry, data + offset, sizeof(EntryType));
|
||||||
|
|
||||||
|
const size_t name_length = std::min(entry_end + entry.name_length, size) - entry_end;
|
||||||
|
std::string name(reinterpret_cast<const char*>(data + entry_end), name_length);
|
||||||
|
|
||||||
|
return {entry, std::move(name)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<DirectoryEntry, std::string> GetDirectoryEntry(const RomFSTraversalContext& ctx,
|
||||||
|
size_t directory_offset) {
|
||||||
|
return GetEntry<DirectoryEntry, &RomFSTraversalContext::directory_meta>(ctx, directory_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<FileEntry, std::string> GetFileEntry(const RomFSTraversalContext& ctx,
|
||||||
|
size_t file_offset) {
|
||||||
|
return GetEntry<FileEntry, &RomFSTraversalContext::file_meta>(ctx, file_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessFile(const RomFSTraversalContext& ctx, u32 this_file_offset,
|
||||||
|
std::shared_ptr<VectorVfsDirectory>& parent) {
|
||||||
|
while (this_file_offset != ROMFS_ENTRY_EMPTY) {
|
||||||
|
auto entry = GetFileEntry(ctx, this_file_offset);
|
||||||
|
|
||||||
|
parent->AddFile(std::make_shared<OffsetVfsFile>(ctx.file, entry.first.size,
|
||||||
|
entry.first.offset + ctx.header.data_offset,
|
||||||
|
std::move(entry.second)));
|
||||||
|
|
||||||
|
this_file_offset = entry.first.sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessDirectory(const RomFSTraversalContext& ctx, u32 this_dir_offset,
|
||||||
|
std::shared_ptr<VectorVfsDirectory>& parent) {
|
||||||
|
while (this_dir_offset != ROMFS_ENTRY_EMPTY) {
|
||||||
|
auto entry = GetDirectoryEntry(ctx, this_dir_offset);
|
||||||
|
auto current = std::make_shared<VectorVfsDirectory>(
|
||||||
|
std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, entry.second);
|
||||||
|
|
||||||
|
if (entry.first.child_file != ROMFS_ENTRY_EMPTY) {
|
||||||
|
ProcessFile(ctx, entry.first.child_file, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.first.child_dir != ROMFS_ENTRY_EMPTY) {
|
||||||
|
ProcessDirectory(ctx, entry.first.child_dir, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent->AddDirectory(current);
|
||||||
|
this_dir_offset = entry.first.sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
VirtualDir ExtractRomFS(VirtualFile file) {
|
||||||
|
auto root_container = std::make_shared<VectorVfsDirectory>();
|
||||||
|
if (!file) {
|
||||||
|
return root_container;
|
||||||
|
}
|
||||||
|
|
||||||
|
RomFSTraversalContext ctx{};
|
||||||
|
|
||||||
|
if (file->ReadObject(&ctx.header) != sizeof(RomFSHeader)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.header.header_size != sizeof(RomFSHeader)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.file = file;
|
||||||
|
ctx.directory_meta =
|
||||||
|
file->ReadBytes(ctx.header.directory_meta.size, ctx.header.directory_meta.offset);
|
||||||
|
ctx.file_meta = file->ReadBytes(ctx.header.file_meta.size, ctx.header.file_meta.offset);
|
||||||
|
|
||||||
|
ProcessDirectory(ctx, 0, root_container);
|
||||||
|
|
||||||
|
if (auto root = root_container->GetSubdirectory(""); root) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT(false);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile CreateRomFS(VirtualDir dir, VirtualDir ext) {
|
||||||
|
if (dir == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
RomFSBuildContext ctx{dir, ext};
|
||||||
|
return ConcatenatedVfsFile::MakeConcatenatedFile(0, dir->GetName(), ctx.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
18
core/fs/romfs.h
Normal file
18
core/fs/romfs.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
// Converts a RomFS binary blob to VFS Filesystem
|
||||||
|
// Returns nullptr on failure
|
||||||
|
VirtualDir ExtractRomFS(VirtualFile file);
|
||||||
|
|
||||||
|
// Converts a VFS filesystem into a RomFS binary
|
||||||
|
// Returns nullptr on failure
|
||||||
|
VirtualFile CreateRomFS(VirtualDir dir, VirtualDir ext = nullptr);
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
96
core/fs/romfs_factory.cpp
Normal file
96
core/fs/romfs_factory.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/file_sys/common_funcs.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/patch_manager.h"
|
||||||
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/romfs_factory.h"
|
||||||
|
#include "core/hle/kernel/k_process.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
RomFSFactory::RomFSFactory(Loader::AppLoader& app_loader, ContentProvider& provider,
|
||||||
|
Service::FileSystem::FileSystemController& controller)
|
||||||
|
: content_provider{provider}, filesystem_controller{controller} {
|
||||||
|
// Load the RomFS from the app
|
||||||
|
if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) {
|
||||||
|
LOG_WARNING(Service_FS, "Unable to read base RomFS");
|
||||||
|
}
|
||||||
|
|
||||||
|
updatable = app_loader.IsRomFSUpdatable();
|
||||||
|
}
|
||||||
|
|
||||||
|
RomFSFactory::~RomFSFactory() = default;
|
||||||
|
|
||||||
|
void RomFSFactory::SetPackedUpdate(VirtualFile update_raw_file) {
|
||||||
|
packed_update_raw = std::move(update_raw_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile RomFSFactory::OpenCurrentProcess(u64 current_process_title_id) const {
|
||||||
|
if (!updatable) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto type = ContentRecordType::Program;
|
||||||
|
const auto nca = content_provider.GetEntry(current_process_title_id, type);
|
||||||
|
const PatchManager patch_manager{current_process_title_id, filesystem_controller,
|
||||||
|
content_provider};
|
||||||
|
return patch_manager.PatchRomFS(nca.get(), file, ContentRecordType::Program, packed_update_raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile RomFSFactory::OpenPatchedRomFS(u64 title_id, ContentRecordType type) const {
|
||||||
|
auto nca = content_provider.GetEntry(title_id, type);
|
||||||
|
|
||||||
|
if (nca == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PatchManager patch_manager{title_id, filesystem_controller, content_provider};
|
||||||
|
|
||||||
|
return patch_manager.PatchRomFS(nca.get(), nca->GetRomFS(), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile RomFSFactory::OpenPatchedRomFSWithProgramIndex(u64 title_id, u8 program_index,
|
||||||
|
ContentRecordType type) const {
|
||||||
|
const auto res_title_id = GetBaseTitleIDWithProgramIndex(title_id, program_index);
|
||||||
|
|
||||||
|
return OpenPatchedRomFS(res_title_id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile RomFSFactory::Open(u64 title_id, StorageId storage, ContentRecordType type) const {
|
||||||
|
const std::shared_ptr<NCA> res = GetEntry(title_id, storage, type);
|
||||||
|
if (res == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res->GetRomFS();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<NCA> RomFSFactory::GetEntry(u64 title_id, StorageId storage,
|
||||||
|
ContentRecordType type) const {
|
||||||
|
switch (storage) {
|
||||||
|
case StorageId::None:
|
||||||
|
return content_provider.GetEntry(title_id, type);
|
||||||
|
case StorageId::NandSystem:
|
||||||
|
return filesystem_controller.GetSystemNANDContents()->GetEntry(title_id, type);
|
||||||
|
case StorageId::NandUser:
|
||||||
|
return filesystem_controller.GetUserNANDContents()->GetEntry(title_id, type);
|
||||||
|
case StorageId::SdCard:
|
||||||
|
return filesystem_controller.GetSDMCContents()->GetEntry(title_id, type);
|
||||||
|
case StorageId::Host:
|
||||||
|
case StorageId::GameCard:
|
||||||
|
default:
|
||||||
|
UNIMPLEMENTED_MSG("Unimplemented storage_id={:02X}", static_cast<u8>(storage));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
64
core/fs/romfs_factory.h
Normal file
64
core/fs/romfs_factory.h
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
class AppLoader;
|
||||||
|
} // namespace Loader
|
||||||
|
|
||||||
|
namespace Service::FileSystem {
|
||||||
|
class FileSystemController;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class ContentProvider;
|
||||||
|
class NCA;
|
||||||
|
|
||||||
|
enum class ContentRecordType : u8;
|
||||||
|
|
||||||
|
enum class StorageId : u8 {
|
||||||
|
None = 0,
|
||||||
|
Host = 1,
|
||||||
|
GameCard = 2,
|
||||||
|
NandSystem = 3,
|
||||||
|
NandUser = 4,
|
||||||
|
SdCard = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// File system interface to the RomFS archive
|
||||||
|
class RomFSFactory {
|
||||||
|
public:
|
||||||
|
explicit RomFSFactory(Loader::AppLoader& app_loader, ContentProvider& provider,
|
||||||
|
Service::FileSystem::FileSystemController& controller);
|
||||||
|
~RomFSFactory();
|
||||||
|
|
||||||
|
void SetPackedUpdate(VirtualFile packed_update_raw);
|
||||||
|
[[nodiscard]] VirtualFile OpenCurrentProcess(u64 current_process_title_id) const;
|
||||||
|
[[nodiscard]] VirtualFile OpenPatchedRomFS(u64 title_id, ContentRecordType type) const;
|
||||||
|
[[nodiscard]] VirtualFile OpenPatchedRomFSWithProgramIndex(u64 title_id, u8 program_index,
|
||||||
|
ContentRecordType type) const;
|
||||||
|
[[nodiscard]] VirtualFile Open(u64 title_id, StorageId storage, ContentRecordType type) const;
|
||||||
|
[[nodiscard]] std::shared_ptr<NCA> GetEntry(u64 title_id, StorageId storage,
|
||||||
|
ContentRecordType type) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualFile file;
|
||||||
|
VirtualFile packed_update_raw;
|
||||||
|
|
||||||
|
VirtualFile base;
|
||||||
|
|
||||||
|
bool updatable;
|
||||||
|
|
||||||
|
ContentProvider& content_provider;
|
||||||
|
Service::FileSystem::FileSystemController& filesystem_controller;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
194
core/fs/savedata_factory.cpp
Normal file
194
core/fs/savedata_factory.cpp
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/uuid.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/file_sys/savedata_factory.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool ShouldSaveDataBeAutomaticallyCreated(SaveDataSpaceId space, const SaveDataAttribute& attr) {
|
||||||
|
return attr.type == SaveDataType::Cache || attr.type == SaveDataType::Temporary ||
|
||||||
|
(space == SaveDataSpaceId::User && ///< Normal Save Data -- Current Title & User
|
||||||
|
(attr.type == SaveDataType::Account || attr.type == SaveDataType::Device) &&
|
||||||
|
attr.program_id == 0 && attr.system_save_data_id == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u64 title_id,
|
||||||
|
u128 user_id) {
|
||||||
|
// Only detect nand user saves.
|
||||||
|
const auto space_id_path = [space_id]() -> std::string_view {
|
||||||
|
switch (space_id) {
|
||||||
|
case SaveDataSpaceId::User:
|
||||||
|
return "/user/save";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (space_id_path.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::UUID uuid;
|
||||||
|
std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
|
||||||
|
|
||||||
|
// Only detect account/device saves from the future location.
|
||||||
|
switch (type) {
|
||||||
|
case SaveDataType::Account:
|
||||||
|
return fmt::format("{}/account/{}/{:016X}/0", space_id_path, uuid.RawString(), title_id);
|
||||||
|
case SaveDataType::Device:
|
||||||
|
return fmt::format("{}/device/{:016X}/0", space_id_path, title_id);
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||||
|
VirtualDir save_directory_)
|
||||||
|
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
|
||||||
|
// Delete all temporary storages
|
||||||
|
// On hardware, it is expected that temporary storage be empty at first use.
|
||||||
|
dir->DeleteSubdirectoryRecursive("temp");
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveDataFactory::~SaveDataFactory() = default;
|
||||||
|
|
||||||
|
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||||
|
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
||||||
|
meta.user_id, meta.system_save_data_id);
|
||||||
|
|
||||||
|
return dir->CreateDirectoryRelative(save_directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||||
|
|
||||||
|
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
||||||
|
meta.user_id, meta.system_save_data_id);
|
||||||
|
|
||||||
|
auto out = dir->GetDirectoryRelative(save_directory);
|
||||||
|
|
||||||
|
if (out == nullptr && (ShouldSaveDataBeAutomaticallyCreated(space, meta) && auto_create)) {
|
||||||
|
return Create(space, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) const {
|
||||||
|
return dir->GetDirectoryRelative(GetSaveDataSpaceIdPath(space));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||||
|
switch (space) {
|
||||||
|
case SaveDataSpaceId::System:
|
||||||
|
return "/system/";
|
||||||
|
case SaveDataSpaceId::User:
|
||||||
|
return "/user/";
|
||||||
|
case SaveDataSpaceId::Temporary:
|
||||||
|
return "/temp/";
|
||||||
|
default:
|
||||||
|
ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
||||||
|
return "/unrecognized/"; ///< To prevent corruption when ignoring asserts.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
||||||
|
SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||||
|
u128 user_id, u64 save_id) {
|
||||||
|
// According to switchbrew, if a save is of type SaveData and the title id field is 0, it should
|
||||||
|
// be interpreted as the title id of the current process.
|
||||||
|
if (type == SaveDataType::Account || type == SaveDataType::Device) {
|
||||||
|
if (title_id == 0) {
|
||||||
|
title_id = program_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For compat with a future impl.
|
||||||
|
if (std::string future_path =
|
||||||
|
GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id);
|
||||||
|
!future_path.empty()) {
|
||||||
|
// Check if this location exists, and prefer it over the old.
|
||||||
|
if (const auto future_dir = dir->GetDirectoryRelative(future_path); future_dir != nullptr) {
|
||||||
|
LOG_INFO(Service_FS, "Using save at new location: {}", future_path);
|
||||||
|
return future_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out = GetSaveDataSpaceIdPath(space);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SaveDataType::System:
|
||||||
|
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]);
|
||||||
|
case SaveDataType::Account:
|
||||||
|
case SaveDataType::Device:
|
||||||
|
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
||||||
|
title_id);
|
||||||
|
case SaveDataType::Temporary:
|
||||||
|
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
||||||
|
title_id);
|
||||||
|
case SaveDataType::Cache:
|
||||||
|
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
||||||
|
default:
|
||||||
|
ASSERT_MSG(false, "Unrecognized SaveDataType: {:02X}", static_cast<u8>(type));
|
||||||
|
return fmt::format("{}save/unknown_{:X}/{:016X}", out, static_cast<u8>(type), title_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
|
||||||
|
if (future) {
|
||||||
|
Common::UUID uuid;
|
||||||
|
std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
|
||||||
|
return fmt::format("/user/save/account/{}", uuid.RawString());
|
||||||
|
}
|
||||||
|
return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id,
|
||||||
|
u128 user_id) const {
|
||||||
|
const auto path =
|
||||||
|
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||||
|
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||||
|
|
||||||
|
const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName());
|
||||||
|
if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) {
|
||||||
|
return {0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveDataSize out;
|
||||||
|
if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) {
|
||||||
|
return {0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,
|
||||||
|
SaveDataSize new_value) const {
|
||||||
|
const auto path =
|
||||||
|
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||||
|
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||||
|
|
||||||
|
const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName());
|
||||||
|
if (size_file == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_file->Resize(sizeof(SaveDataSize));
|
||||||
|
size_file->WriteObject(new_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveDataFactory::SetAutoCreate(bool state) {
|
||||||
|
auto_create = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
56
core/fs/savedata_factory.h
Normal file
56
core/fs/savedata_factory.h
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/fs_save_data_types.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr const char* GetSaveDataSizeFileName() {
|
||||||
|
return ".yuzu_save_size";
|
||||||
|
}
|
||||||
|
|
||||||
|
using ProgramId = u64;
|
||||||
|
|
||||||
|
/// File system interface to the SaveData archive
|
||||||
|
class SaveDataFactory {
|
||||||
|
public:
|
||||||
|
explicit SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||||
|
VirtualDir save_directory_);
|
||||||
|
~SaveDataFactory();
|
||||||
|
|
||||||
|
VirtualDir Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const;
|
||||||
|
VirtualDir Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const;
|
||||||
|
|
||||||
|
VirtualDir GetSaveDataSpaceDirectory(SaveDataSpaceId space) const;
|
||||||
|
|
||||||
|
static std::string GetSaveDataSpaceIdPath(SaveDataSpaceId space);
|
||||||
|
static std::string GetFullPath(ProgramId program_id, VirtualDir dir, SaveDataSpaceId space,
|
||||||
|
SaveDataType type, u64 title_id, u128 user_id, u64 save_id);
|
||||||
|
static std::string GetUserGameSaveDataRoot(u128 user_id, bool future);
|
||||||
|
|
||||||
|
SaveDataSize ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const;
|
||||||
|
void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,
|
||||||
|
SaveDataSize new_value) const;
|
||||||
|
|
||||||
|
void SetAutoCreate(bool state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Core::System& system;
|
||||||
|
ProgramId program_id;
|
||||||
|
VirtualDir dir;
|
||||||
|
bool auto_create{true};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
62
core/fs/sdmc_factory.cpp
Normal file
62
core/fs/sdmc_factory.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/sdmc_factory.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/xts_archive.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
constexpr u64 SDMC_TOTAL_SIZE = 0x10000000000; // 1 TiB
|
||||||
|
|
||||||
|
SDMCFactory::SDMCFactory(VirtualDir sd_dir_, VirtualDir sd_mod_dir_)
|
||||||
|
: sd_dir(std::move(sd_dir_)), sd_mod_dir(std::move(sd_mod_dir_)),
|
||||||
|
contents(std::make_unique<RegisteredCache>(
|
||||||
|
GetOrCreateDirectoryRelative(sd_dir, "/Nintendo/Contents/registered"),
|
||||||
|
[](const VirtualFile& file, const NcaID& id) {
|
||||||
|
return NAX{file, id}.GetDecrypted();
|
||||||
|
})),
|
||||||
|
placeholder(std::make_unique<PlaceholderCache>(
|
||||||
|
GetOrCreateDirectoryRelative(sd_dir, "/Nintendo/Contents/placehld"))) {}
|
||||||
|
|
||||||
|
SDMCFactory::~SDMCFactory() = default;
|
||||||
|
|
||||||
|
VirtualDir SDMCFactory::Open() const {
|
||||||
|
return sd_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir SDMCFactory::GetSDMCModificationLoadRoot(u64 title_id) const {
|
||||||
|
// LayeredFS doesn't work on updates and title id-less homebrew
|
||||||
|
if (title_id == 0 || (title_id & 0xFFF) == 0x800) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return GetOrCreateDirectoryRelative(sd_mod_dir, fmt::format("/{:016X}", title_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir SDMCFactory::GetSDMCContentDirectory() const {
|
||||||
|
return GetOrCreateDirectoryRelative(sd_dir, "/Nintendo/Contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredCache* SDMCFactory::GetSDMCContents() const {
|
||||||
|
return contents.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceholderCache* SDMCFactory::GetSDMCPlaceholder() const {
|
||||||
|
return placeholder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir SDMCFactory::GetImageDirectory() const {
|
||||||
|
return GetOrCreateDirectoryRelative(sd_dir, "/Nintendo/Album");
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 SDMCFactory::GetSDMCFreeSpace() const {
|
||||||
|
return GetSDMCTotalSpace() - sd_dir->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 SDMCFactory::GetSDMCTotalSpace() const {
|
||||||
|
return SDMC_TOTAL_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
42
core/fs/sdmc_factory.h
Normal file
42
core/fs/sdmc_factory.h
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "core/file_sys/vfs/vfs_types.h"
|
||||||
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class RegisteredCache;
|
||||||
|
class PlaceholderCache;
|
||||||
|
|
||||||
|
/// File system interface to the SDCard archive
|
||||||
|
class SDMCFactory {
|
||||||
|
public:
|
||||||
|
explicit SDMCFactory(VirtualDir sd_dir_, VirtualDir sd_mod_dir_);
|
||||||
|
~SDMCFactory();
|
||||||
|
|
||||||
|
VirtualDir Open() const;
|
||||||
|
|
||||||
|
VirtualDir GetSDMCModificationLoadRoot(u64 title_id) const;
|
||||||
|
VirtualDir GetSDMCContentDirectory() const;
|
||||||
|
|
||||||
|
RegisteredCache* GetSDMCContents() const;
|
||||||
|
PlaceholderCache* GetSDMCPlaceholder() const;
|
||||||
|
|
||||||
|
VirtualDir GetImageDirectory() const;
|
||||||
|
|
||||||
|
u64 GetSDMCFreeSpace() const;
|
||||||
|
u64 GetSDMCTotalSpace() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VirtualDir sd_dir;
|
||||||
|
VirtualDir sd_mod_dir;
|
||||||
|
|
||||||
|
std::unique_ptr<RegisteredCache> contents;
|
||||||
|
std::unique_ptr<PlaceholderCache> placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FileSys
|
||||||
289
core/fs/submission_package.cpp
Normal file
289
core/fs/submission_package.cpp
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <fmt/ostream.h>
|
||||||
|
|
||||||
|
#include "common/hex_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/crypto/key_manager.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/partition_filesystem.h"
|
||||||
|
#include "core/file_sys/program_metadata.h"
|
||||||
|
#include "core/file_sys/submission_package.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
NSP::NSP(VirtualFile file_, u64 title_id_, std::size_t program_index_)
|
||||||
|
: file(std::move(file_)), expected_program_id(title_id_),
|
||||||
|
program_index(program_index_), status{Loader::ResultStatus::Success},
|
||||||
|
pfs(std::make_shared<PartitionFilesystem>(file)), keys{Core::Crypto::KeyManager::Instance()} {
|
||||||
|
if (pfs->GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
status = pfs->GetStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto files = pfs->GetFiles();
|
||||||
|
|
||||||
|
if (IsDirectoryExeFS(pfs)) {
|
||||||
|
extracted = true;
|
||||||
|
InitializeExeFSAndRomFS(files);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTicketKeys(files);
|
||||||
|
ReadNCAs(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSP::~NSP() = default;
|
||||||
|
|
||||||
|
Loader::ResultStatus NSP::GetStatus() const {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader::ResultStatus NSP::GetProgramStatus() const {
|
||||||
|
if (IsExtractedType() && GetExeFS() != nullptr && FileSys::IsDirectoryExeFS(GetExeFS())) {
|
||||||
|
return Loader::ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto iter = program_status.find(GetProgramTitleID());
|
||||||
|
if (iter == program_status.end())
|
||||||
|
return Loader::ResultStatus::ErrorNSPMissingProgramNCA;
|
||||||
|
return iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NSP::GetProgramTitleID() const {
|
||||||
|
if (IsExtractedType()) {
|
||||||
|
return GetExtractedTitleID() + program_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto program_id = expected_program_id;
|
||||||
|
if (program_id == 0) {
|
||||||
|
if (!program_status.empty()) {
|
||||||
|
program_id = program_status.begin()->first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
program_id = program_id + program_index;
|
||||||
|
if (program_status.find(program_id) != program_status.end()) {
|
||||||
|
return program_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ids = GetProgramTitleIDs();
|
||||||
|
const auto iter =
|
||||||
|
std::find_if(ids.begin(), ids.end(), [](u64 tid) { return (tid & 0x800) == 0; });
|
||||||
|
return iter == ids.end() ? 0 : *iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 NSP::GetExtractedTitleID() const {
|
||||||
|
if (GetExeFS() == nullptr || !IsDirectoryExeFS(GetExeFS())) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramMetadata meta;
|
||||||
|
if (meta.Load(GetExeFS()->GetFile("main.npdm")) == Loader::ResultStatus::Success) {
|
||||||
|
return meta.GetTitleID();
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u64> NSP::GetProgramTitleIDs() const {
|
||||||
|
if (IsExtractedType()) {
|
||||||
|
return {GetExtractedTitleID()};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u64> out{program_ids.cbegin(), program_ids.cend()};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NSP::IsExtractedType() const {
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile NSP::GetRomFS() const {
|
||||||
|
return romfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir NSP::GetExeFS() const {
|
||||||
|
return exefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<NCA>> NSP::GetNCAsCollapsed() const {
|
||||||
|
if (extracted)
|
||||||
|
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||||
|
std::vector<std::shared_ptr<NCA>> out;
|
||||||
|
for (const auto& map : ncas) {
|
||||||
|
for (const auto& inner_map : map.second)
|
||||||
|
out.push_back(inner_map.second);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::multimap<u64, std::shared_ptr<NCA>> NSP::GetNCAsByTitleID() const {
|
||||||
|
if (extracted)
|
||||||
|
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||||
|
std::multimap<u64, std::shared_ptr<NCA>> out;
|
||||||
|
for (const auto& map : ncas) {
|
||||||
|
for (const auto& inner_map : map.second)
|
||||||
|
out.emplace(map.first, inner_map.second);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>>
|
||||||
|
NSP::GetNCAs() const {
|
||||||
|
return ncas;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<NCA> NSP::GetNCA(u64 title_id, ContentRecordType type, TitleType title_type) const {
|
||||||
|
if (extracted)
|
||||||
|
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||||
|
|
||||||
|
const auto title_id_iter = ncas.find(title_id);
|
||||||
|
if (title_id_iter == ncas.end())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const auto type_iter = title_id_iter->second.find({title_type, type});
|
||||||
|
if (type_iter == title_id_iter->second.end())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return type_iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile NSP::GetNCAFile(u64 title_id, ContentRecordType type, TitleType title_type) const {
|
||||||
|
if (extracted)
|
||||||
|
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||||
|
const auto nca = GetNCA(title_id, type, title_type);
|
||||||
|
if (nca != nullptr)
|
||||||
|
return nca->GetBaseFile();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualFile> NSP::GetFiles() const {
|
||||||
|
return pfs->GetFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VirtualDir> NSP::GetSubdirectories() const {
|
||||||
|
return pfs->GetSubdirectories();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NSP::GetName() const {
|
||||||
|
return file->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualDir NSP::GetParentDirectory() const {
|
||||||
|
return file->GetContainingDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NSP::SetTicketKeys(const std::vector<VirtualFile>& files) {
|
||||||
|
for (const auto& ticket_file : files) {
|
||||||
|
if (ticket_file == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticket_file->GetExtension() != "tik") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ticket = Core::Crypto::Ticket::Read(ticket_file);
|
||||||
|
if (!keys.AddTicket(ticket)) {
|
||||||
|
LOG_WARNING(Common_Filesystem, "Could not load NSP ticket {}", ticket_file->GetName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NSP::InitializeExeFSAndRomFS(const std::vector<VirtualFile>& files) {
|
||||||
|
exefs = pfs;
|
||||||
|
|
||||||
|
const auto iter = std::find_if(files.begin(), files.end(), [](const VirtualFile& entry) {
|
||||||
|
return entry->GetName().rfind(".romfs") != std::string::npos;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (iter == files.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
romfs = *iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
||||||
|
for (const auto& outer_file : files) {
|
||||||
|
if (outer_file->GetName().size() < 9 ||
|
||||||
|
outer_file->GetName().substr(outer_file->GetName().size() - 9) != ".cnmt.nca") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nca = std::make_shared<NCA>(outer_file);
|
||||||
|
if (nca->GetStatus() != Loader::ResultStatus::Success || nca->GetSubdirectories().empty()) {
|
||||||
|
program_status[nca->GetTitleId()] = nca->GetStatus();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto section0 = nca->GetSubdirectories()[0];
|
||||||
|
|
||||||
|
for (const auto& inner_file : section0->GetFiles()) {
|
||||||
|
if (inner_file->GetExtension() != "cnmt") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CNMT cnmt(inner_file);
|
||||||
|
|
||||||
|
ncas[cnmt.GetTitleID()][{cnmt.GetType(), ContentRecordType::Meta}] = nca;
|
||||||
|
|
||||||
|
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||||
|
const auto id_string = Common::HexToString(rec.nca_id, false);
|
||||||
|
auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
|
||||||
|
|
||||||
|
if (next_file == nullptr) {
|
||||||
|
if (rec.type != ContentRecordType::DeltaFragment) {
|
||||||
|
LOG_WARNING(Service_FS,
|
||||||
|
"NCA with ID {}.nca is listed in content metadata, but cannot "
|
||||||
|
"be found in PFS. NSP appears to be corrupted.",
|
||||||
|
id_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto next_nca = std::make_shared<NCA>(std::move(next_file));
|
||||||
|
|
||||||
|
if (next_nca->GetType() == NCAContentType::Program) {
|
||||||
|
program_status[next_nca->GetTitleId()] = next_nca->GetStatus();
|
||||||
|
program_ids.insert(next_nca->GetTitleId() & 0xFFFFFFFFFFFFF000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next_nca->GetStatus() != Loader::ResultStatus::Success &&
|
||||||
|
next_nca->GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last 3 hexadecimal digits of the CNMT TitleID is 0x800 or is missing the
|
||||||
|
// BKTRBaseRomFS, this is an update NCA. Otherwise, this is a base NCA.
|
||||||
|
if ((cnmt.GetTitleID() & 0x800) != 0 ||
|
||||||
|
next_nca->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
|
||||||
|
// If the last 3 hexadecimal digits of the NCA's TitleID is between 0x1 and
|
||||||
|
// 0x7FF, this is a multi-program update NCA. Otherwise, this is a regular
|
||||||
|
// update NCA.
|
||||||
|
if ((next_nca->GetTitleId() & 0x7FF) != 0 &&
|
||||||
|
(next_nca->GetTitleId() & 0x800) == 0) {
|
||||||
|
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
|
||||||
|
std::move(next_nca);
|
||||||
|
} else {
|
||||||
|
ncas[cnmt.GetTitleID()][{cnmt.GetType(), rec.type}] = std::move(next_nca);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] = std::move(next_nca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace FileSys
|
||||||
90
core/fs/submission_package.h
Normal file
90
core/fs/submission_package.h
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
|
namespace Core::Crypto {
|
||||||
|
class KeyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
enum class ResultStatus : u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileSys {
|
||||||
|
|
||||||
|
class NCA;
|
||||||
|
class PartitionFilesystem;
|
||||||
|
|
||||||
|
enum class ContentRecordType : u8;
|
||||||
|
|
||||||
|
class NSP : public ReadOnlyVfsDirectory {
|
||||||
|
public:
|
||||||
|
explicit NSP(VirtualFile file_, u64 title_id = 0, std::size_t program_index_ = 0);
|
||||||
|
~NSP() override;
|
||||||
|
|
||||||
|
Loader::ResultStatus GetStatus() const;
|
||||||
|
Loader::ResultStatus GetProgramStatus() const;
|
||||||
|
// Should only be used when one title id can be assured.
|
||||||
|
u64 GetProgramTitleID() const;
|
||||||
|
u64 GetExtractedTitleID() const;
|
||||||
|
std::vector<u64> GetProgramTitleIDs() const;
|
||||||
|
|
||||||
|
bool IsExtractedType() const;
|
||||||
|
|
||||||
|
// Common (Can be safely called on both types)
|
||||||
|
VirtualFile GetRomFS() const;
|
||||||
|
VirtualDir GetExeFS() const;
|
||||||
|
|
||||||
|
// Type 0 Only (Collection of NCAs + Certificate + Ticket + Meta XML)
|
||||||
|
std::vector<std::shared_ptr<NCA>> GetNCAsCollapsed() const;
|
||||||
|
std::multimap<u64, std::shared_ptr<NCA>> GetNCAsByTitleID() const;
|
||||||
|
std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>> GetNCAs()
|
||||||
|
const;
|
||||||
|
std::shared_ptr<NCA> GetNCA(u64 title_id, ContentRecordType type,
|
||||||
|
TitleType title_type = TitleType::Application) const;
|
||||||
|
VirtualFile GetNCAFile(u64 title_id, ContentRecordType type,
|
||||||
|
TitleType title_type = TitleType::Application) const;
|
||||||
|
|
||||||
|
std::vector<VirtualFile> GetFiles() const override;
|
||||||
|
|
||||||
|
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||||
|
|
||||||
|
std::string GetName() const override;
|
||||||
|
|
||||||
|
VirtualDir GetParentDirectory() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetTicketKeys(const std::vector<VirtualFile>& files);
|
||||||
|
void InitializeExeFSAndRomFS(const std::vector<VirtualFile>& files);
|
||||||
|
void ReadNCAs(const std::vector<VirtualFile>& files);
|
||||||
|
|
||||||
|
VirtualFile file;
|
||||||
|
|
||||||
|
const u64 expected_program_id;
|
||||||
|
const std::size_t program_index;
|
||||||
|
|
||||||
|
bool extracted = false;
|
||||||
|
Loader::ResultStatus status;
|
||||||
|
std::map<u64, Loader::ResultStatus> program_status;
|
||||||
|
|
||||||
|
std::shared_ptr<PartitionFilesystem> pfs;
|
||||||
|
// Map title id -> {map type -> NCA}
|
||||||
|
std::map<u64, std::map<std::pair<TitleType, ContentRecordType>, std::shared_ptr<NCA>>> ncas;
|
||||||
|
std::set<u64> program_ids;
|
||||||
|
std::vector<VirtualFile> ticket_files;
|
||||||
|
|
||||||
|
Core::Crypto::KeyManager& keys;
|
||||||
|
|
||||||
|
VirtualFile romfs;
|
||||||
|
VirtualDir exefs;
|
||||||
|
};
|
||||||
|
} // namespace FileSys
|
||||||
13591
core/fs/system_archive/data/font_chinese_simplified.cpp
Normal file
13591
core/fs/system_archive/data/font_chinese_simplified.cpp
Normal file
File diff suppressed because it is too large
Load diff
12
core/fs/system_archive/data/font_chinese_simplified.h
Normal file
12
core/fs/system_archive/data/font_chinese_simplified.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace FileSys::SystemArchive::SharedFontData {
|
||||||
|
|
||||||
|
extern const std::array<unsigned char, 217276> FONT_CHINESE_SIMPLIFIED;
|
||||||
|
|
||||||
|
} // namespace FileSys::SystemArchive::SharedFontData
|
||||||
13901
core/fs/system_archive/data/font_chinese_traditional.cpp
Normal file
13901
core/fs/system_archive/data/font_chinese_traditional.cpp
Normal file
File diff suppressed because it is too large
Load diff
12
core/fs/system_archive/data/font_chinese_traditional.h
Normal file
12
core/fs/system_archive/data/font_chinese_traditional.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace FileSys::SystemArchive::SharedFontData {
|
||||||
|
|
||||||
|
extern const std::array<unsigned char, 222236> FONT_CHINESE_TRADITIONAL;
|
||||||
|
|
||||||
|
} // namespace FileSys::SystemArchive::SharedFontData
|
||||||
18356
core/fs/system_archive/data/font_extended_chinese_simplified.cpp
Normal file
18356
core/fs/system_archive/data/font_extended_chinese_simplified.cpp
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue