File xenia_PR2216.patch of Package xenia
From c21328dfdf6c2c98634892cc1ea191ff233df548 Mon Sep 17 00:00:00 2001
From: Gliniak <Gliniak93@gmail.com>
Date: Tue, 21 Jun 2022 11:05:32 +0200
Subject: [PATCH] [VFS] Separation of STFS, SVOD into different entities
---
src/xenia/emulator.cc | 16 +-
.../vfs/devices/stfs_container_device.cc | 858 ------------------
src/xenia/vfs/devices/stfs_container_device.h | 148 ---
src/xenia/vfs/devices/stfs_xbox.h | 42 +-
.../vfs/devices/xcontent_container_device.cc | 165 ++++
.../vfs/devices/xcontent_container_device.h | 98 ++
...r_entry.cc => xcontent_container_entry.cc} | 24 +-
...ner_entry.h => xcontent_container_entry.h} | 26 +-
...ner_file.cc => xcontent_container_file.cc} | 17 +-
...ainer_file.h => xcontent_container_file.h} | 18 +-
.../xcontent_devices/stfs_container_device.cc | 324 +++++++
.../xcontent_devices/stfs_container_device.h | 95 ++
.../xcontent_devices/svod_container_device.cc | 417 +++++++++
.../xcontent_devices/svod_container_device.h | 77 ++
src/xenia/vfs/vfs_dump.cc | 7 +-
15 files changed, 1269 insertions(+), 1063 deletions(-)
delete mode 100644 src/xenia/vfs/devices/stfs_container_device.cc
delete mode 100644 src/xenia/vfs/devices/stfs_container_device.h
create mode 100644 src/xenia/vfs/devices/xcontent_container_device.cc
create mode 100644 src/xenia/vfs/devices/xcontent_container_device.h
rename src/xenia/vfs/devices/{stfs_container_entry.cc => xcontent_container_entry.cc} (51%)
rename src/xenia/vfs/devices/{stfs_container_entry.h => xcontent_container_entry.h} (63%)
rename src/xenia/vfs/devices/{stfs_container_file.cc => xcontent_container_file.cc} (77%)
rename src/xenia/vfs/devices/{stfs_container_file.h => xcontent_container_file.h} (68%)
create mode 100644 src/xenia/vfs/devices/xcontent_devices/stfs_container_device.cc
create mode 100644 src/xenia/vfs/devices/xcontent_devices/stfs_container_device.h
create mode 100644 src/xenia/vfs/devices/xcontent_devices/svod_container_device.cc
create mode 100644 src/xenia/vfs/devices/xcontent_devices/svod_container_device.h
diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc
index cca28982f4..0ba0758b98 100644
--- a/src/xenia/emulator.cc
+++ b/src/xenia/emulator.cc
@@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2021 Ben Vanik. All rights reserved. *
+ * Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@@ -45,11 +45,12 @@
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/window.h"
#include "xenia/ui/windowed_app_context.h"
+#include "xenia/vfs/device.h"
#include "xenia/vfs/devices/disc_image_device.h"
#include "xenia/vfs/devices/host_path_device.h"
#include "xenia/vfs/devices/null_device.h"
-#include "xenia/vfs/devices/stfs_container_device.h"
#include "xenia/vfs/virtual_file_system.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h"
@@ -351,16 +352,21 @@ X_STATUS Emulator::LaunchDiscImage(const std::filesystem::path& path) {
X_STATUS Emulator::LaunchStfsContainer(const std::filesystem::path& path) {
auto mount_path = "\\Device\\Cdrom0";
+ auto device =
+ vfs::XContentContainerDevice::CreateContentDevice(mount_path, path);
+ if (!device) {
+ xe::FatalError("Cannot create XContent (STFS, SVOD) device.");
+ return X_STATUS_NO_SUCH_FILE;
+ }
// Register the container in the virtual filesystem.
- auto device = std::make_unique<vfs::StfsContainerDevice>(mount_path, path);
if (!device->Initialize()) {
xe::FatalError(
- "Unable to mount STFS container; file not found or corrupt.");
+ "Unable to initialize XContent container; file not found or corrupt.");
return X_STATUS_NO_SUCH_FILE;
}
if (!file_system_->RegisterDevice(std::move(device))) {
- xe::FatalError("Unable to register STFS container.");
+ xe::FatalError("Unable to register XContent container.");
return X_STATUS_NO_SUCH_FILE;
}
diff --git a/src/xenia/vfs/devices/stfs_container_device.cc b/src/xenia/vfs/devices/stfs_container_device.cc
deleted file mode 100644
index 1381786322..0000000000
--- a/src/xenia/vfs/devices/stfs_container_device.cc
+++ /dev/null
@@ -1,858 +0,0 @@
-/**
- ******************************************************************************
- * Xenia : Xbox 360 Emulator Research Project *
- ******************************************************************************
- * Copyright 2020 Ben Vanik. All rights reserved. *
- * Released under the BSD license - see LICENSE in the root for more details. *
- ******************************************************************************
- */
-
-#include "xenia/vfs/devices/stfs_container_device.h"
-
-#include <algorithm>
-#include <queue>
-#include <vector>
-
-#include "xenia/base/logging.h"
-#include "xenia/base/math.h"
-#include "xenia/vfs/devices/stfs_container_entry.h"
-
-#if XE_PLATFORM_WIN32
-#include "xenia/base/platform_win.h"
-#define timegm _mkgmtime
-#endif
-
-namespace xe {
-namespace vfs {
-
-// Convert FAT timestamp to 100-nanosecond intervals since January 1, 1601 (UTC)
-uint64_t decode_fat_timestamp(uint32_t date, uint32_t time) {
- struct tm tm = {0};
- // 80 is the difference between 1980 (FAT) and 1900 (tm);
- tm.tm_year = ((0xFE00 & date) >> 9) + 80;
- tm.tm_mon = (0x01E0 & date) >> 5;
- tm.tm_mday = (0x001F & date) >> 0;
- tm.tm_hour = (0xF800 & time) >> 11;
- tm.tm_min = (0x07E0 & time) >> 5;
- tm.tm_sec = (0x001F & time) << 1; // the value stored in 2-seconds intervals
- tm.tm_isdst = 0;
- time_t timet = timegm(&tm);
- if (timet == -1) {
- return 0;
- }
- // 11644473600LL is a difference between 1970 and 1601
- return (timet + 11644473600LL) * 10000000;
-}
-
-StfsContainerDevice::StfsContainerDevice(const std::string_view mount_path,
- const std::filesystem::path& host_path)
- : Device(mount_path),
- name_("STFS"),
- host_path_(host_path),
- files_total_size_(),
- svod_base_offset_(),
- header_(),
- svod_layout_(),
- blocks_per_hash_table_(1),
- block_step{0, 0} {}
-
-StfsContainerDevice::~StfsContainerDevice() { CloseFiles(); }
-
-bool StfsContainerDevice::Initialize() {
- // Resolve a valid STFS file if a directory is given.
- if (std::filesystem::is_directory(host_path_) &&
- !ResolveFromFolder(host_path_)) {
- XELOGE("Could not resolve an STFS container given path {}",
- xe::path_to_utf8(host_path_));
- return false;
- }
-
- if (!std::filesystem::exists(host_path_)) {
- XELOGE("Path to STFS container does not exist: {}",
- xe::path_to_utf8(host_path_));
- return false;
- }
-
- // Open the data file(s)
- auto open_result = OpenFiles();
- if (open_result != Error::kSuccess) {
- XELOGE("Failed to open STFS container: {}", open_result);
- return false;
- }
-
- switch (header_.metadata.volume_type) {
- case XContentVolumeType::kStfs:
- return ReadSTFS() == Error::kSuccess;
- break;
- case XContentVolumeType::kSvod:
- return ReadSVOD() == Error::kSuccess;
- default:
- XELOGE("Unknown XContent volume type: {}",
- xe::byte_swap(uint32_t(header_.metadata.volume_type.value)));
- return false;
- }
-}
-
-StfsContainerDevice::Error StfsContainerDevice::OpenFiles() {
- // Map the file containing the STFS Header and read it.
- XELOGI("Loading STFS header file: {}", xe::path_to_utf8(host_path_));
-
- auto header_file = xe::filesystem::OpenFile(host_path_, "rb");
- if (!header_file) {
- XELOGE("Error opening STFS header file.");
- return Error::kErrorReadError;
- }
-
- auto header_result = ReadHeaderAndVerify(header_file);
- if (header_result != Error::kSuccess) {
- XELOGE("Error reading STFS header: {}", header_result);
- fclose(header_file);
- files_total_size_ = 0;
- return header_result;
- }
-
- // If the STFS package is a single file, the header is self contained and
- // we don't need to map any extra files.
- // NOTE: data_file_count is 0 for STFS and 1 for SVOD
- if (header_.metadata.data_file_count <= 1) {
- XELOGI("STFS container is a single file.");
- files_.emplace(std::make_pair(0, header_file));
- return Error::kSuccess;
- }
-
- // If the STFS package is multi-file, it is an SVOD system. We need to map
- // the files in the .data folder and can discard the header.
- auto data_fragment_path = host_path_;
- data_fragment_path += ".data";
- if (!std::filesystem::exists(data_fragment_path)) {
- XELOGE("STFS container is multi-file, but path {} does not exist.",
- xe::path_to_utf8(data_fragment_path));
- return Error::kErrorFileMismatch;
- }
-
- // Ensure data fragment files are sorted
- auto fragment_files = filesystem::ListFiles(data_fragment_path);
- std::sort(fragment_files.begin(), fragment_files.end(),
- [](filesystem::FileInfo& left, filesystem::FileInfo& right) {
- return left.name < right.name;
- });
-
- if (fragment_files.size() != header_.metadata.data_file_count) {
- XELOGE("SVOD expecting {} data fragments, but {} are present.",
- header_.metadata.data_file_count, fragment_files.size());
- return Error::kErrorFileMismatch;
- }
-
- for (size_t i = 0; i < fragment_files.size(); i++) {
- auto& fragment = fragment_files.at(i);
- auto path = fragment.path / fragment.name;
- auto file = xe::filesystem::OpenFile(path, "rb");
- if (!file) {
- XELOGI("Failed to map SVOD file {}.", xe::path_to_utf8(path));
- CloseFiles();
- return Error::kErrorReadError;
- }
-
- xe::filesystem::Seek(file, 0L, SEEK_END);
- files_total_size_ += xe::filesystem::Tell(file);
- // no need to seek back, any reads from this file will seek first anyway
- files_.emplace(std::make_pair(i, file));
- }
- XELOGI("SVOD successfully mapped {} files.", fragment_files.size());
- return Error::kSuccess;
-}
-
-void StfsContainerDevice::CloseFiles() {
- for (auto& file : files_) {
- fclose(file.second);
- }
- files_.clear();
- files_total_size_ = 0;
-}
-
-void StfsContainerDevice::Dump(StringBuffer* string_buffer) {
- auto global_lock = global_critical_region_.Acquire();
- root_entry_->Dump(string_buffer, 0);
-}
-
-Entry* StfsContainerDevice::ResolvePath(const std::string_view path) {
- // The filesystem will have stripped our prefix off already, so the path will
- // be in the form:
- // some\PATH.foo
- XELOGFS("StfsContainerDevice::ResolvePath({})", path);
- return root_entry_->ResolvePath(path);
-}
-
-StfsContainerDevice::Error StfsContainerDevice::ReadHeaderAndVerify(
- FILE* header_file) {
- // Check size of the file is enough to store an STFS header
- xe::filesystem::Seek(header_file, 0L, SEEK_END);
- files_total_size_ = xe::filesystem::Tell(header_file);
- xe::filesystem::Seek(header_file, 0L, SEEK_SET);
-
- if (sizeof(StfsHeader) > files_total_size_) {
- return Error::kErrorTooSmall;
- }
-
- // Read header & check signature
- if (fread(&header_, sizeof(StfsHeader), 1, header_file) != 1) {
- return Error::kErrorReadError;
- }
-
- if (!header_.header.is_magic_valid()) {
- // Unexpected format.
- return Error::kErrorFileMismatch;
- }
-
- // Pre-calculate some values used in block number calculations
- if (header_.metadata.volume_type == XContentVolumeType::kStfs) {
- blocks_per_hash_table_ =
- header_.metadata.volume_descriptor.stfs.flags.bits.read_only_format ? 1
- : 2;
-
- block_step[0] = kBlocksPerHashLevel[0] + blocks_per_hash_table_;
- block_step[1] = kBlocksPerHashLevel[1] +
- ((kBlocksPerHashLevel[0] + 1) * blocks_per_hash_table_);
- }
-
- return Error::kSuccess;
-}
-
-StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
- // SVOD Systems can have different layouts. The root block is
- // denoted by the magic "MICROSOFT*XBOX*MEDIA" and is always in
- // the first "actual" data fragment of the system.
- auto& svod_header = files_.at(0);
- const char* MEDIA_MAGIC = "MICROSOFT*XBOX*MEDIA";
-
- uint8_t magic_buf[20];
- size_t magic_offset;
-
- // Check for EDGF layout
- if (header_.metadata.volume_descriptor.svod.features.bits
- .enhanced_gdf_layout) {
- // The STFS header has specified that this SVOD system uses the EGDF layout.
- // We can expect the magic block to be located immediately after the hash
- // blocks. We also offset block address calculation by 0x1000 by shifting
- // block indices by +0x2.
- xe::filesystem::Seek(svod_header, 0x2000, SEEK_SET);
- if (fread(magic_buf, 1, countof(magic_buf), svod_header) !=
- countof(magic_buf)) {
- XELOGE("ReadSVOD failed to read SVOD magic at 0x2000");
- return Error::kErrorReadError;
- }
-
- if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) {
- svod_base_offset_ = 0x0000;
- magic_offset = 0x2000;
- svod_layout_ = SvodLayoutType::kEnhancedGDF;
- XELOGI("SVOD uses an EGDF layout. Magic block present at 0x2000.");
- } else {
- XELOGE("SVOD uses an EGDF layout, but the magic block was not found.");
- return Error::kErrorFileMismatch;
- }
- } else {
- xe::filesystem::Seek(svod_header, 0x12000, SEEK_SET);
- if (fread(magic_buf, 1, countof(magic_buf), svod_header) !=
- countof(magic_buf)) {
- XELOGE("ReadSVOD failed to read SVOD magic at 0x12000");
- return Error::kErrorReadError;
- }
- if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) {
- // If the SVOD's magic block is at 0x12000, it is likely using an XSF
- // layout. This is usually due to converting the game using a third-party
- // tool, as most of them use a nulled XSF as a template.
-
- svod_base_offset_ = 0x10000;
- magic_offset = 0x12000;
-
- // Check for XSF Header
- const char* XSF_MAGIC = "XSF";
- xe::filesystem::Seek(svod_header, 0x2000, SEEK_SET);
- if (fread(magic_buf, 1, 3, svod_header) != 3) {
- XELOGE("ReadSVOD failed to read SVOD XSF magic at 0x2000");
- return Error::kErrorReadError;
- }
- if (std::memcmp(magic_buf, XSF_MAGIC, 3) == 0) {
- svod_layout_ = SvodLayoutType::kXSF;
- XELOGI("SVOD uses an XSF layout. Magic block present at 0x12000.");
- XELOGI("Game was likely converted using a third-party tool.");
- } else {
- svod_layout_ = SvodLayoutType::kUnknown;
- XELOGI("SVOD appears to use an XSF layout, but no header is present.");
- XELOGI("SVOD magic block found at 0x12000");
- }
- } else {
- xe::filesystem::Seek(svod_header, 0xD000, SEEK_SET);
- if (fread(magic_buf, 1, countof(magic_buf), svod_header) !=
- countof(magic_buf)) {
- XELOGE("ReadSVOD failed to read SVOD magic at 0xD000");
- return Error::kErrorReadError;
- }
- if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) {
- // If the SVOD's magic block is at 0xD000, it most likely means that it
- // is a single-file system. The STFS Header is 0xB000 bytes , and the
- // remaining 0x2000 is from hash tables. In most cases, these will be
- // STFS, not SVOD.
-
- svod_base_offset_ = 0xB000;
- magic_offset = 0xD000;
-
- // Check for single file system
- if (header_.metadata.data_file_count == 1) {
- svod_layout_ = SvodLayoutType::kSingleFile;
- XELOGI("SVOD is a single file. Magic block present at 0xD000.");
- } else {
- svod_layout_ = SvodLayoutType::kUnknown;
- XELOGE(
- "SVOD is not a single file, but the magic block was found at "
- "0xD000.");
- }
- } else {
- XELOGE("Could not locate SVOD magic block.");
- return Error::kErrorReadError;
- }
- }
- }
-
- // Parse the root directory
- xe::filesystem::Seek(svod_header, magic_offset + 0x14, SEEK_SET);
-
- struct {
- uint32_t block;
- uint32_t size;
- uint32_t creation_date;
- uint32_t creation_time;
- } root_data;
- static_assert_size(root_data, 0x10);
-
- if (fread(&root_data, sizeof(root_data), 1, svod_header) != 1) {
- XELOGE("ReadSVOD failed to read root block data at 0x{X}",
- magic_offset + 0x14);
- return Error::kErrorReadError;
- }
-
- uint64_t root_creation_timestamp =
- decode_fat_timestamp(root_data.creation_date, root_data.creation_time);
-
- auto root_entry = new StfsContainerEntry(this, nullptr, "", &files_);
- root_entry->attributes_ = kFileAttributeDirectory;
- root_entry->access_timestamp_ = root_creation_timestamp;
- root_entry->create_timestamp_ = root_creation_timestamp;
- root_entry->write_timestamp_ = root_creation_timestamp;
- root_entry_ = std::unique_ptr<Entry>(root_entry);
-
- // Traverse all child entries
- return ReadEntrySVOD(root_data.block, 0, root_entry);
-}
-
-StfsContainerDevice::Error StfsContainerDevice::ReadEntrySVOD(
- uint32_t block, uint32_t ordinal, StfsContainerEntry* parent) {
- // For games with a large amount of files, the ordinal offset can overrun
- // the current block and potentially hit a hash block.
- size_t ordinal_offset = ordinal * 0x4;
- size_t block_offset = ordinal_offset / 0x800;
- size_t true_ordinal_offset = ordinal_offset % 0x800;
-
- // Calculate the file & address of the block
- size_t entry_address, entry_file;
- BlockToOffsetSVOD(block + block_offset, &entry_address, &entry_file);
- entry_address += true_ordinal_offset;
-
- // Read directory entry
- auto& file = files_.at(entry_file);
- xe::filesystem::Seek(file, entry_address, SEEK_SET);
-
-#pragma pack(push, 1)
- struct {
- uint16_t node_l;
- uint16_t node_r;
- uint32_t data_block;
- uint32_t length;
- uint8_t attributes;
- uint8_t name_length;
- } dir_entry;
- static_assert_size(dir_entry, 0xE);
-#pragma pack(pop)
-
- if (fread(&dir_entry, sizeof(dir_entry), 1, file) != 1) {
- XELOGE("ReadEntrySVOD failed to read directory entry at 0x{X}",
- entry_address);
- return Error::kErrorReadError;
- }
-
- auto name_buffer = std::make_unique<char[]>(dir_entry.name_length);
- if (fread(name_buffer.get(), 1, dir_entry.name_length, file) !=
- dir_entry.name_length) {
- XELOGE("ReadEntrySVOD failed to read directory entry name at 0x{X}",
- entry_address);
- return Error::kErrorReadError;
- }
-
- auto name = std::string(name_buffer.get(), dir_entry.name_length);
-
- // Read the left node
- if (dir_entry.node_l) {
- auto node_result = ReadEntrySVOD(block, dir_entry.node_l, parent);
- if (node_result != Error::kSuccess) {
- return node_result;
- }
- }
-
- // Read file & address of block's data
- size_t data_address, data_file;
- BlockToOffsetSVOD(dir_entry.data_block, &data_address, &data_file);
-
- // Create the entry
- // NOTE: SVOD entries don't have timestamps for individual files, which can
- // cause issues when decrypting games. Using the root entry's timestamp
- // solves this issues.
- auto entry = StfsContainerEntry::Create(this, parent, name, &files_);
- if (dir_entry.attributes & kFileAttributeDirectory) {
- // Entry is a directory
- entry->attributes_ = kFileAttributeDirectory | kFileAttributeReadOnly;
- entry->data_offset_ = 0;
- entry->data_size_ = 0;
- entry->block_ = block;
- entry->access_timestamp_ = root_entry_->create_timestamp();
- entry->create_timestamp_ = root_entry_->create_timestamp();
- entry->write_timestamp_ = root_entry_->create_timestamp();
-
- if (dir_entry.length) {
- // If length is greater than 0, traverse the directory's children
- auto directory_result =
- ReadEntrySVOD(dir_entry.data_block, 0, entry.get());
- if (directory_result != Error::kSuccess) {
- return directory_result;
- }
- }
- } else {
- // Entry is a file
- entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
- entry->size_ = dir_entry.length;
- entry->allocation_size_ = xe::round_up(dir_entry.length, kBlockSize);
- entry->data_offset_ = data_address;
- entry->data_size_ = dir_entry.length;
- entry->block_ = dir_entry.data_block;
- entry->access_timestamp_ = root_entry_->create_timestamp();
- entry->create_timestamp_ = root_entry_->create_timestamp();
- entry->write_timestamp_ = root_entry_->create_timestamp();
-
- // Fill in all block records, sector by sector.
- if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
- uint32_t block_index = dir_entry.data_block;
- size_t remaining_size = xe::round_up(dir_entry.length, 0x800);
-
- size_t last_record = -1;
- size_t last_offset = -1;
- while (remaining_size) {
- const size_t BLOCK_SIZE = 0x800;
-
- size_t offset, file_index;
- BlockToOffsetSVOD(block_index, &offset, &file_index);
-
- block_index++;
- remaining_size -= BLOCK_SIZE;
-
- if (offset - last_offset == 0x800) {
- // Consecutive, so append to last entry.
- entry->block_list_[last_record].length += BLOCK_SIZE;
- last_offset = offset;
- continue;
- }
-
- entry->block_list_.push_back({file_index, offset, BLOCK_SIZE});
- last_record = entry->block_list_.size() - 1;
- last_offset = offset;
- }
- }
- }
-
- parent->children_.emplace_back(std::move(entry));
-
- // Read the right node.
- if (dir_entry.node_r) {
- auto node_result = ReadEntrySVOD(block, dir_entry.node_r, parent);
- if (node_result != Error::kSuccess) {
- return node_result;
- }
- }
-
- return Error::kSuccess;
-}
-
-void StfsContainerDevice::BlockToOffsetSVOD(size_t block, size_t* out_address,
- size_t* out_file_index) {
- // SVOD Systems use hash blocks for integrity checks. These hash blocks
- // cause blocks to be discontinuous in memory, and must be accounted for.
- // - Each data block is 0x800 bytes in length
- // - Every group of 0x198 data blocks is preceded a Level0 hash table.
- // Level0 tables contain 0xCC hashes, each representing two data blocks.
- // The total size of each Level0 hash table is 0x1000 bytes in length.
- // - Every 0xA1C4 Level0 hash tables is preceded by a Level1 hash table.
- // Level1 tables contain 0xCB hashes, each representing two Level0 hashes.
- // The total size of each Level1 hash table is 0x1000 bytes in length.
- // - Files are split into fragments of 0xA290000 bytes in length,
- // consisting of 0x14388 data blocks, 0xCB Level0 hash tables, and 0x1
- // Level1 hash table.
-
- const size_t BLOCK_SIZE = 0x800;
- const size_t HASH_BLOCK_SIZE = 0x1000;
- const size_t BLOCKS_PER_L0_HASH = 0x198;
- const size_t HASHES_PER_L1_HASH = 0xA1C4;
- const size_t BLOCKS_PER_FILE = 0x14388;
- const size_t MAX_FILE_SIZE = 0xA290000;
- const size_t BLOCK_OFFSET =
- header_.metadata.volume_descriptor.svod.start_data_block();
-
- // Resolve the true block address and file index
- size_t true_block = block - (BLOCK_OFFSET * 2);
- if (svod_layout_ == SvodLayoutType::kEnhancedGDF) {
- // EGDF has an 0x1000 byte offset, which is two blocks
- true_block += 0x2;
- }
-
- size_t file_block = true_block % BLOCKS_PER_FILE;
- size_t file_index = true_block / BLOCKS_PER_FILE;
- size_t offset = 0;
-
- // Calculate offset caused by Level0 Hash Tables
- size_t level0_table_count = (file_block / BLOCKS_PER_L0_HASH) + 1;
- offset += level0_table_count * HASH_BLOCK_SIZE;
-
- // Calculate offset caused by Level1 Hash Tables
- size_t level1_table_count = (level0_table_count / HASHES_PER_L1_HASH) + 1;
- offset += level1_table_count * HASH_BLOCK_SIZE;
-
- // For single-file SVOD layouts, include the size of the header in the offset.
- if (svod_layout_ == SvodLayoutType::kSingleFile) {
- offset += svod_base_offset_;
- }
-
- size_t block_address = (file_block * BLOCK_SIZE) + offset;
-
- // If the offset causes the block address to overrun the file, round it.
- if (block_address >= MAX_FILE_SIZE) {
- file_index += 1;
- block_address %= MAX_FILE_SIZE;
- block_address += 0x2000;
- }
-
- *out_address = block_address;
- *out_file_index = file_index;
-}
-
-StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
- auto& file = files_.at(0);
-
- auto root_entry = new StfsContainerEntry(this, nullptr, "", &files_);
- root_entry->attributes_ = kFileAttributeDirectory;
- root_entry_ = std::unique_ptr<Entry>(root_entry);
-
- std::vector<StfsContainerEntry*> all_entries;
-
- // Load all listings.
- StfsDirectoryBlock directory;
-
- auto& descriptor = header_.metadata.volume_descriptor.stfs;
- uint32_t table_block_index = descriptor.file_table_block_number();
- size_t n = 0;
- for (n = 0; n < descriptor.file_table_block_count; n++) {
- auto offset = BlockToOffsetSTFS(table_block_index);
- xe::filesystem::Seek(file, offset, SEEK_SET);
-
- if (fread(&directory, sizeof(StfsDirectoryBlock), 1, file) != 1) {
- XELOGE("ReadSTFS failed to read directory block at 0x{X}", offset);
- return Error::kErrorReadError;
- }
-
- for (size_t m = 0; m < kEntriesPerDirectoryBlock; m++) {
- auto& dir_entry = directory.entries[m];
-
- if (dir_entry.name[0] == 0) {
- // Done.
- break;
- }
-
- StfsContainerEntry* parent_entry = nullptr;
- if (dir_entry.directory_index == 0xFFFF) {
- parent_entry = root_entry;
- } else {
- parent_entry = all_entries[dir_entry.directory_index];
- }
-
- std::string name(reinterpret_cast<const char*>(dir_entry.name),
- dir_entry.flags.name_length & 0x3F);
- auto entry =
- StfsContainerEntry::Create(this, parent_entry, name, &files_);
-
- if (dir_entry.flags.directory) {
- entry->attributes_ = kFileAttributeDirectory;
- } else {
- entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
- entry->data_offset_ = BlockToOffsetSTFS(dir_entry.start_block_number());
- entry->data_size_ = dir_entry.length;
- }
- entry->size_ = dir_entry.length;
- entry->allocation_size_ = xe::round_up(dir_entry.length, kBlockSize);
-
- entry->create_timestamp_ =
- decode_fat_timestamp(dir_entry.create_date, dir_entry.create_time);
- entry->write_timestamp_ = decode_fat_timestamp(dir_entry.modified_date,
- dir_entry.modified_time);
- entry->access_timestamp_ = entry->write_timestamp_;
-
- all_entries.push_back(entry.get());
-
- // Fill in all block records.
- // It's easier to do this now and just look them up later, at the cost
- // of some memory. Nasty chain walk.
- // TODO(benvanik): optimize if flags.contiguous is set.
- if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
- uint32_t block_index = dir_entry.start_block_number();
- size_t remaining_size = dir_entry.length;
- while (remaining_size && block_index != kEndOfChain) {
- size_t block_size =
- std::min(static_cast<size_t>(kBlockSize), remaining_size);
- size_t offset = BlockToOffsetSTFS(block_index);
- entry->block_list_.push_back({0, offset, block_size});
- remaining_size -= block_size;
- auto block_hash = GetBlockHash(block_index);
- block_index = block_hash->level0_next_block();
- }
-
- if (remaining_size) {
- // Loop above must have exited prematurely, bad hash tables?
- XELOGW(
- "STFS file {} only found {} bytes for file, expected {} ({} "
- "bytes missing)",
- name, dir_entry.length - remaining_size, dir_entry.length,
- remaining_size);
- assert_always();
- }
-
- // Check that the number of blocks retrieved from hash entries matches
- // the block count read from the file entry
- if (entry->block_list_.size() != dir_entry.allocated_data_blocks()) {
- XELOGW(
- "STFS failed to read correct block-chain for entry {}, read {} "
- "blocks, expected {}",
- entry->name_, entry->block_list_.size(),
- dir_entry.allocated_data_blocks());
- assert_always();
- }
- }
-
- parent_entry->children_.emplace_back(std::move(entry));
- }
-
- auto block_hash = GetBlockHash(table_block_index);
- table_block_index = block_hash->level0_next_block();
- if (table_block_index == kEndOfChain) {
- break;
- }
- }
-
- if (n + 1 != descriptor.file_table_block_count) {
- XELOGW("STFS read {} file table blocks, but STFS headers expected {}!",
- n + 1, descriptor.file_table_block_count);
- assert_always();
- }
-
- return Error::kSuccess;
-}
-
-size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) const {
- // For every level there is a hash table
- // Level 0: hash table of next 170 blocks
- // Level 1: hash table of next 170 hash tables
- // Level 2: hash table of next 170 level 1 hash tables
- // And so on...
- uint64_t base = kBlocksPerHashLevel[0];
- uint64_t block = block_index;
- for (uint32_t i = 0; i < 3; i++) {
- block += ((block_index + base) / base) * blocks_per_hash_table_;
- if (block_index < base) {
- break;
- }
-
- base *= kBlocksPerHashLevel[0];
- }
-
- return xe::round_up(header_.header.header_size, kBlockSize) + (block << 12);
-}
-
-uint32_t StfsContainerDevice::BlockToHashBlockNumberSTFS(
- uint32_t block_index, uint32_t hash_level) const {
- uint32_t block = 0;
- if (hash_level == 0) {
- if (block_index < kBlocksPerHashLevel[0]) {
- return 0;
- }
-
- block = (block_index / kBlocksPerHashLevel[0]) * block_step[0];
- block +=
- ((block_index / kBlocksPerHashLevel[1]) + 1) * blocks_per_hash_table_;
-
- if (block_index < kBlocksPerHashLevel[1]) {
- return block;
- }
-
- return block + blocks_per_hash_table_;
- }
-
- if (hash_level == 1) {
- if (block_index < kBlocksPerHashLevel[1]) {
- return block_step[0];
- }
-
- block = (block_index / kBlocksPerHashLevel[1]) * block_step[1];
- return block + blocks_per_hash_table_;
- }
-
- // Level 2 is always at blockStep1
- return block_step[1];
-}
-
-size_t StfsContainerDevice::BlockToHashBlockOffsetSTFS(
- uint32_t block_index, uint32_t hash_level) const {
- uint64_t block = BlockToHashBlockNumberSTFS(block_index, hash_level);
- return xe::round_up(header_.header.header_size, kBlockSize) + (block << 12);
-}
-
-const StfsHashEntry* StfsContainerDevice::GetBlockHash(uint32_t block_index) {
- auto& file = files_.at(0);
-
- auto& descriptor = header_.metadata.volume_descriptor.stfs;
-
- // Offset for selecting the secondary hash block, in packages that have them
- uint32_t secondary_table_offset =
- descriptor.flags.bits.root_active_index ? kBlockSize : 0;
-
- auto hash_offset_lv0 = BlockToHashBlockOffsetSTFS(block_index, 0);
- if (!cached_hash_tables_.count(hash_offset_lv0)) {
- // If this is read_only_format then it doesn't contain secondary blocks, no
- // need to check upper hash levels
- if (descriptor.flags.bits.read_only_format) {
- secondary_table_offset = 0;
- } else {
- // Not a read-only package, need to check each levels active index flag to
- // see if we need to use secondary block or not
-
- // Check level1 table if package has it
- if (descriptor.total_block_count > kBlocksPerHashLevel[0]) {
- auto hash_offset_lv1 = BlockToHashBlockOffsetSTFS(block_index, 1);
-
- if (!cached_hash_tables_.count(hash_offset_lv1)) {
- // Check level2 table if package has it
- if (descriptor.total_block_count > kBlocksPerHashLevel[1]) {
- auto hash_offset_lv2 = BlockToHashBlockOffsetSTFS(block_index, 2);
-
- if (!cached_hash_tables_.count(hash_offset_lv2)) {
- xe::filesystem::Seek(
- file, hash_offset_lv2 + secondary_table_offset, SEEK_SET);
-
- StfsHashTable table_lv2;
- if (fread(&table_lv2, sizeof(StfsHashTable), 1, file) != 1) {
- XELOGE("GetBlockHash failed to read level2 hash table at 0x{X}",
- hash_offset_lv2 + secondary_table_offset);
- return nullptr;
- }
- cached_hash_tables_[hash_offset_lv2] = table_lv2;
- }
-
- auto record =
- (block_index / kBlocksPerHashLevel[1]) % kBlocksPerHashLevel[0];
- auto record_data =
- &cached_hash_tables_[hash_offset_lv2].entries[record];
- secondary_table_offset =
- record_data->levelN_active_index() ? kBlockSize : 0;
- }
-
- xe::filesystem::Seek(file, hash_offset_lv1 + secondary_table_offset,
- SEEK_SET);
-
- StfsHashTable table_lv1;
- if (fread(&table_lv1, sizeof(StfsHashTable), 1, file) != 1) {
- XELOGE("GetBlockHash failed to read level1 hash table at 0x{X}",
- hash_offset_lv1 + secondary_table_offset);
- return nullptr;
- }
- cached_hash_tables_[hash_offset_lv1] = table_lv1;
- }
-
- auto record =
- (block_index / kBlocksPerHashLevel[0]) % kBlocksPerHashLevel[0];
- auto record_data =
- &cached_hash_tables_[hash_offset_lv1].entries[record];
- secondary_table_offset =
- record_data->levelN_active_index() ? kBlockSize : 0;
- }
- }
-
- xe::filesystem::Seek(file, hash_offset_lv0 + secondary_table_offset,
- SEEK_SET);
-
- StfsHashTable table_lv0;
- if (fread(&table_lv0, sizeof(StfsHashTable), 1, file) != 1) {
- XELOGE("GetBlockHash failed to read level0 hash table at 0x{X}",
- hash_offset_lv0 + secondary_table_offset);
- return nullptr;
- }
- cached_hash_tables_[hash_offset_lv0] = table_lv0;
- }
-
- auto record = block_index % kBlocksPerHashLevel[0];
- auto record_data = &cached_hash_tables_[hash_offset_lv0].entries[record];
-
- return record_data;
-}
-
-XContentPackageType StfsContainerDevice::ReadMagic(
- const std::filesystem::path& path) {
- auto map = MappedMemory::Open(path, MappedMemory::Mode::kRead, 0, 4);
- return XContentPackageType(xe::load_and_swap<uint32_t>(map->data()));
-}
-
-bool StfsContainerDevice::ResolveFromFolder(const std::filesystem::path& path) {
- // Scan through folders until a file with magic is found
- std::queue<filesystem::FileInfo> queue;
-
- filesystem::FileInfo folder;
- filesystem::GetInfo(host_path_, &folder);
- queue.push(folder);
-
- while (!queue.empty()) {
- auto current_file = queue.front();
- queue.pop();
-
- if (current_file.type == filesystem::FileInfo::Type::kDirectory) {
- auto path = current_file.path / current_file.name;
- auto child_files = filesystem::ListFiles(path);
- for (auto file : child_files) {
- queue.push(file);
- }
- } else {
- // Try to read the file's magic
- auto path = current_file.path / current_file.name;
- auto magic = ReadMagic(path);
-
- if (magic == XContentPackageType::kCon ||
- magic == XContentPackageType::kLive ||
- magic == XContentPackageType::kPirs) {
- host_path_ = current_file.path / current_file.name;
- XELOGI("STFS Package found: {}", xe::path_to_utf8(host_path_));
- return true;
- }
- }
- }
-
- if (host_path_ == path) {
- // Could not find a suitable container file
- return false;
- }
- return true;
-}
-
-} // namespace vfs
-} // namespace xe
diff --git a/src/xenia/vfs/devices/stfs_container_device.h b/src/xenia/vfs/devices/stfs_container_device.h
deleted file mode 100644
index 504e22cfd0..0000000000
--- a/src/xenia/vfs/devices/stfs_container_device.h
+++ /dev/null
@@ -1,148 +0,0 @@
-/**
- ******************************************************************************
- * Xenia : Xbox 360 Emulator Research Project *
- ******************************************************************************
- * Copyright 2020 Ben Vanik. All rights reserved. *
- * Released under the BSD license - see LICENSE in the root for more details. *
- ******************************************************************************
- */
-
-#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
-#define XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
-
-#include <map>
-#include <memory>
-#include <string>
-#include <unordered_map>
-
-#include "xenia/base/math.h"
-#include "xenia/base/string_util.h"
-#include "xenia/kernel/util/xex2_info.h"
-#include "xenia/vfs/device.h"
-#include "xenia/vfs/devices/stfs_xbox.h"
-
-namespace xe {
-namespace vfs {
-
-// https://free60project.github.io/wiki/STFS.html
-
-class StfsContainerEntry;
-
-class StfsContainerDevice : public Device {
- public:
- const static uint32_t kBlockSize = 0x1000;
-
- StfsContainerDevice(const std::string_view mount_path,
- const std::filesystem::path& host_path);
- ~StfsContainerDevice() override;
-
- bool Initialize() override;
-
- bool is_read_only() const override {
- return header_.metadata.volume_type != XContentVolumeType::kStfs ||
- header_.metadata.volume_descriptor.stfs.flags.bits.read_only_format;
- }
-
- void Dump(StringBuffer* string_buffer) override;
- Entry* ResolvePath(const std::string_view path) override;
-
- const std::string& name() const override { return name_; }
- uint32_t attributes() const override { return 0; }
- uint32_t component_name_max_length() const override { return 40; }
-
- uint32_t total_allocation_units() const override {
- if (header_.metadata.volume_type == XContentVolumeType::kStfs) {
- return header_.metadata.volume_descriptor.stfs.total_block_count;
- }
-
- return uint32_t(data_size() / sectors_per_allocation_unit() /
- bytes_per_sector());
- }
- uint32_t available_allocation_units() const override {
- if (!is_read_only()) {
- auto& descriptor = header_.metadata.volume_descriptor.stfs;
- return kBlocksPerHashLevel[2] -
- (descriptor.total_block_count - descriptor.free_block_count);
- }
- return 0;
- }
- uint32_t sectors_per_allocation_unit() const override { return 8; }
- uint32_t bytes_per_sector() const override { return 0x200; }
-
- size_t data_size() const {
- if (header_.header.header_size) {
- if (header_.metadata.volume_type == XContentVolumeType::kStfs) {
- return header_.metadata.volume_descriptor.stfs.total_block_count *
- kBlockSize;
- }
- return files_total_size_ -
- xe::round_up(header_.header.header_size, kBlockSize);
- }
- return files_total_size_ - sizeof(StfsHeader);
- }
-
- private:
- const uint32_t kBlocksPerHashLevel[3] = {170, 28900, 4913000};
- const uint32_t kEndOfChain = 0xFFFFFF;
- const uint32_t kEntriesPerDirectoryBlock =
- kBlockSize / sizeof(StfsDirectoryEntry);
-
- enum class Error {
- kSuccess = 0,
- kErrorOutOfMemory = -1,
- kErrorReadError = -10,
- kErrorFileMismatch = -30,
- kErrorDamagedFile = -31,
- kErrorTooSmall = -32,
- };
-
- enum class SvodLayoutType {
- kUnknown = 0x0,
- kEnhancedGDF = 0x1,
- kXSF = 0x2,
- kSingleFile = 0x4,
- };
-
- XContentPackageType ReadMagic(const std::filesystem::path& path);
- bool ResolveFromFolder(const std::filesystem::path& path);
-
- Error OpenFiles();
- void CloseFiles();
-
- Error ReadHeaderAndVerify(FILE* header_file);
-
- Error ReadSVOD();
- Error ReadEntrySVOD(uint32_t sector, uint32_t ordinal,
- StfsContainerEntry* parent);
- void BlockToOffsetSVOD(size_t sector, size_t* address, size_t* file_index);
-
- Error ReadSTFS();
- size_t BlockToOffsetSTFS(uint64_t block_index) const;
- uint32_t BlockToHashBlockNumberSTFS(uint32_t block_index,
- uint32_t hash_level) const;
- size_t BlockToHashBlockOffsetSTFS(uint32_t block_index,
- uint32_t hash_level) const;
-
- const StfsHashEntry* GetBlockHash(uint32_t block_index);
-
- std::string name_;
- std::filesystem::path host_path_;
-
- std::map<size_t, FILE*> files_;
- size_t files_total_size_;
-
- size_t svod_base_offset_;
-
- std::unique_ptr<Entry> root_entry_;
- StfsHeader header_;
- SvodLayoutType svod_layout_;
- uint32_t blocks_per_hash_table_;
- uint32_t block_step[2];
-
- std::unordered_map<size_t, StfsHashTable> cached_hash_tables_;
-};
-
-} // namespace vfs
-} // namespace xe
-
-#endif // XENIA_VFS_DEVICES_STFS_CONTAINER_DEVICE_H_
diff --git a/src/xenia/vfs/devices/stfs_xbox.h b/src/xenia/vfs/devices/stfs_xbox.h
index 0cba213937..8763f0b62c 100644
--- a/src/xenia/vfs/devices/stfs_xbox.h
+++ b/src/xenia/vfs/devices/stfs_xbox.h
@@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2021 Ben Vanik. All rights reserved. *
+ * Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@@ -13,9 +13,33 @@
#include "xenia/base/string_util.h"
#include "xenia/kernel/util/xex2_info.h"
+#if XE_PLATFORM_WIN32
+#include "xenia/base/platform_win.h"
+#define timegm _mkgmtime
+#endif
+
namespace xe {
namespace vfs {
+// Convert FAT timestamp to 100-nanosecond intervals since January 1, 1601 (UTC)
+inline uint64_t decode_fat_timestamp(uint32_t date, uint32_t time) {
+ struct tm tm = {0};
+ // 80 is the difference between 1980 (FAT) and 1900 (tm);
+ tm.tm_year = ((0xFE00 & date) >> 9) + 80;
+ tm.tm_mon = ((0x01E0 & date) >> 5) - 1;
+ tm.tm_mday = (0x001F & date) >> 0;
+ tm.tm_hour = (0xF800 & time) >> 11;
+ tm.tm_min = (0x07E0 & time) >> 5;
+ tm.tm_sec = (0x001F & time) << 1; // the value stored in 2-seconds intervals
+ tm.tm_isdst = 0;
+ time_t timet = timegm(&tm);
+ if (timet == -1) {
+ return 0;
+ }
+ // 11644473600LL is a difference between 1970 and 1601
+ return (timet + 11644473600LL) * 10000000;
+}
+
// Structs used for interchange between Xenia and actual Xbox360 kernel/XAM
inline uint32_t load_uint24_be(const uint8_t* p) {
@@ -455,13 +479,21 @@ struct XContentHeader {
static_assert_size(XContentHeader, 0x344);
#pragma pack(pop)
-struct StfsHeader {
- XContentHeader header;
- XContentMetadata metadata;
+struct XContentContainerHeader {
+ XContentHeader content_header;
+ XContentMetadata content_metadata;
// TODO: title/system updates contain more data after XContentMetadata, seems
// to affect header.header_size
+
+ bool is_package_readonly() const {
+ if (content_metadata.volume_type == vfs::XContentVolumeType::kSvod) {
+ return true;
+ }
+
+ return content_metadata.volume_descriptor.stfs.flags.bits.read_only_format;
+ }
};
-static_assert_size(StfsHeader, 0x971A);
+static_assert_size(XContentContainerHeader, 0x971A);
} // namespace vfs
} // namespace xe
diff --git a/src/xenia/vfs/devices/xcontent_container_device.cc b/src/xenia/vfs/devices/xcontent_container_device.cc
new file mode 100644
index 0000000000..49c3ccc5f2
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_container_device.cc
@@ -0,0 +1,165 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#include "xenia/base/logging.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
+#include "xenia/vfs/devices/xcontent_devices/stfs_container_device.h"
+#include "xenia/vfs/devices/xcontent_devices/svod_container_device.h"
+
+namespace xe {
+namespace vfs {
+
+std::unique_ptr<Device> XContentContainerDevice::CreateContentDevice(
+ const std::string_view mount_path, const std::filesystem::path& host_path) {
+ if (!std::filesystem::exists(host_path)) {
+ XELOGE("Path to XContent container does not exist: {}",
+ xe::path_to_utf8(host_path));
+ return nullptr;
+ }
+
+ if (std::filesystem::is_directory(host_path)) {
+ return nullptr;
+ }
+
+ FILE* host_file = xe::filesystem::OpenFile(host_path, "rb");
+ if (!host_file) {
+ XELOGE("Error opening XContent file.");
+ return nullptr;
+ }
+
+ const uint64_t package_size = std::filesystem::file_size(host_path);
+ if (package_size < sizeof(XContentContainerHeader)) {
+ return nullptr;
+ }
+
+ const auto header = XContentContainerDevice::ReadContainerHeader(host_file);
+ if (header == nullptr) {
+ return nullptr;
+ }
+
+ fclose(host_file);
+
+ if (!header->content_header.is_magic_valid()) {
+ return nullptr;
+ }
+
+ switch (header->content_metadata.volume_type) {
+ case XContentVolumeType::kStfs:
+ return std::make_unique<StfsContainerDevice>(mount_path, host_path);
+ break;
+ case XContentVolumeType::kSvod:
+ return std::make_unique<SvodContainerDevice>(mount_path, host_path);
+ break;
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+XContentContainerDevice::XContentContainerDevice(
+ const std::string_view mount_path, const std::filesystem::path& host_path)
+ : Device(mount_path),
+ name_("XContent"),
+ host_path_(host_path),
+ files_total_size_(0),
+ header_(std::make_unique<XContentContainerHeader>()) {}
+
+XContentContainerDevice::~XContentContainerDevice() {}
+
+bool XContentContainerDevice::Initialize() {
+ if (!std::filesystem::exists(host_path_)) {
+ XELOGE("Path to XContent container does not exist: {}",
+ xe::path_to_utf8(host_path_));
+ return false;
+ }
+
+ if (std::filesystem::is_directory(host_path_)) {
+ return false;
+ }
+
+ XELOGI("Loading XContent header file: {}", xe::path_to_utf8(host_path_));
+ auto header_file = xe::filesystem::OpenFile(host_path_, "rb");
+ if (!header_file) {
+ XELOGE("Error opening XContent header file.");
+ return false;
+ }
+
+ auto header_loading_result = ReadHeaderAndVerify(header_file);
+ if (header_loading_result != Result::kSuccess) {
+ XELOGE("Error reading XContent header: {}", header_loading_result);
+ fclose(header_file);
+ return false;
+ }
+
+ SetupContainer();
+
+ if (LoadHostFiles(header_file) != Result::kSuccess) {
+ XELOGE("Error loading XContent host files.");
+ return false;
+ }
+
+ return Read() == Result::kSuccess;
+}
+
+XContentContainerHeader* XContentContainerDevice::ReadContainerHeader(
+ FILE* host_file) {
+ XContentContainerHeader* header = new XContentContainerHeader();
+ // Read header & check signature
+ if (fread(header, sizeof(XContentContainerHeader), 1, host_file) != 1) {
+ return nullptr;
+ }
+ return header;
+}
+
+Entry* XContentContainerDevice::ResolvePath(const std::string_view path) {
+ // The filesystem will have stripped our prefix off already, so the path will
+ // be in the form:
+ // some\PATH.foo
+ XELOGFS("StfsContainerDevice::ResolvePath({})", path);
+ return root_entry_->ResolvePath(path);
+}
+
+void XContentContainerDevice::Dump(StringBuffer* string_buffer) {
+ auto global_lock = global_critical_region_.Acquire();
+ root_entry_->Dump(string_buffer, 0);
+}
+
+void XContentContainerDevice::CloseFiles() {
+ for (auto& file : files_) {
+ fclose(file.second);
+ }
+ files_.clear();
+ files_total_size_ = 0;
+}
+
+XContentContainerDevice::Result XContentContainerDevice::ReadHeaderAndVerify(
+ FILE* header_file) {
+ files_total_size_ = std::filesystem::file_size(host_path_);
+ if (files_total_size_ < sizeof(XContentContainerHeader)) {
+ return Result::kTooSmall;
+ }
+
+ const XContentContainerHeader* header = ReadContainerHeader(header_file);
+ if (header == nullptr) {
+ return Result::kReadError;
+ }
+
+ std::memcpy(header_.get(), header, sizeof(XContentContainerHeader));
+
+ if (!header_->content_header.is_magic_valid()) {
+ // Unexpected format.
+ return Result::kFileMismatch;
+ }
+
+ return Result::kSuccess;
+}
+
+} // namespace vfs
+} // namespace xe
\ No newline at end of file
diff --git a/src/xenia/vfs/devices/xcontent_container_device.h b/src/xenia/vfs/devices/xcontent_container_device.h
new file mode 100644
index 0000000000..954565c09d
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_container_device.h
@@ -0,0 +1,98 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#ifndef XENIA_VFS_DEVICES_XCONTENT_CONTAINER_DEVICE_H_
+#define XENIA_VFS_DEVICES_XCONTENT_CONTAINER_DEVICE_H_
+
+#include <filesystem>
+#include <map>
+#include <string_view>
+
+#include "xenia/base/math.h"
+#include "xenia/kernel/util/xex2_info.h"
+#include "xenia/vfs/device.h"
+#include "xenia/vfs/devices/stfs_xbox.h"
+
+namespace xe {
+namespace vfs {
+class XContentContainerDevice : public Device {
+ public:
+ const static uint32_t kBlockSize = 0x1000;
+
+ static std::unique_ptr<Device> CreateContentDevice(
+ const std::string_view mount_path,
+ const std::filesystem::path& host_path);
+
+ ~XContentContainerDevice() override;
+
+ bool Initialize();
+
+ const std::string& name() const override { return name_; }
+ uint32_t attributes() const override { return 0; }
+
+ uint32_t sectors_per_allocation_unit() const override { return 8; }
+ uint32_t bytes_per_sector() const override { return 0x200; }
+
+ size_t data_size() const {
+ if (header_->content_header.header_size) {
+ return files_total_size_ -
+ xe::round_up(header_->content_header.header_size, kBlockSize);
+ }
+ return files_total_size_ - sizeof(XContentContainerHeader);
+ }
+
+ protected:
+ XContentContainerDevice(const std::string_view mount_path,
+ const std::filesystem::path& host_path);
+ enum class Result {
+ kSuccess = 0,
+ kOutOfMemory = -1,
+ kReadError = -10,
+ kFileMismatch = -30,
+ kDamagedFile = -31,
+ kTooSmall = -32,
+ };
+
+ virtual Result Read() = 0;
+ // Load all host files. Usually STFS is only 1 file, meanwhile SVOD is usually multiple file.
+ virtual Result LoadHostFiles(FILE* header_file) = 0;
+ // Initialize any container specific fields.
+ virtual void SetupContainer() { };
+
+ Entry* ResolvePath(const std::string_view path);
+ void CloseFiles();
+ void Dump(StringBuffer* string_buffer);
+ Result ReadHeaderAndVerify(FILE* header_file);
+
+ void SetName(std::string name) { name_ = name; }
+ const std::string& GetName() const { return name_; }
+
+ void SetFilesSize(uint64_t files_size) { files_total_size_ = files_size; }
+ const uint64_t GetFilesSize() const { return files_total_size_; }
+
+ const std::filesystem::path& GetHostPath() const { return host_path_; }
+
+ const XContentContainerHeader* GetContainerHeader() const { return header_.get(); }
+
+ std::string name_;
+ std::filesystem::path host_path_;
+
+ std::map<size_t, FILE*> files_;
+ size_t files_total_size_;
+ std::unique_ptr<Entry> root_entry_;
+ std::unique_ptr<XContentContainerHeader> header_;
+
+ private:
+ static XContentContainerHeader* ReadContainerHeader(FILE* host_file);
+};
+
+} // namespace vfs
+} // namespace xe
+
+#endif
diff --git a/src/xenia/vfs/devices/stfs_container_entry.cc b/src/xenia/vfs/devices/xcontent_container_entry.cc
similarity index 51%
rename from src/xenia/vfs/devices/stfs_container_entry.cc
rename to src/xenia/vfs/devices/xcontent_container_entry.cc
index 1197460cc9..1f264dfec5 100644
--- a/src/xenia/vfs/devices/stfs_container_entry.cc
+++ b/src/xenia/vfs/devices/xcontent_container_entry.cc
@@ -2,43 +2,43 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2020 Ben Vanik. All rights reserved. *
+ * Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
-#include "xenia/vfs/devices/stfs_container_entry.h"
-#include "xenia/base/math.h"
-#include "xenia/vfs/devices/stfs_container_file.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+#include "xenia/vfs/devices/xcontent_container_file.h"
#include <map>
namespace xe {
namespace vfs {
-StfsContainerEntry::StfsContainerEntry(Device* device, Entry* parent,
- const std::string_view path,
- MultiFileHandles* files)
+XContentContainerEntry::XContentContainerEntry(Device* device, Entry* parent,
+ const std::string_view path,
+ MultiFileHandles* files)
: Entry(device, parent, path),
files_(files),
data_offset_(0),
data_size_(0),
block_(0) {}
-StfsContainerEntry::~StfsContainerEntry() = default;
+XContentContainerEntry::~XContentContainerEntry() = default;
-std::unique_ptr<StfsContainerEntry> StfsContainerEntry::Create(
+std::unique_ptr<XContentContainerEntry> XContentContainerEntry::Create(
Device* device, Entry* parent, const std::string_view name,
MultiFileHandles* files) {
auto path = xe::utf8::join_guest_paths(parent->path(), name);
auto entry =
- std::make_unique<StfsContainerEntry>(device, parent, path, files);
+ std::make_unique<XContentContainerEntry>(device, parent, path, files);
return std::move(entry);
}
-X_STATUS StfsContainerEntry::Open(uint32_t desired_access, File** out_file) {
- *out_file = new StfsContainerFile(desired_access, this);
+X_STATUS XContentContainerEntry::Open(uint32_t desired_access,
+ File** out_file) {
+ *out_file = new XContentContainerFile(desired_access, this);
return X_STATUS_SUCCESS;
}
diff --git a/src/xenia/vfs/devices/stfs_container_entry.h b/src/xenia/vfs/devices/xcontent_container_entry.h
similarity index 63%
rename from src/xenia/vfs/devices/stfs_container_entry.h
rename to src/xenia/vfs/devices/xcontent_container_entry.h
index 0990b97ce7..8022a088ab 100644
--- a/src/xenia/vfs/devices/stfs_container_entry.h
+++ b/src/xenia/vfs/devices/xcontent_container_entry.h
@@ -2,13 +2,13 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2020 Ben Vanik. All rights reserved. *
+ * Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
-#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
-#define XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
+#ifndef XENIA_VFS_DEVICES_XCONTENT_CONTAINER_ENTRY_H_
+#define XENIA_VFS_DEVICES_XCONTENT_CONTAINER_ENTRY_H_
#include <map>
#include <string>
@@ -21,18 +21,17 @@ namespace xe {
namespace vfs {
typedef std::map<size_t, FILE*> MultiFileHandles;
-class StfsContainerDevice;
+class XContentContainerDevice;
-class StfsContainerEntry : public Entry {
+class XContentContainerEntry : public Entry {
public:
- StfsContainerEntry(Device* device, Entry* parent, const std::string_view path,
- MultiFileHandles* files);
- ~StfsContainerEntry() override;
+ XContentContainerEntry(Device* device, Entry* parent,
+ const std::string_view path, MultiFileHandles* files);
+ ~XContentContainerEntry() override;
- static std::unique_ptr<StfsContainerEntry> Create(Device* device,
- Entry* parent,
- const std::string_view name,
- MultiFileHandles* files);
+ static std::unique_ptr<XContentContainerEntry> Create(
+ Device* device, Entry* parent, const std::string_view name,
+ MultiFileHandles* files);
MultiFileHandles* files() const { return files_; }
size_t data_offset() const { return data_offset_; }
@@ -50,6 +49,7 @@ class StfsContainerEntry : public Entry {
private:
friend class StfsContainerDevice;
+ friend class SvodContainerDevice;
MultiFileHandles* files_;
size_t data_offset_;
@@ -61,4 +61,4 @@ class StfsContainerEntry : public Entry {
} // namespace vfs
} // namespace xe
-#endif // XENIA_VFS_DEVICES_STFS_CONTAINER_ENTRY_H_
\ No newline at end of file
+#endif // XENIA_VFS_DEVICES_XCONTENT_CONTAINER_ENTRY_H_
\ No newline at end of file
diff --git a/src/xenia/vfs/devices/stfs_container_file.cc b/src/xenia/vfs/devices/xcontent_container_file.cc
similarity index 77%
rename from src/xenia/vfs/devices/stfs_container_file.cc
rename to src/xenia/vfs/devices/xcontent_container_file.cc
index 791d791016..6c2b918164 100644
--- a/src/xenia/vfs/devices/stfs_container_file.cc
+++ b/src/xenia/vfs/devices/xcontent_container_file.cc
@@ -2,31 +2,30 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2014 Ben Vanik. All rights reserved. *
+ * Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
-#include "xenia/vfs/devices/stfs_container_file.h"
-
#include <algorithm>
#include <cmath>
#include "xenia/base/math.h"
-#include "xenia/vfs/devices/stfs_container_entry.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+#include "xenia/vfs/devices/xcontent_container_file.h"
namespace xe {
namespace vfs {
-StfsContainerFile::StfsContainerFile(uint32_t file_access,
- StfsContainerEntry* entry)
+XContentContainerFile::XContentContainerFile(uint32_t file_access,
+ XContentContainerEntry* entry)
: File(file_access, entry), entry_(entry) {}
-StfsContainerFile::~StfsContainerFile() = default;
+XContentContainerFile::~XContentContainerFile() = default;
-void StfsContainerFile::Destroy() { delete this; }
+void XContentContainerFile::Destroy() { delete this; }
-X_STATUS StfsContainerFile::ReadSync(void* buffer, size_t buffer_length,
+X_STATUS XContentContainerFile::ReadSync(void* buffer, size_t buffer_length,
size_t byte_offset,
size_t* out_bytes_read) {
if (byte_offset >= entry_->size()) {
diff --git a/src/xenia/vfs/devices/stfs_container_file.h b/src/xenia/vfs/devices/xcontent_container_file.h
similarity index 68%
rename from src/xenia/vfs/devices/stfs_container_file.h
rename to src/xenia/vfs/devices/xcontent_container_file.h
index d680dffe52..e2b46bb7af 100644
--- a/src/xenia/vfs/devices/stfs_container_file.h
+++ b/src/xenia/vfs/devices/xcontent_container_file.h
@@ -2,13 +2,13 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
- * Copyright 2014 Ben Vanik. All rights reserved. *
+ * Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
-#ifndef XENIA_VFS_DEVICES_STFS_CONTAINER_FILE_H_
-#define XENIA_VFS_DEVICES_STFS_CONTAINER_FILE_H_
+#ifndef XENIA_VFS_DEVICES_XCONTENT_CONTAINER_FILE_H_
+#define XENIA_VFS_DEVICES_XCONTENT_CONTAINER_FILE_H_
#include "xenia/vfs/file.h"
@@ -17,12 +17,12 @@
namespace xe {
namespace vfs {
-class StfsContainerEntry;
+class XContentContainerEntry;
-class StfsContainerFile : public File {
+class XContentContainerFile : public File {
public:
- StfsContainerFile(uint32_t file_access, StfsContainerEntry* entry);
- ~StfsContainerFile() override;
+ XContentContainerFile(uint32_t file_access, XContentContainerEntry* entry);
+ ~XContentContainerFile() override;
void Destroy() override;
@@ -35,10 +35,10 @@ class StfsContainerFile : public File {
X_STATUS SetLength(size_t length) override { return X_STATUS_ACCESS_DENIED; }
private:
- StfsContainerEntry* entry_;
+ XContentContainerEntry* entry_;
};
} // namespace vfs
} // namespace xe
-#endif // XENIA_VFS_DEVICES_STFS_CONTAINER_FILE_H_
+#endif // XENIA_VFS_DEVICES_XCONTENT_CONTAINER_FILE_H_
diff --git a/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.cc b/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.cc
new file mode 100644
index 0000000000..c241c2c019
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.cc
@@ -0,0 +1,324 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#include <algorithm>
+#include <vector>
+
+#include "xenia/base/logging.h"
+#include "xenia/kernel/xam/content_manager.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+#include "xenia/vfs/devices/xcontent_devices/stfs_container_device.h"
+
+namespace xe {
+namespace vfs {
+
+StfsContainerDevice::StfsContainerDevice(const std::string_view mount_path,
+ const std::filesystem::path& host_path)
+ : XContentContainerDevice(mount_path, host_path),
+ blocks_per_hash_table_(1),
+ block_step_{0, 0} {
+ SetName("STFS");
+}
+
+StfsContainerDevice::~StfsContainerDevice() { CloseFiles(); }
+
+void StfsContainerDevice::SetupContainer() {
+ // Additional part specific to STFS container.
+ const XContentContainerHeader* header = GetContainerHeader();
+ blocks_per_hash_table_ = header->is_package_readonly() ? 1 : 2;
+
+ block_step_[0] = kBlocksPerHashLevel[0] + blocks_per_hash_table_;
+ block_step_[1] = kBlocksPerHashLevel[1] +
+ ((kBlocksPerHashLevel[0] + 1) * blocks_per_hash_table_);
+}
+
+XContentContainerDevice::Result StfsContainerDevice::LoadHostFiles(
+ FILE* header_file) {
+ const XContentContainerHeader* header = GetContainerHeader();
+
+ if (header->content_metadata.data_file_count > 0) {
+ XELOGW("STFS container is not a single file. Loading might fail!");
+ }
+
+ files_.emplace(std::make_pair(0, header_file));
+ return Result::kSuccess;
+}
+
+StfsContainerDevice::Result StfsContainerDevice::Read() {
+ auto& file = files_.at(0);
+
+ auto root_entry = new XContentContainerEntry(this, nullptr, "", &files_);
+ root_entry->attributes_ = kFileAttributeDirectory;
+ root_entry_ = std::unique_ptr<Entry>(root_entry);
+
+ std::vector<XContentContainerEntry*> all_entries;
+
+ // Load all listings.
+ StfsDirectoryBlock directory;
+
+ auto& descriptor =
+ GetContainerHeader()->content_metadata.volume_descriptor.stfs;
+ uint32_t table_block_index = descriptor.file_table_block_number();
+ size_t n = 0;
+ for (n = 0; n < descriptor.file_table_block_count; n++) {
+ const size_t offset = BlockToOffset(table_block_index);
+ xe::filesystem::Seek(file, offset, SEEK_SET);
+
+ if (fread(&directory, sizeof(StfsDirectoryBlock), 1, file) != 1) {
+ XELOGE("ReadSTFS failed to read directory block at 0x{X}", offset);
+ return Result::kReadError;
+ }
+
+ for (size_t m = 0; m < kEntriesPerDirectoryBlock; m++) {
+ const StfsDirectoryEntry& dir_entry = directory.entries[m];
+
+ if (dir_entry.name[0] == 0) {
+ // Done.
+ break;
+ }
+
+ XContentContainerEntry* parent_entry =
+ dir_entry.directory_index == 0xFFFF
+ ? root_entry
+ : all_entries[dir_entry.directory_index];
+
+ std::unique_ptr<XContentContainerEntry> entry =
+ ReadEntry(parent_entry, &files_, &dir_entry);
+ all_entries.push_back(entry.get());
+ parent_entry->children_.emplace_back(std::move(entry));
+ }
+
+ const StfsHashEntry* block_hash = GetBlockHash(table_block_index);
+ table_block_index = block_hash->level0_next_block();
+ if (table_block_index == kEndOfChain) {
+ break;
+ }
+ }
+
+ if (n + 1 != descriptor.file_table_block_count) {
+ XELOGW("STFS read {} file table blocks, but STFS headers expected {}!",
+ n + 1, descriptor.file_table_block_count);
+ assert_always();
+ }
+
+ return Result::kSuccess;
+}
+
+std::unique_ptr<XContentContainerEntry> StfsContainerDevice::ReadEntry(
+ Entry* parent, MultiFileHandles* files,
+ const StfsDirectoryEntry* dir_entry) {
+ std::string name(reinterpret_cast<const char*>(dir_entry->name),
+ dir_entry->flags.name_length & 0x3F);
+
+ auto entry = XContentContainerEntry::Create(this, parent, name, &files_);
+
+ if (dir_entry->flags.directory) {
+ entry->attributes_ = kFileAttributeDirectory;
+ } else {
+ entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
+ entry->data_offset_ = BlockToOffset(dir_entry->start_block_number());
+ entry->data_size_ = dir_entry->length;
+ }
+ entry->size_ = dir_entry->length;
+ entry->allocation_size_ = xe::round_up(dir_entry->length, kBlockSize);
+
+ entry->create_timestamp_ =
+ decode_fat_timestamp(dir_entry->create_date, dir_entry->create_time);
+ entry->write_timestamp_ =
+ decode_fat_timestamp(dir_entry->modified_date, dir_entry->modified_time);
+ entry->access_timestamp_ = entry->write_timestamp_;
+
+ // Fill in all block records.
+ // It's easier to do this now and just look them up later, at the cost
+ // of some memory. Nasty chain walk.
+ // TODO(benvanik): optimize if flags.contiguous is set.
+ if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
+ uint32_t block_index = dir_entry->start_block_number();
+ size_t remaining_size = dir_entry->length;
+ while (remaining_size && block_index != kEndOfChain) {
+ size_t block_size =
+ std::min(static_cast<size_t>(kBlockSize), remaining_size);
+ size_t offset = BlockToOffset(block_index);
+ entry->block_list_.push_back({0, offset, block_size});
+ remaining_size -= block_size;
+ auto block_hash = GetBlockHash(block_index);
+ block_index = block_hash->level0_next_block();
+ }
+
+ if (remaining_size) {
+ // Loop above must have exited prematurely, bad hash tables?
+ XELOGW(
+ "STFS file {} only found {} bytes for file, expected {} ({} "
+ "bytes missing)",
+ name, dir_entry->length - remaining_size, dir_entry->length,
+ remaining_size);
+ assert_always();
+ }
+
+ // Check that the number of blocks retrieved from hash entries matches
+ // the block count read from the file entry
+ if (entry->block_list_.size() != dir_entry->allocated_data_blocks()) {
+ XELOGW(
+ "STFS failed to read correct block-chain for entry {}, read {} "
+ "blocks, expected {}",
+ entry->name_, entry->block_list_.size(),
+ dir_entry->allocated_data_blocks());
+ assert_always();
+ }
+ }
+
+ return entry;
+}
+
+size_t StfsContainerDevice::BlockToOffset(uint64_t block_index) const {
+ // For every level there is a hash table
+ // Level 0: hash table of next 170 blocks
+ // Level 1: hash table of next 170 hash tables
+ // Level 2: hash table of next 170 level 1 hash tables
+ // And so on...
+ uint64_t block = block_index;
+ for (uint32_t i = 0; i < kBlocksHashLevelAmount; i++) {
+ const uint32_t level_base = kBlocksPerHashLevel[i];
+ block += ((block_index + level_base) / level_base) * blocks_per_hash_table_;
+ if (block_index < level_base) {
+ break;
+ }
+ }
+
+ return xe::round_up(GetContainerHeader()->content_header.header_size,
+ kBlockSize) +
+ (block << 12);
+}
+
+uint32_t StfsContainerDevice::BlockToHashBlockNumber(
+ uint32_t block_index, uint32_t hash_level) const {
+ if (hash_level == 2) {
+ return block_step_[1];
+ }
+
+ if (block_index < kBlocksPerHashLevel[hash_level]) {
+ return hash_level == 0 ? 0 : block_step_[hash_level - 1];
+ }
+
+ uint32_t block =
+ (block_index / kBlocksPerHashLevel[hash_level]) * block_step_[hash_level];
+
+ if (hash_level == 0) {
+ block +=
+ ((block_index / kBlocksPerHashLevel[1]) + 1) * blocks_per_hash_table_;
+
+ if (block_index < kBlocksPerHashLevel[1]) {
+ return block;
+ }
+ }
+
+ return block + blocks_per_hash_table_;
+}
+
+size_t StfsContainerDevice::BlockToHashBlockOffset(uint32_t block_index,
+ uint32_t hash_level) const {
+ const uint64_t block = BlockToHashBlockNumber(block_index, hash_level);
+ return xe::round_up(header_->content_header.header_size, kBlockSize) +
+ (block << 12);
+}
+
+const uint8_t StfsContainerDevice::GetAmountOfHashLevelsToCheck(
+ uint32_t total_block_count) const {
+ for (uint8_t level = 0; level < kBlocksHashLevelAmount; level++) {
+ if (total_block_count < kBlocksPerHashLevel[level]) {
+ return level;
+ }
+ }
+ XELOGE("GetAmountOfHashLevelsToCheck - Invalid total_block_count: {}",
+ total_block_count);
+ return 0;
+}
+
+void StfsContainerDevice::UpdateCachedHashTable(
+ uint32_t block_index, uint8_t hash_level,
+ uint32_t& secondary_table_offset) {
+ const size_t hash_offset = BlockToHashBlockOffset(block_index, hash_level);
+ // Do nothing. It's already there.
+ if (!cached_hash_tables_.count(hash_offset)) {
+ auto& file = files_.at(0);
+ xe::filesystem::Seek(file, hash_offset + secondary_table_offset, SEEK_SET);
+ StfsHashTable table;
+ if (fread(&table, sizeof(StfsHashTable), 1, file) != 1) {
+ XELOGE("GetBlockHash failed to read level{} hash table at 0x{X}",
+ hash_level, hash_offset + secondary_table_offset);
+ return;
+ }
+ cached_hash_tables_[hash_offset] = table;
+ }
+
+ uint32_t record = block_index % kBlocksPerHashLevel[0];
+ if (hash_level >= 1) {
+ record = (block_index / kBlocksPerHashLevel[hash_level - 1]) %
+ kBlocksPerHashLevel[0];
+ }
+ const StfsHashEntry* record_data =
+ &cached_hash_tables_[hash_offset].entries[record];
+ secondary_table_offset = record_data->levelN_active_index() ? kBlockSize : 0;
+}
+
+void StfsContainerDevice::UpdateCachedHashTables(
+ uint32_t block_index, uint8_t highest_hash_level_to_update,
+ uint32_t& secondary_table_offset) {
+ for (int8_t level = highest_hash_level_to_update; level >= 0; level--) {
+ UpdateCachedHashTable(block_index, level, secondary_table_offset);
+ }
+}
+
+const StfsHashEntry* StfsContainerDevice::GetBlockHash(uint32_t block_index) {
+ auto& file = files_.at(0);
+
+ const StfsVolumeDescriptor& descriptor =
+ header_->content_metadata.volume_descriptor.stfs;
+
+ // Offset for selecting the secondary hash block, in packages that have them
+ uint32_t secondary_table_offset =
+ descriptor.flags.bits.root_active_index ? kBlockSize : 0;
+
+ uint8_t hash_levels_to_process =
+ GetAmountOfHashLevelsToCheck(descriptor.total_block_count);
+
+ if (header_->is_package_readonly()) {
+ secondary_table_offset = 0;
+ // Because we have read only package we only need to check first hash level.
+ hash_levels_to_process = 0;
+ }
+
+ UpdateCachedHashTables(block_index, hash_levels_to_process,
+ secondary_table_offset);
+
+ const size_t hash_offset = BlockToHashBlockOffset(block_index, 0);
+ const uint32_t record = block_index % kBlocksPerHashLevel[0];
+ return &cached_hash_tables_[hash_offset].entries[record];
+}
+
+const uint8_t StfsContainerDevice::GetBlocksPerHashTableFromContainerHeader()
+ const {
+ const XContentContainerHeader* header = GetContainerHeader();
+ if (!header) {
+ XELOGE(
+ "VFS: SetBlocksPerHashTableBasedOnContainerHeader - Missing "
+ "Container "
+ "Header!");
+ return 0;
+ }
+
+ if (header->is_package_readonly()) {
+ return 1;
+ }
+
+ return 2;
+}
+
+} // namespace vfs
+} // namespace xe
diff --git a/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.h b/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.h
new file mode 100644
index 0000000000..828e00921b
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_devices/stfs_container_device.h
@@ -0,0 +1,95 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#ifndef XENIA_VFS_DEVICES_XCONTENT_DEVICES_STFS_CONTAINER_DEVICE_H_
+#define XENIA_VFS_DEVICES_XCONTENT_DEVICES_STFS_CONTAINER_DEVICE_H_
+
+#include <string>
+#include <unordered_map>
+
+#include "xenia/base/string_util.h"
+#include "xenia/kernel/util/xex2_info.h"
+#include "xenia/vfs/device.h"
+#include "xenia/vfs/devices/stfs_xbox.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+
+namespace xe {
+namespace vfs {
+
+// https://free60project.github.io/wiki/STFS.html
+
+class StfsContainerDevice : public XContentContainerDevice {
+ public:
+ StfsContainerDevice(const std::string_view mount_path,
+ const std::filesystem::path& host_path);
+ ~StfsContainerDevice() override;
+
+ bool is_read_only() const override {
+ return GetContainerHeader()
+ ->content_metadata.volume_descriptor.stfs.flags.bits.read_only_format;
+ }
+
+ uint32_t component_name_max_length() const override { return 40; }
+
+ uint32_t total_allocation_units() const override {
+ return GetContainerHeader()
+ ->content_metadata.volume_descriptor.stfs.total_block_count;
+ }
+ uint32_t available_allocation_units() const override {
+ if (!is_read_only()) {
+ auto& descriptor =
+ GetContainerHeader()->content_metadata.volume_descriptor.stfs;
+ return kBlocksPerHashLevel[2] -
+ (descriptor.total_block_count - descriptor.free_block_count);
+ }
+ return 0;
+ }
+
+ private:
+ static const uint8_t kBlocksHashLevelAmount = 3;
+ const uint32_t kBlocksPerHashLevel[kBlocksHashLevelAmount] = {170, 28900,
+ 4913000};
+ const uint32_t kEndOfChain = 0xFFFFFF;
+ const uint32_t kEntriesPerDirectoryBlock =
+ kBlockSize / sizeof(StfsDirectoryEntry);
+
+ void SetupContainer() override;
+ Result LoadHostFiles(FILE* header_file) override;
+
+ Result Read() override;
+ std::unique_ptr<XContentContainerEntry> ReadEntry(
+ Entry* parent, MultiFileHandles* files,
+ const StfsDirectoryEntry* dir_entry);
+
+ size_t BlockToOffset(uint64_t block_index) const;
+ uint32_t BlockToHashBlockNumber(uint32_t block_index,
+ uint32_t hash_level) const;
+ size_t BlockToHashBlockOffset(uint32_t block_index,
+ uint32_t hash_level) const;
+ const uint8_t GetAmountOfHashLevelsToCheck(uint32_t total_block_count) const;
+
+ const StfsHashEntry* GetBlockHash(uint32_t block_index);
+ void UpdateCachedHashTable(uint32_t block_index, uint8_t hash_level,
+ uint32_t& secondary_table_offset);
+ void UpdateCachedHashTables(uint32_t block_index,
+ uint8_t highest_hash_level_to_update,
+ uint32_t& secondary_table_offset);
+ const uint8_t GetBlocksPerHashTableFromContainerHeader() const;
+
+ uint8_t blocks_per_hash_table_;
+ uint32_t block_step_[2];
+
+ std::unordered_map<size_t, StfsHashTable> cached_hash_tables_;
+};
+
+} // namespace vfs
+} // namespace xe
+
+#endif // XENIA_VFS_DEVICES_XCONTENT_DEVICES_STFS_CONTAINER_DEVICE_H_
diff --git a/src/xenia/vfs/devices/xcontent_devices/svod_container_device.cc b/src/xenia/vfs/devices/xcontent_devices/svod_container_device.cc
new file mode 100644
index 0000000000..369ecf10dc
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_devices/svod_container_device.cc
@@ -0,0 +1,417 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#include <algorithm>
+#include <vector>
+
+#include "xenia/base/logging.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+#include "xenia/vfs/devices/xcontent_devices/svod_container_device.h"
+
+namespace xe {
+namespace vfs {
+
+SvodContainerDevice::SvodContainerDevice(const std::string_view mount_path,
+ const std::filesystem::path& host_path)
+ : XContentContainerDevice(mount_path, host_path),
+ svod_base_offset_(),
+ svod_layout_() {
+ SetName("FATX");
+}
+
+SvodContainerDevice::~SvodContainerDevice() { CloseFiles(); }
+
+SvodContainerDevice::Result SvodContainerDevice::LoadHostFiles(
+ FILE* header_file) {
+ std::filesystem::path data_fragment_path = host_path_;
+ data_fragment_path += ".data";
+ if (!std::filesystem::exists(data_fragment_path)) {
+ XELOGE("STFS container is multi-file, but path {} does not exist.",
+ xe::path_to_utf8(data_fragment_path));
+ return Result::kFileMismatch;
+ }
+
+ // Ensure data fragment files are sorted
+ auto fragment_files = filesystem::ListFiles(data_fragment_path);
+ std::sort(fragment_files.begin(), fragment_files.end(),
+ [](filesystem::FileInfo& left, filesystem::FileInfo& right) {
+ return left.name < right.name;
+ });
+
+ if (fragment_files.size() != header_->content_metadata.data_file_count) {
+ XELOGE("SVOD expecting {} data fragments, but {} are present.",
+ header_->content_metadata.data_file_count, fragment_files.size());
+ return Result::kFileMismatch;
+ }
+
+ for (size_t i = 0; i < fragment_files.size(); i++) {
+ auto& fragment = fragment_files.at(i);
+ auto path = fragment.path / fragment.name;
+ auto file = xe::filesystem::OpenFile(path, "rb");
+ if (!file) {
+ XELOGI("Failed to map SVOD file {}.", xe::path_to_utf8(path));
+ CloseFiles();
+ return Result::kReadError;
+ }
+
+ xe::filesystem::Seek(file, 0L, SEEK_END);
+ files_total_size_ += xe::filesystem::Tell(file);
+ // no need to seek back, any reads from this file will seek first anyway
+ files_.emplace(std::make_pair(i, file));
+ }
+ XELOGI("SVOD successfully mapped {} files.", fragment_files.size());
+ return Result::kSuccess;
+}
+
+XContentContainerDevice::Result SvodContainerDevice::Read() {
+ // SVOD Systems can have different layouts. The root block is
+ // denoted by the magic "MICROSOFT*XBOX*MEDIA" and is always in
+ // the first "actual" data fragment of the system.
+ auto& svod_header = files_.at(0);
+
+ size_t magic_offset;
+ SetLayout(svod_header, magic_offset);
+
+ // Parse the root directory
+ xe::filesystem::Seek(svod_header, magic_offset + 0x14, SEEK_SET);
+
+ struct {
+ uint32_t block;
+ uint32_t size;
+ uint32_t creation_date;
+ uint32_t creation_time;
+ } root_data;
+ static_assert_size(root_data, 0x10);
+
+ if (fread(&root_data, sizeof(root_data), 1, svod_header) != 1) {
+ XELOGE("ReadSVOD failed to read root block data at 0x{X}",
+ magic_offset + 0x14);
+ return Result::kReadError;
+ }
+
+ const uint64_t root_creation_timestamp =
+ decode_fat_timestamp(root_data.creation_date, root_data.creation_time);
+
+ auto root_entry = new XContentContainerEntry(this, nullptr, "", &files_);
+ root_entry->attributes_ = kFileAttributeDirectory;
+ root_entry->access_timestamp_ = root_creation_timestamp;
+ root_entry->create_timestamp_ = root_creation_timestamp;
+ root_entry->write_timestamp_ = root_creation_timestamp;
+ root_entry_ = std::unique_ptr<Entry>(root_entry);
+
+ // Traverse all child entries
+ return ReadEntry(root_data.block, 0, root_entry);
+}
+
+SvodContainerDevice::Result SvodContainerDevice::ReadEntry(
+ uint32_t block, uint32_t ordinal, XContentContainerEntry* parent) {
+ // For games with a large amount of files, the ordinal offset can overrun
+ // the current block and potentially hit a hash block.
+ size_t ordinal_offset = ordinal * 0x4;
+ size_t block_offset = ordinal_offset / 0x800;
+ size_t true_ordinal_offset = ordinal_offset % 0x800;
+
+ // Calculate the file & address of the block
+ size_t entry_address, entry_file;
+ BlockToOffset(block + block_offset, &entry_address, &entry_file);
+ entry_address += true_ordinal_offset;
+
+ // Read directory entry
+ auto& file = files_.at(entry_file);
+ xe::filesystem::Seek(file, entry_address, SEEK_SET);
+
+#pragma pack(push, 1)
+ struct {
+ uint16_t node_l;
+ uint16_t node_r;
+ uint32_t data_block;
+ uint32_t length;
+ uint8_t attributes;
+ uint8_t name_length;
+ } dir_entry;
+ static_assert_size(dir_entry, 0xE);
+#pragma pack(pop)
+
+ if (fread(&dir_entry, sizeof(dir_entry), 1, file) != 1) {
+ XELOGE("ReadEntrySVOD failed to read directory entry at 0x{X}",
+ entry_address);
+ return Result::kReadError;
+ }
+
+ auto name_buffer = std::make_unique<char[]>(dir_entry.name_length);
+ if (fread(name_buffer.get(), 1, dir_entry.name_length, file) !=
+ dir_entry.name_length) {
+ XELOGE("ReadEntrySVOD failed to read directory entry name at 0x{X}",
+ entry_address);
+ return Result::kReadError;
+ }
+
+ auto name = std::string(name_buffer.get(), dir_entry.name_length);
+
+ // Read the left node
+ if (dir_entry.node_l) {
+ auto node_result = ReadEntry(block, dir_entry.node_l, parent);
+ if (node_result != Result::kSuccess) {
+ return node_result;
+ }
+ }
+
+ // Read file & address of block's data
+ size_t data_address, data_file;
+ BlockToOffset(dir_entry.data_block, &data_address, &data_file);
+
+ // Create the entry
+ // NOTE: SVOD entries don't have timestamps for individual files, which can
+ // cause issues when decrypting games. Using the root entry's timestamp
+ // solves this issues.
+ auto entry = XContentContainerEntry::Create(this, parent, name, &files_);
+ if (dir_entry.attributes & kFileAttributeDirectory) {
+ // Entry is a directory
+ entry->attributes_ = kFileAttributeDirectory | kFileAttributeReadOnly;
+ entry->data_offset_ = 0;
+ entry->data_size_ = 0;
+ entry->block_ = block;
+ entry->access_timestamp_ = root_entry_->create_timestamp();
+ entry->create_timestamp_ = root_entry_->create_timestamp();
+ entry->write_timestamp_ = root_entry_->create_timestamp();
+
+ if (dir_entry.length) {
+ // If length is greater than 0, traverse the directory's children
+ auto directory_result = ReadEntry(dir_entry.data_block, 0, entry.get());
+ if (directory_result != Result::kSuccess) {
+ return directory_result;
+ }
+ }
+ } else {
+ // Entry is a file
+ entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly;
+ entry->size_ = dir_entry.length;
+ entry->allocation_size_ = xe::round_up(dir_entry.length, kBlockSize);
+ entry->data_offset_ = data_address;
+ entry->data_size_ = dir_entry.length;
+ entry->block_ = dir_entry.data_block;
+ entry->access_timestamp_ = root_entry_->create_timestamp();
+ entry->create_timestamp_ = root_entry_->create_timestamp();
+ entry->write_timestamp_ = root_entry_->create_timestamp();
+
+ // Fill in all block records, sector by sector.
+ if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
+ uint32_t block_index = dir_entry.data_block;
+ size_t remaining_size = xe::round_up(dir_entry.length, 0x800);
+
+ size_t last_record = -1;
+ size_t last_offset = -1;
+ while (remaining_size) {
+ const size_t BLOCK_SIZE = 0x800;
+
+ size_t offset, file_index;
+ BlockToOffset(block_index, &offset, &file_index);
+
+ block_index++;
+ remaining_size -= BLOCK_SIZE;
+
+ if (offset - last_offset == BLOCK_SIZE) {
+ // Consecutive, so append to last entry.
+ entry->block_list_[last_record].length += BLOCK_SIZE;
+ last_offset = offset;
+ continue;
+ }
+
+ entry->block_list_.push_back({file_index, offset, BLOCK_SIZE});
+ last_record = entry->block_list_.size() - 1;
+ last_offset = offset;
+ }
+ }
+ }
+
+ parent->children_.emplace_back(std::move(entry));
+
+ // Read the right node.
+ if (dir_entry.node_r) {
+ auto node_result = ReadEntry(block, dir_entry.node_r, parent);
+ if (node_result != Result::kSuccess) {
+ return node_result;
+ }
+ }
+
+ return Result::kSuccess;
+}
+
+XContentContainerDevice::Result SvodContainerDevice::SetLayout(
+ FILE* header, size_t& magic_offset) {
+ if (IsEDGFLayout()) {
+ return SetEDGFLayout(header, magic_offset);
+ }
+
+ if (IsXSFLayout(header)) {
+ return SetXSFLayout(header, magic_offset);
+ }
+
+ return SetNormalLayout(header, magic_offset);
+}
+
+XContentContainerDevice::Result SvodContainerDevice::SetEDGFLayout(
+ FILE* header, size_t& magic_offset) {
+ uint8_t magic_buf[20];
+ xe::filesystem::Seek(header, 0x2000, SEEK_SET);
+ if (fread(magic_buf, 1, countof(magic_buf), header) != countof(magic_buf)) {
+ XELOGE("ReadSVOD failed to read SVOD magic at 0x2000");
+ return Result::kReadError;
+ }
+
+ if (std::memcmp(magic_buf, MEDIA_MAGIC, countof(magic_buf)) != 0) {
+ XELOGE("SVOD uses an EGDF layout, but the magic block was not found.");
+ return Result::kFileMismatch;
+ }
+
+ svod_base_offset_ = 0x0000;
+ magic_offset = 0x2000;
+ svod_layout_ = SvodLayoutType::kEnhancedGDF;
+ XELOGI("SVOD uses an EGDF layout. Magic block present at 0x2000.");
+ return Result::kSuccess;
+}
+
+const bool SvodContainerDevice::IsXSFLayout(FILE* header) const {
+ uint8_t magic_buf[20];
+ xe::filesystem::Seek(header, 0x12000, SEEK_SET);
+
+ if (fread(magic_buf, 1, countof(magic_buf), header) != countof(magic_buf)) {
+ XELOGE("ReadSVOD failed to read SVOD magic at 0x12000");
+ return false;
+ }
+
+ return std::memcmp(magic_buf, MEDIA_MAGIC, countof(magic_buf)) == 0;
+}
+
+XContentContainerDevice::Result SvodContainerDevice::SetXSFLayout(
+ FILE* header, size_t& magic_offset) {
+ uint8_t magic_buf[20];
+ const char* XSF_MAGIC = "XSF";
+
+ xe::filesystem::Seek(header, 0x2000, SEEK_SET);
+ if (fread(magic_buf, 1, 3, header) != 3) {
+ XELOGE("ReadSVOD failed to read SVOD XSF magic at 0x2000");
+ return Result::kReadError;
+ }
+
+ svod_base_offset_ = 0x10000;
+ magic_offset = 0x12000;
+
+ if (std::memcmp(magic_buf, XSF_MAGIC, 3) != 0) {
+ svod_layout_ = SvodLayoutType::kUnknown;
+ XELOGI("SVOD appears to use an XSF layout, but no header is present.");
+ XELOGI("SVOD magic block found at 0x12000");
+ return Result::kSuccess;
+ }
+
+ svod_layout_ = SvodLayoutType::kXSF;
+ XELOGI("SVOD uses an XSF layout. Magic block present at 0x12000.");
+ XELOGI("Game was likely converted using a third-party tool.");
+ return Result::kSuccess;
+}
+
+XContentContainerDevice::Result SvodContainerDevice::SetNormalLayout(
+ FILE* header, size_t& magic_offset) {
+ uint8_t magic_buf[20];
+
+ xe::filesystem::Seek(header, 0xD000, SEEK_SET);
+ if (fread(magic_buf, 1, countof(magic_buf), header) != countof(magic_buf)) {
+ XELOGE("ReadSVOD failed to read SVOD magic at 0xD000");
+ return Result::kReadError;
+ }
+
+ if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) != 0) {
+ XELOGE("Could not locate SVOD magic block.");
+ return Result::kReadError;
+ }
+
+ // If the SVOD's magic block is at 0xD000, it most likely means that it
+ // is a single-file system. The STFS Header is 0xB000 bytes and the
+ // remaining 0x2000 is from hash tables. In most cases, these will be
+ // STFS, not SVOD.
+ svod_base_offset_ = 0xB000;
+ magic_offset = 0xD000;
+
+ // Check for single file system
+ if (header_->content_metadata.data_file_count == 1) {
+ svod_layout_ = SvodLayoutType::kSingleFile;
+ XELOGI("SVOD is a single file. Magic block present at 0xD000.");
+ } else {
+ svod_layout_ = SvodLayoutType::kUnknown;
+ XELOGE(
+ "SVOD is not a single file, but the magic block was found at "
+ "0xD000.");
+ }
+ return Result::kSuccess;
+}
+
+void SvodContainerDevice::BlockToOffset(size_t block, size_t* out_address,
+ size_t* out_file_index) const {
+ // SVOD Systems use hash blocks for integrity checks. These hash blocks
+ // cause blocks to be discontinuous in memory, and must be accounted for.
+ // - Each data block is 0x800 bytes in length
+ // - Every group of 0x198 data blocks is preceded a Level0 hash table.
+ // Level0 tables contain 0xCC hashes, each representing two data blocks.
+ // The total size of each Level0 hash table is 0x1000 bytes in length.
+ // - Every 0xA1C4 Level0 hash tables is preceded by a Level1 hash table.
+ // Level1 tables contain 0xCB hashes, each representing two Level0 hashes.
+ // The total size of each Level1 hash table is 0x1000 bytes in length.
+ // - Files are split into fragments of 0xA290000 bytes in length,
+ // consisting of 0x14388 data blocks, 0xCB Level0 hash tables, and 0x1
+ // Level1 hash table.
+
+ const size_t BLOCK_SIZE = 0x800;
+ const size_t HASH_BLOCK_SIZE = 0x1000;
+ const size_t BLOCKS_PER_L0_HASH = 0x198;
+ const size_t HASHES_PER_L1_HASH = 0xA1C4;
+ const size_t BLOCKS_PER_FILE = 0x14388;
+ const size_t MAX_FILE_SIZE = 0xA290000;
+ const size_t BLOCK_OFFSET =
+ header_->content_metadata.volume_descriptor.svod.start_data_block();
+
+ // Resolve the true block address and file index
+ size_t true_block = block - (BLOCK_OFFSET * 2);
+ if (svod_layout_ == SvodLayoutType::kEnhancedGDF) {
+ // EGDF has an 0x1000 byte offset, which is two blocks
+ true_block += 0x2;
+ }
+
+ size_t file_block = true_block % BLOCKS_PER_FILE;
+ size_t file_index = true_block / BLOCKS_PER_FILE;
+ size_t offset = 0;
+
+ // Calculate offset caused by Level0 Hash Tables
+ size_t level0_table_count = (file_block / BLOCKS_PER_L0_HASH) + 1;
+ offset += level0_table_count * HASH_BLOCK_SIZE;
+
+ // Calculate offset caused by Level1 Hash Tables
+ size_t level1_table_count = (level0_table_count / HASHES_PER_L1_HASH) + 1;
+ offset += level1_table_count * HASH_BLOCK_SIZE;
+
+ // For single-file SVOD layouts, include the size of the header in the offset.
+ if (svod_layout_ == SvodLayoutType::kSingleFile) {
+ offset += svod_base_offset_;
+ }
+
+ size_t block_address = (file_block * BLOCK_SIZE) + offset;
+
+ // If the offset causes the block address to overrun the file, round it.
+ if (block_address >= MAX_FILE_SIZE) {
+ file_index += 1;
+ block_address %= MAX_FILE_SIZE;
+ block_address += 0x2000;
+ }
+
+ *out_address = block_address;
+ *out_file_index = file_index;
+}
+
+} // namespace vfs
+} // namespace xe
diff --git a/src/xenia/vfs/devices/xcontent_devices/svod_container_device.h b/src/xenia/vfs/devices/xcontent_devices/svod_container_device.h
new file mode 100644
index 0000000000..2c389f6a9e
--- /dev/null
+++ b/src/xenia/vfs/devices/xcontent_devices/svod_container_device.h
@@ -0,0 +1,77 @@
+/**
+ ******************************************************************************
+ * Xenia : Xbox 360 Emulator Research Project *
+ ******************************************************************************
+ * Copyright 2023 Ben Vanik. All rights reserved. *
+ * Released under the BSD license - see LICENSE in the root for more details. *
+ ******************************************************************************
+ */
+
+#ifndef XENIA_VFS_DEVICES_XCONTENT_DEVICES_SVOD_CONTAINER_DEVICE_H_
+#define XENIA_VFS_DEVICES_XCONTENT_DEVICES_SVOD_CONTAINER_DEVICE_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+#include "xenia/base/string_util.h"
+#include "xenia/kernel/util/xex2_info.h"
+#include "xenia/vfs/device.h"
+#include "xenia/vfs/devices/stfs_xbox.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
+#include "xenia/vfs/devices/xcontent_container_entry.h"
+
+namespace xe {
+namespace vfs {
+class SvodContainerDevice : public XContentContainerDevice {
+ public:
+ SvodContainerDevice(const std::string_view mount_path,
+ const std::filesystem::path& host_path);
+ ~SvodContainerDevice() override;
+
+ bool is_read_only() const override { return true; }
+
+ uint32_t component_name_max_length() const override { return 255; }
+
+ uint32_t total_allocation_units() const override {
+ return uint32_t(data_size() / sectors_per_allocation_unit() /
+ bytes_per_sector());
+ }
+ uint32_t available_allocation_units() const override { return 0; }
+
+ private:
+ enum class SvodLayoutType {
+ kUnknown = 0x0,
+ kEnhancedGDF = 0x1,
+ kXSF = 0x2,
+ kSingleFile = 0x4,
+ };
+ const char* MEDIA_MAGIC = "MICROSOFT*XBOX*MEDIA";
+
+ Result LoadHostFiles(FILE* header_file) override;
+
+ Result Read() override;
+ Result ReadEntry(uint32_t sector, uint32_t ordinal,
+ XContentContainerEntry* parent);
+ void BlockToOffset(size_t sector, size_t* address, size_t* file_index) const;
+
+ Result SetLayout(FILE* header, size_t& magic_offset);
+ Result SetEDGFLayout(FILE* header, size_t& magic_offset);
+ Result SetXSFLayout(FILE* header, size_t& magic_offset);
+ Result SetNormalLayout(FILE* header, size_t& magic_offset);
+
+ const bool IsEDGFLayout() const {
+ return header_->content_metadata.volume_descriptor.svod.features.bits
+ .enhanced_gdf_layout;
+ }
+ const bool IsXSFLayout(FILE* header) const;
+
+ size_t svod_base_offset_;
+ SvodLayoutType svod_layout_;
+};
+
+} // namespace vfs
+} // namespace xe
+
+#endif // XENIA_VFS_DEVICES_XCONTENT_DEVICES_SVOD_CONTAINER_DEVICE_H_
diff --git a/src/xenia/vfs/vfs_dump.cc b/src/xenia/vfs/vfs_dump.cc
index f2416b59a6..6551f1598e 100644
--- a/src/xenia/vfs/vfs_dump.cc
+++ b/src/xenia/vfs/vfs_dump.cc
@@ -17,7 +17,7 @@
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
-#include "xenia/vfs/devices/stfs_container_device.h"
+#include "xenia/vfs/devices/xcontent_container_device.h"
#include "xenia/vfs/file.h"
namespace xe {
@@ -38,10 +38,9 @@ int vfs_dump_main(const std::vector<std::string>& args) {
}
std::filesystem::path base_path = cvars::dump_path;
- std::unique_ptr<vfs::Device> device;
+ std::unique_ptr<vfs::Device> device =
+ vfs::XContentContainerDevice::CreateContentDevice("", cvars::source);
- // TODO: Flags specifying the type of device.
- device = std::make_unique<vfs::StfsContainerDevice>("", cvars::source);
if (!device->Initialize()) {
XELOGE("Failed to initialize device");
return 1;