summaryrefslogtreecommitdiffstats
path: root/src/core/file_sys
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/file_sys')
-rw-r--r--src/core/file_sys/card_image.cpp150
-rw-r--r--src/core/file_sys/card_image.h93
-rw-r--r--src/core/file_sys/content_archive.cpp168
-rw-r--r--src/core/file_sys/content_archive.h26
-rw-r--r--src/core/file_sys/vfs.cpp21
-rw-r--r--src/core/file_sys/vfs.h3
6 files changed, 425 insertions, 36 deletions
diff --git a/src/core/file_sys/card_image.cpp b/src/core/file_sys/card_image.cpp
new file mode 100644
index 000000000..5ff09a362
--- /dev/null
+++ b/src/core/file_sys/card_image.cpp
@@ -0,0 +1,150 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <array>
+#include <string>
+#include <core/loader/loader.h>
+#include "core/file_sys/card_image.h"
+#include "core/file_sys/partition_filesystem.h"
+#include "core/file_sys/vfs_offset.h"
+
+namespace FileSys {
+
+XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
+ if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
+ status = Loader::ResultStatus::ErrorInvalidFormat;
+ return;
+ }
+
+ if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
+ status = Loader::ResultStatus::ErrorInvalidFormat;
+ return;
+ }
+
+ PartitionFilesystem main_hfs(
+ std::make_shared<OffsetVfsFile>(file, header.hfs_size, header.hfs_offset));
+
+ if (main_hfs.GetStatus() != Loader::ResultStatus::Success) {
+ status = main_hfs.GetStatus();
+ return;
+ }
+
+ const static std::array<std::string, 0x4> partition_names = {"update", "normal", "secure",
+ "logo"};
+
+ for (XCIPartition partition :
+ {XCIPartition::Update, XCIPartition::Normal, XCIPartition::Secure, XCIPartition::Logo}) {
+ auto raw = main_hfs.GetFile(partition_names[static_cast<size_t>(partition)]);
+ if (raw != nullptr)
+ partitions[static_cast<size_t>(partition)] = std::make_shared<PartitionFilesystem>(raw);
+ }
+
+ auto result = AddNCAFromPartition(XCIPartition::Secure);
+ if (result != Loader::ResultStatus::Success) {
+ status = result;
+ return;
+ }
+ result = AddNCAFromPartition(XCIPartition::Update);
+ if (result != Loader::ResultStatus::Success) {
+ status = result;
+ return;
+ }
+
+ 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;
+}
+
+Loader::ResultStatus XCI::GetStatus() const {
+ return status;
+}
+
+VirtualDir XCI::GetPartition(XCIPartition partition) const {
+ return partitions[static_cast<size_t>(partition)];
+}
+
+VirtualDir XCI::GetSecurePartition() const {
+ return GetPartition(XCIPartition::Secure);
+}
+
+VirtualDir XCI::GetNormalPartition() const {
+ return GetPartition(XCIPartition::Normal);
+}
+
+VirtualDir XCI::GetUpdatePartition() const {
+ return GetPartition(XCIPartition::Update);
+}
+
+VirtualDir XCI::GetLogoPartition() const {
+ return GetPartition(XCIPartition::Logo);
+}
+
+std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
+ for (const auto& nca : ncas) {
+ if (nca->GetType() == type)
+ return nca;
+ }
+
+ return nullptr;
+}
+
+VirtualFile XCI::GetNCAFileByType(NCAContentType type) const {
+ auto nca = GetNCAByType(type);
+ if (nca != nullptr)
+ return nca->GetBaseFile();
+ return nullptr;
+}
+
+std::vector<std::shared_ptr<VfsFile>> XCI::GetFiles() const {
+ return {};
+}
+
+std::vector<std::shared_ptr<VfsDirectory>> XCI::GetSubdirectories() const {
+ return std::vector<std::shared_ptr<VfsDirectory>>();
+}
+
+std::string XCI::GetName() const {
+ return file->GetName();
+}
+
+std::shared_ptr<VfsDirectory> XCI::GetParentDirectory() const {
+ return file->GetContainingDirectory();
+}
+
+bool XCI::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
+ return false;
+}
+
+Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
+ if (partitions[static_cast<size_t>(part)] == nullptr) {
+ return Loader::ResultStatus::ErrorInvalidFormat;
+ }
+
+ for (VirtualFile file : partitions[static_cast<size_t>(part)]->GetFiles()) {
+ if (file->GetExtension() != "nca")
+ continue;
+ auto nca = std::make_shared<NCA>(file);
+ if (nca->GetStatus() == Loader::ResultStatus::Success)
+ ncas.push_back(std::move(nca));
+ }
+
+ return Loader::ResultStatus::Success;
+}
+
+u8 XCI::GetFormatVersion() const {
+ return GetLogoPartition() == nullptr ? 0x1 : 0x2;
+}
+} // namespace FileSys
diff --git a/src/core/file_sys/card_image.h b/src/core/file_sys/card_image.h
new file mode 100644
index 000000000..b765c8bc1
--- /dev/null
+++ b/src/core/file_sys/card_image.h
@@ -0,0 +1,93 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <vector>
+#include "common/swap.h"
+#include "core/file_sys/content_archive.h"
+#include "core/file_sys/vfs.h"
+
+namespace FileSys {
+
+enum class GamecardSize : u8 {
+ S_1GB = 0xFA,
+ S_2GB = 0xF8,
+ S_4GB = 0xF0,
+ S_8GB = 0xE0,
+ S_16GB = 0xE1,
+ S_32GB = 0xE2,
+};
+
+struct GamecardInfo {
+ std::array<u8, 0x70> data;
+};
+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);
+
+ Loader::ResultStatus GetStatus() const;
+
+ u8 GetFormatVersion() const;
+
+ VirtualDir GetPartition(XCIPartition partition) const;
+ VirtualDir GetSecurePartition() const;
+ VirtualDir GetNormalPartition() const;
+ VirtualDir GetUpdatePartition() const;
+ VirtualDir GetLogoPartition() const;
+
+ std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
+ VirtualFile GetNCAFileByType(NCAContentType type) const;
+
+ std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
+
+ std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override;
+
+ std::string GetName() const override;
+
+ std::shared_ptr<VfsDirectory> GetParentDirectory() const override;
+
+protected:
+ bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
+
+private:
+ Loader::ResultStatus AddNCAFromPartition(XCIPartition part);
+
+ VirtualFile file;
+ GamecardHeader header{};
+
+ Loader::ResultStatus status;
+
+ std::vector<VirtualDir> partitions;
+ std::vector<std::shared_ptr<NCA>> ncas;
+};
+} // namespace FileSys
diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp
index 61cb0bbe3..add01974e 100644
--- a/src/core/file_sys/content_archive.cpp
+++ b/src/core/file_sys/content_archive.cpp
@@ -6,9 +6,12 @@
#include <utility>
#include "common/logging/log.h"
+#include "core/crypto/aes_util.h"
+#include "core/crypto/ctr_encryption_layer.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/vfs_offset.h"
#include "core/loader/loader.h"
+#include "mbedtls/cipher.h"
#include "romfs.h"
namespace FileSys {
@@ -29,11 +32,19 @@ enum class NCASectionFilesystemType : u8 {
struct NCASectionHeaderBlock {
INSERT_PADDING_BYTES(3);
NCASectionFilesystemType filesystem_type;
- u8 crypto_type;
+ NCASectionCryptoType crypto_type;
INSERT_PADDING_BYTES(3);
};
static_assert(sizeof(NCASectionHeaderBlock) == 0x8, "NCASectionHeaderBlock has incorrect size.");
+struct NCASectionRaw {
+ NCASectionHeaderBlock header;
+ std::array<u8, 0x138> block_data;
+ std::array<u8, 0x8> section_ctr;
+ INSERT_PADDING_BYTES(0xB8);
+};
+static_assert(sizeof(NCASectionRaw) == 0x200, "NCASectionRaw has incorrect size.");
+
struct PFS0Superblock {
NCASectionHeaderBlock header_block;
std::array<u8, 0x20> hash;
@@ -43,62 +54,155 @@ struct PFS0Superblock {
u64_le hash_table_size;
u64_le pfs0_header_offset;
u64_le pfs0_size;
- INSERT_PADDING_BYTES(432);
+ INSERT_PADDING_BYTES(0x1B0);
};
static_assert(sizeof(PFS0Superblock) == 0x200, "PFS0Superblock has incorrect size.");
struct RomFSSuperblock {
NCASectionHeaderBlock header_block;
IVFCHeader ivfc;
+ INSERT_PADDING_BYTES(0x118);
+};
+static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
+
+union NCASectionHeader {
+ NCASectionRaw raw;
+ PFS0Superblock pfs0;
+ RomFSSuperblock romfs;
};
-static_assert(sizeof(RomFSSuperblock) == 0xE8, "RomFSSuperblock has incorrect size.");
+static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
+
+bool IsValidNCA(const NCAHeader& header) {
+ // TODO(DarkLordZach): Add NCA2/NCA0 support.
+ return header.magic == Common::MakeMagic('N', 'C', 'A', '3');
+}
+
+Crypto::Key128 NCA::GetKeyAreaKey(NCASectionCryptoType type) {
+ u8 master_key_id = header.crypto_type;
+ if (header.crypto_type_2 > master_key_id)
+ master_key_id = header.crypto_type_2;
+ if (master_key_id > 0)
+ --master_key_id;
+
+ std::vector<u8> key_area(header.key_area.begin(), header.key_area.end());
+ if (!Crypto::keys.ValidateKey(Crypto::S128KeyType::KEY_AREA, master_key_id, header.key_index)) {
+ status = Loader::ResultStatus::ErrorEncrypted;
+ return {};
+ }
+ Crypto::AESCipher<Crypto::Key128> cipher(
+ Crypto::keys.GetKey(Crypto::S128KeyType::KEY_AREA, master_key_id, header.key_index),
+ Crypto::Mode::ECB);
+ cipher.Transcode(key_area.data(), key_area.size(), key_area.data(), Crypto::Op::DECRYPT);
+
+ Crypto::Key128 out;
+ if (type == NCASectionCryptoType::XTS)
+ std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
+ else if (type == NCASectionCryptoType::CTR)
+ std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
+ else
+ LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
+ static_cast<u8>(type));
+
+ u128 out_128 = *reinterpret_cast<u128*>(&out);
+ LOG_DEBUG(Crypto, "called with crypto_rev={:02X}, kak_index={:02X}, key={:016X}{:016X}",
+ master_key_id, header.key_index, out_128[1], out_128[0]);
+
+ return out;
+}
+
+VirtualFile NCA::Decrypt(NCASectionHeader header, VirtualFile in, u64 starting_offset) {
+ if (!encrypted)
+ return in;
+
+ switch (header.raw.header.crypto_type) {
+ case NCASectionCryptoType::NONE:
+ LOG_DEBUG(Crypto, "called with mode=NONE");
+ return in;
+ case NCASectionCryptoType::CTR:
+ LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
+ {
+ auto out = std::make_shared<Crypto::CTREncryptionLayer>(
+ std::move(in), GetKeyAreaKey(NCASectionCryptoType::CTR), starting_offset);
+ std::vector<u8> iv(16, 0);
+ for (u8 i = 0; i < 8; ++i)
+ iv[i] = header.raw.section_ctr[0x8 - i - 1];
+ out->SetIV(iv);
+ return out;
+ }
+ case NCASectionCryptoType::XTS:
+ // TODO(DarkLordZach): Implement XTSEncryptionLayer and title key encryption.
+ default:
+ LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
+ static_cast<u8>(header.raw.header.crypto_type));
+ return nullptr;
+ }
+}
NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
if (sizeof(NCAHeader) != file->ReadObject(&header))
- LOG_CRITICAL(Loader, "File reader errored out during header read.");
+ LOG_ERROR(Loader, "File reader errored out during header read.");
+
+ encrypted = false;
if (!IsValidNCA(header)) {
- status = Loader::ResultStatus::ErrorInvalidFormat;
- return;
+ NCAHeader dec_header{};
+ if (!Crypto::keys.ValidateKey(Crypto::S256KeyType::HEADER)) {
+ status = Loader::ResultStatus::ErrorEncrypted;
+ return;
+ }
+ Crypto::AESCipher<Crypto::Key256> cipher(Crypto::keys.GetKey(Crypto::S256KeyType::HEADER),
+ Crypto::Mode::XTS);
+ cipher.XTSTranscode(&header, sizeof(NCAHeader), &dec_header, 0, 0x200, Crypto::Op::DECRYPT);
+ if (IsValidNCA(dec_header)) {
+ header = dec_header;
+ encrypted = true;
+ } else {
+ status = Loader::ResultStatus::ErrorInvalidFormat;
+ return;
+ }
}
- std::ptrdiff_t number_sections =
+ const std::ptrdiff_t number_sections =
std::count_if(std::begin(header.section_tables), std::end(header.section_tables),
[](NCASectionTableEntry entry) { return entry.media_offset > 0; });
+ std::vector<NCASectionHeader> sections(number_sections);
+ const auto length_sections = SECTION_HEADER_SIZE * number_sections;
+
+ if (encrypted) {
+ auto raw = file->ReadBytes(length_sections, SECTION_HEADER_OFFSET);
+ if (!Crypto::keys.ValidateKey(Crypto::S256KeyType::HEADER)) {
+ status = Loader::ResultStatus::ErrorEncrypted;
+ return;
+ }
+ Crypto::AESCipher<Crypto::Key256> cipher(Crypto::keys.GetKey(Crypto::S256KeyType::HEADER),
+ Crypto::Mode::XTS);
+ cipher.XTSTranscode(raw.data(), length_sections, sections.data(), 2, SECTION_HEADER_SIZE,
+ Crypto::Op::DECRYPT);
+ } else {
+ file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
+ }
+
for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
- // Seek to beginning of this section.
- NCASectionHeaderBlock block{};
- if (sizeof(NCASectionHeaderBlock) !=
- file->ReadObject(&block, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
- LOG_CRITICAL(Loader, "File reader errored out during header read.");
-
- if (block.filesystem_type == NCASectionFilesystemType::ROMFS) {
- RomFSSuperblock sb{};
- if (sizeof(RomFSSuperblock) !=
- file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
- LOG_CRITICAL(Loader, "File reader errored out during header read.");
+ auto section = sections[i];
+ if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
const size_t romfs_offset =
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER +
- sb.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
- const size_t romfs_size = sb.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
- files.emplace_back(std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset));
+ section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
+ const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
+ files.emplace_back(
+ Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset),
+ romfs_offset));
romfs = files.back();
- } else if (block.filesystem_type == NCASectionFilesystemType::PFS0) {
- PFS0Superblock sb{};
- // Seek back to beginning of this section.
- if (sizeof(PFS0Superblock) !=
- file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
- LOG_CRITICAL(Loader, "File reader errored out during header read.");
-
+ } else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
MEDIA_OFFSET_MULTIPLIER) +
- sb.pfs0_header_offset;
+ section.pfs0.pfs0_header_offset;
u64 size = MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset -
header.section_tables[i].media_offset);
auto npfs = std::make_shared<PartitionFilesystem>(
- std::make_shared<OffsetVfsFile>(file, size, offset));
+ Decrypt(section, std::make_shared<OffsetVfsFile>(file, size, offset), offset));
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
dirs.emplace_back(npfs);
@@ -153,6 +257,10 @@ VirtualDir NCA::GetExeFS() const {
return exefs;
}
+VirtualFile NCA::GetBaseFile() const {
+ return file;
+}
+
bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
return false;
}
diff --git a/src/core/file_sys/content_archive.h b/src/core/file_sys/content_archive.h
index 0b8b9db61..1e8d9c8ae 100644
--- a/src/core/file_sys/content_archive.h
+++ b/src/core/file_sys/content_archive.h
@@ -12,10 +12,10 @@
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
+#include "core/crypto/key_manager.h"
#include "core/file_sys/partition_filesystem.h"
namespace FileSys {
-
enum class NCAContentType : u8 {
Program = 0,
Meta = 1,
@@ -24,6 +24,13 @@ enum class NCAContentType : u8 {
Data = 4,
};
+enum class NCASectionCryptoType : u8 {
+ NONE = 1,
+ XTS = 2,
+ CTR = 3,
+ BKTR = 4,
+};
+
struct NCASectionTableEntry {
u32_le media_offset;
u32_le media_end_offset;
@@ -48,20 +55,19 @@ struct NCAHeader {
std::array<u8, 0x10> rights_id;
std::array<NCASectionTableEntry, 0x4> section_tables;
std::array<std::array<u8, 0x20>, 0x4> hash_tables;
- std::array<std::array<u8, 0x10>, 0x4> key_area;
+ std::array<u8, 0x40> key_area;
INSERT_PADDING_BYTES(0xC0);
};
static_assert(sizeof(NCAHeader) == 0x400, "NCAHeader has incorrect size.");
+union NCASectionHeader;
+
inline bool IsDirectoryExeFS(const std::shared_ptr<VfsDirectory>& pfs) {
// According to switchbrew, an exefs must only contain these two files:
return pfs->GetFile("main") != nullptr && pfs->GetFile("main.npdm") != nullptr;
}
-inline bool IsValidNCA(const NCAHeader& header) {
- return header.magic == Common::MakeMagic('N', 'C', 'A', '2') ||
- header.magic == Common::MakeMagic('N', 'C', 'A', '3');
-}
+bool IsValidNCA(const NCAHeader& header);
// An implementation of VfsDirectory that represents a Nintendo Content Archive (NCA) conatiner.
// After construction, use GetStatus to determine if the file is valid and ready to be used.
@@ -81,6 +87,8 @@ public:
VirtualFile GetRomFS() const;
VirtualDir GetExeFS() const;
+ VirtualFile GetBaseFile() const;
+
protected:
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
@@ -95,6 +103,12 @@ private:
NCAHeader header{};
Loader::ResultStatus status{};
+
+ bool encrypted;
+
+ Crypto::Key128 GetKeyAreaKey(NCASectionCryptoType type);
+
+ VirtualFile Decrypt(NCASectionHeader header, VirtualFile in, size_t starting_offset);
};
} // namespace FileSys
diff --git a/src/core/file_sys/vfs.cpp b/src/core/file_sys/vfs.cpp
index 84a6a7397..57d0aeb85 100644
--- a/src/core/file_sys/vfs.cpp
+++ b/src/core/file_sys/vfs.cpp
@@ -285,6 +285,27 @@ bool ReadOnlyVfsDirectory::Rename(std::string_view name) {
return false;
}
+bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size) {
+ if (file1->GetSize() != file2->GetSize())
+ return false;
+
+ std::vector<u8> f1_v(block_size);
+ std::vector<u8> f2_v(block_size);
+ for (size_t i = 0; i < file1->GetSize(); i += block_size) {
+ auto f1_vs = file1->Read(f1_v.data(), block_size, i);
+ auto f2_vs = file2->Read(f2_v.data(), block_size, i);
+
+ if (f1_vs != f2_vs)
+ return false;
+ for (size_t j = 0; j < f1_vs; ++j) {
+ if (f1_v[j] != f2_v[j])
+ return false;
+ }
+ }
+
+ return true;
+}
+
bool VfsRawCopy(VirtualFile src, VirtualFile dest) {
if (src == nullptr || dest == nullptr)
return false;
diff --git a/src/core/file_sys/vfs.h b/src/core/file_sys/vfs.h
index cf871edd6..fab9e2b45 100644
--- a/src/core/file_sys/vfs.h
+++ b/src/core/file_sys/vfs.h
@@ -245,6 +245,9 @@ struct ReadOnlyVfsDirectory : public VfsDirectory {
bool Rename(std::string_view name) override;
};
+// Compare the two files, byte-for-byte, in increments specificed by block_size
+bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size = 0x200);
+
// A method that copies the raw data between two different implementations of VirtualFile. If you
// are using the same implementation, it is probably better to use the Copy method in the parent
// directory of src/dest.