[VFS] STFS/XContent structure improvements
Now makes use of xe::be<T> and removes need for read() method
This commit is contained in:
parent
010e14b4cd
commit
84f566d92b
|
@ -61,8 +61,8 @@ StfsContainerDevice::StfsContainerDevice(const std::string_view mount_path,
|
|||
mmap_total_size_(),
|
||||
base_offset_(),
|
||||
magic_offset_(),
|
||||
package_type_(),
|
||||
header_(),
|
||||
svod_layout_(),
|
||||
table_size_shift_() {}
|
||||
|
||||
StfsContainerDevice::~StfsContainerDevice() = default;
|
||||
|
@ -89,14 +89,15 @@ bool StfsContainerDevice::Initialize() {
|
|||
return false;
|
||||
}
|
||||
|
||||
switch (header_.descriptor_type) {
|
||||
case StfsDescriptorType::kStfs:
|
||||
switch (header_.metadata.volume_type) {
|
||||
case XContentVolumeType::kStfs:
|
||||
return ReadSTFS() == Error::kSuccess;
|
||||
break;
|
||||
case StfsDescriptorType::kSvod:
|
||||
case XContentVolumeType::kSvod:
|
||||
return ReadSVOD() == Error::kSuccess;
|
||||
default:
|
||||
XELOGE("Unknown STFS Descriptor Type: {}", header_.descriptor_type);
|
||||
XELOGE("Unknown STFS Descriptor Type: {}",
|
||||
xe::byte_swap(uint32_t(header_.metadata.volume_type.value)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +121,7 @@ StfsContainerDevice::Error StfsContainerDevice::MapFiles() {
|
|||
// 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_.data_file_count <= 1) {
|
||||
if (header_.metadata.data_file_count <= 1) {
|
||||
XELOGI("STFS container is a single file.");
|
||||
mmap_.emplace(std::make_pair(0, std::move(header_map)));
|
||||
return Error::kSuccess;
|
||||
|
@ -143,9 +144,9 @@ StfsContainerDevice::Error StfsContainerDevice::MapFiles() {
|
|||
return left.name < right.name;
|
||||
});
|
||||
|
||||
if (fragment_files.size() != header_.data_file_count) {
|
||||
if (fragment_files.size() != header_.metadata.data_file_count) {
|
||||
XELOGE("SVOD expecting {} data fragments, but {} are present.",
|
||||
header_.data_file_count, fragment_files.size());
|
||||
header_.metadata.data_file_count, fragment_files.size());
|
||||
return Error::kErrorFileMismatch;
|
||||
}
|
||||
|
||||
|
@ -177,48 +178,19 @@ Entry* StfsContainerDevice::ResolvePath(const std::string_view path) {
|
|||
return root_entry_->ResolvePath(path);
|
||||
}
|
||||
|
||||
StfsContainerDevice::Error StfsContainerDevice::ReadPackageType(
|
||||
const uint8_t* map_ptr, size_t map_size,
|
||||
StfsPackageType* package_type_out) {
|
||||
if (map_size < 4) {
|
||||
return Error::kErrorFileMismatch;
|
||||
}
|
||||
if (memcmp(map_ptr, "LIVE", 4) == 0) {
|
||||
if (package_type_out) {
|
||||
*package_type_out = StfsPackageType::kLive;
|
||||
}
|
||||
return Error::kSuccess;
|
||||
}
|
||||
if (memcmp(map_ptr, "PIRS", 4) == 0) {
|
||||
if (package_type_out) {
|
||||
*package_type_out = StfsPackageType::kPirs;
|
||||
}
|
||||
return Error::kSuccess;
|
||||
}
|
||||
if (memcmp(map_ptr, "CON ", 4) == 0) {
|
||||
if (package_type_out) {
|
||||
*package_type_out = StfsPackageType::kCon;
|
||||
}
|
||||
return Error::kSuccess;
|
||||
}
|
||||
// Unexpected format.
|
||||
return Error::kErrorFileMismatch;
|
||||
}
|
||||
|
||||
StfsContainerDevice::Error StfsContainerDevice::ReadHeaderAndVerify(
|
||||
const uint8_t* map_ptr, size_t map_size) {
|
||||
// Check signature.
|
||||
auto type_result = ReadPackageType(map_ptr, map_size, &package_type_);
|
||||
if (type_result != Error::kSuccess) {
|
||||
return type_result;
|
||||
// Copy header & check signature
|
||||
memcpy(&header_, map_ptr, sizeof(StfsHeader));
|
||||
if (header_.header.magic != XContentPackageType::kPackageTypeCon &&
|
||||
header_.header.magic != XContentPackageType::kPackageTypeLive &&
|
||||
header_.header.magic != XContentPackageType::kPackageTypePirs) {
|
||||
// Unexpected format.
|
||||
return Error::kErrorFileMismatch;
|
||||
}
|
||||
|
||||
// Read header.
|
||||
if (!header_.Read(map_ptr)) {
|
||||
return Error::kErrorDamagedFile;
|
||||
}
|
||||
|
||||
if (((header_.header_size + 0x0FFF) & 0xB000) == 0xB000) {
|
||||
// Pre-calculate some values used in block number calculations
|
||||
if (((header_.header.header_size + 0x0FFF) & 0xB000) == 0xB000) {
|
||||
table_size_shift_ = 0;
|
||||
} else {
|
||||
table_size_shift_ = 1;
|
||||
|
@ -235,11 +207,7 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
|
|||
const char* MEDIA_MAGIC = "MICROSOFT*XBOX*MEDIA";
|
||||
|
||||
// Check for EDGF layout
|
||||
auto layout = &header_.svod_volume_descriptor.layout_type;
|
||||
auto features = header_.svod_volume_descriptor.device_features;
|
||||
bool has_egdf_layout = features & kFeatureHasEnhancedGDFLayout;
|
||||
|
||||
if (has_egdf_layout) {
|
||||
if (header_.metadata.svod_volume_descriptor.features.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
|
||||
|
@ -247,7 +215,7 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
|
|||
if (memcmp(data + 0x2000, MEDIA_MAGIC, 20) == 0) {
|
||||
base_offset_ = 0x0000;
|
||||
magic_offset_ = 0x2000;
|
||||
*layout = kEnhancedGDFLayout;
|
||||
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.");
|
||||
|
@ -264,11 +232,11 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
|
|||
// Check for XSF Header
|
||||
const char* XSF_MAGIC = "XSF";
|
||||
if (memcmp(data + 0x2000, XSF_MAGIC, 3) == 0) {
|
||||
*layout = kXSFLayout;
|
||||
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 {
|
||||
*layout = kUnknownLayout;
|
||||
svod_layout_ = SvodLayoutType::kUnknown;
|
||||
XELOGI("SVOD appears to use an XSF layout, but no header is present.");
|
||||
XELOGI("SVOD magic block found at 0x12000");
|
||||
}
|
||||
|
@ -281,11 +249,11 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() {
|
|||
magic_offset_ = 0xD000;
|
||||
|
||||
// Check for single file system
|
||||
if (header_.data_file_count == 1) {
|
||||
*layout = kSingleFileLayout;
|
||||
if (header_.metadata.data_file_count == 1) {
|
||||
svod_layout_ = SvodLayoutType::kSingleFile;
|
||||
XELOGI("SVOD is a single file. Magic block present at 0xD000.");
|
||||
} else {
|
||||
*layout = kUnknownLayout;
|
||||
svod_layout_ = SvodLayoutType::kUnknown;
|
||||
XELOGE(
|
||||
"SVOD is not a single file, but the magic block was found at "
|
||||
"0xD000.");
|
||||
|
@ -450,12 +418,12 @@ void StfsContainerDevice::BlockToOffsetSVOD(size_t block, size_t* out_address,
|
|||
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_.svod_volume_descriptor.data_block_offset;
|
||||
const SvodLayoutType LAYOUT = header_.svod_volume_descriptor.layout_type;
|
||||
const size_t BLOCK_OFFSET =
|
||||
header_.metadata.svod_volume_descriptor.start_data_block();
|
||||
|
||||
// Resolve the true block address and file index
|
||||
size_t true_block = block - (BLOCK_OFFSET * 2);
|
||||
if (LAYOUT == kEnhancedGDFLayout) {
|
||||
if (svod_layout_ == SvodLayoutType::kEnhancedGDF) {
|
||||
// EGDF has an 0x1000 byte offset, which is two blocks
|
||||
true_block += 0x2;
|
||||
}
|
||||
|
@ -473,7 +441,7 @@ void StfsContainerDevice::BlockToOffsetSVOD(size_t block, size_t* out_address,
|
|||
offset += level1_table_count * HASH_BLOCK_SIZE;
|
||||
|
||||
// For single-file SVOD layouts, include the size of the header in the offset.
|
||||
if (LAYOUT == kSingleFileLayout) {
|
||||
if (svod_layout_ == SvodLayoutType::kSingleFile) {
|
||||
offset += base_offset_;
|
||||
}
|
||||
|
||||
|
@ -500,8 +468,8 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
|
|||
std::vector<StfsContainerEntry*> all_entries;
|
||||
|
||||
// Load all listings.
|
||||
auto& volume_descriptor = header_.stfs_volume_descriptor;
|
||||
uint32_t table_block_index = volume_descriptor.file_table_block_number;
|
||||
auto& volume_descriptor = header_.metadata.stfs_volume_descriptor;
|
||||
uint32_t table_block_index = volume_descriptor.file_table_block_number();
|
||||
for (size_t n = 0; n < volume_descriptor.file_table_block_count; n++) {
|
||||
const uint8_t* p = data + BlockToOffsetSTFS(table_block_index);
|
||||
for (size_t m = 0; m < 0x1000 / 0x40; m++) {
|
||||
|
@ -559,19 +527,17 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
|
|||
if (entry->attributes() & X_FILE_ATTRIBUTE_NORMAL) {
|
||||
uint32_t block_index = start_block_index;
|
||||
size_t remaining_size = file_size;
|
||||
uint32_t info = 0x80;
|
||||
while (remaining_size && block_index && info >= 0x80) {
|
||||
while (remaining_size && block_index) {
|
||||
size_t block_size =
|
||||
std::min(static_cast<size_t>(0x1000), 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(data, block_index, 0);
|
||||
if (table_size_shift_ && block_hash.info < 0x80) {
|
||||
if (table_size_shift_) {
|
||||
block_hash = GetBlockHash(data, block_index, 1);
|
||||
}
|
||||
block_index = block_hash.next_block_index;
|
||||
info = block_hash.info;
|
||||
block_index = block_hash.level0_next_block();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,10 +545,10 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
|
|||
}
|
||||
|
||||
auto block_hash = GetBlockHash(data, table_block_index, 0);
|
||||
if (table_size_shift_ && block_hash.info < 0x80) {
|
||||
if (table_size_shift_) {
|
||||
block_hash = GetBlockHash(data, table_block_index, 1);
|
||||
}
|
||||
table_block_index = block_hash.next_block_index;
|
||||
table_block_index = block_hash.level0_next_block();
|
||||
if (table_block_index == 0xFFFFFF) {
|
||||
break;
|
||||
}
|
||||
|
@ -594,9 +560,10 @@ StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() {
|
|||
size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) {
|
||||
uint64_t block;
|
||||
uint32_t block_shift = 0;
|
||||
if (((header_.header_size + 0x0FFF) & 0xB000) == 0xB000 ||
|
||||
(header_.stfs_volume_descriptor.flags & 0x1) == 0x0) {
|
||||
block_shift = package_type_ == StfsPackageType::kCon ? 1 : 0;
|
||||
if (((header_.header.header_size + 0x0FFF) & 0xB000) == 0xB000 ||
|
||||
!header_.metadata.stfs_volume_descriptor.flags.read_only_format) {
|
||||
block_shift =
|
||||
header_.header.magic == XContentPackageType::kPackageTypeCon ? 1 : 0;
|
||||
}
|
||||
|
||||
// For every level there is a hash table
|
||||
|
@ -604,7 +571,7 @@ size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) {
|
|||
// 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 = kSTFSHashSpacing;
|
||||
uint64_t base = kBlocksPerHashLevel[0];
|
||||
block = block_index;
|
||||
for (uint32_t i = 0; i < 3; i++) {
|
||||
block += (block_index + (base << block_shift)) / (base << block_shift);
|
||||
|
@ -612,128 +579,35 @@ size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) {
|
|||
break;
|
||||
}
|
||||
|
||||
base *= kSTFSHashSpacing;
|
||||
base *= kBlocksPerHashLevel[0];
|
||||
}
|
||||
|
||||
return xe::round_up(header_.header_size, 0x1000) + (block << 12);
|
||||
return xe::round_up(header_.header.header_size, 0x1000) + (block << 12);
|
||||
}
|
||||
|
||||
StfsContainerDevice::BlockHash StfsContainerDevice::GetBlockHash(
|
||||
const uint8_t* map_ptr, uint32_t block_index, uint32_t table_offset) {
|
||||
StfsHashEntry StfsContainerDevice::GetBlockHash(const uint8_t* map_ptr,
|
||||
uint32_t block_index,
|
||||
uint32_t table_offset) {
|
||||
uint32_t record = block_index % 0xAA;
|
||||
|
||||
// This is a bit hacky, but we'll get a pointer to the first block after the
|
||||
// table and then subtract one sector to land on the table itself.
|
||||
size_t hash_offset = BlockToOffsetSTFS(
|
||||
xe::round_up(block_index + 1, kSTFSHashSpacing) - kSTFSHashSpacing);
|
||||
size_t hash_offset =
|
||||
BlockToOffsetSTFS(xe::round_up(block_index + 1, kBlocksPerHashLevel[0]) -
|
||||
kBlocksPerHashLevel[0]);
|
||||
hash_offset -= kSectorSize;
|
||||
const uint8_t* hash_data = map_ptr + hash_offset;
|
||||
|
||||
// table_index += table_offset - (1 << table_size_shift_);
|
||||
const uint8_t* record_data = hash_data + record * 0x18;
|
||||
uint32_t info = xe::load_and_swap<uint8_t>(record_data + 0x14);
|
||||
uint32_t next_block_index = load_uint24_be(record_data + 0x15);
|
||||
return {next_block_index, info};
|
||||
const StfsHashEntry* record_data =
|
||||
reinterpret_cast<const StfsHashEntry*>(hash_data + record * 0x18);
|
||||
|
||||
return *record_data;
|
||||
}
|
||||
|
||||
bool StfsVolumeDescriptor::Read(const uint8_t* p) {
|
||||
descriptor_size = xe::load_and_swap<uint8_t>(p + 0x00);
|
||||
if (descriptor_size != 0x24) {
|
||||
XELOGE("STFS volume descriptor size mismatch, expected 0x24 but got 0x{:X}",
|
||||
descriptor_size);
|
||||
return false;
|
||||
}
|
||||
version = xe::load_and_swap<uint8_t>(p + 0x01);
|
||||
flags = xe::load_and_swap<uint8_t>(p + 0x02);
|
||||
file_table_block_count = xe::load_and_swap<uint16_t>(p + 0x03);
|
||||
file_table_block_number = load_uint24_be(p + 0x05);
|
||||
std::memcpy(top_hash_table_hash, p + 0x08, 0x14);
|
||||
total_allocated_block_count = xe::load_and_swap<uint32_t>(p + 0x1C);
|
||||
total_unallocated_block_count = xe::load_and_swap<uint32_t>(p + 0x20);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SvodVolumeDescriptor::Read(const uint8_t* p) {
|
||||
descriptor_size = xe::load<uint8_t>(p + 0x00);
|
||||
if (descriptor_size != 0x24) {
|
||||
XELOGE("SVOD volume descriptor size mismatch, expected 0x24 but got 0x{:X}",
|
||||
descriptor_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
block_cache_element_count = xe::load<uint8_t>(p + 0x01);
|
||||
worker_thread_processor = xe::load<uint8_t>(p + 0x02);
|
||||
worker_thread_priority = xe::load<uint8_t>(p + 0x03);
|
||||
std::memcpy(hash, p + 0x04, 0x14);
|
||||
device_features = xe::load<uint8_t>(p + 0x18);
|
||||
data_block_count = load_uint24_be(p + 0x19);
|
||||
data_block_offset = load_uint24_le(p + 0x1C);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StfsHeader::Read(const uint8_t* p) {
|
||||
std::memcpy(license_entries, p + 0x22C, 0x100);
|
||||
std::memcpy(header_hash, p + 0x32C, 0x14);
|
||||
header_size = xe::load_and_swap<uint32_t>(p + 0x340);
|
||||
content_type = (StfsContentType)xe::load_and_swap<uint32_t>(p + 0x344);
|
||||
metadata_version = xe::load_and_swap<uint32_t>(p + 0x348);
|
||||
content_size = xe::load_and_swap<uint32_t>(p + 0x34C);
|
||||
media_id = xe::load_and_swap<uint32_t>(p + 0x354);
|
||||
version = xe::load_and_swap<uint32_t>(p + 0x358);
|
||||
base_version = xe::load_and_swap<uint32_t>(p + 0x35C);
|
||||
title_id = xe::load_and_swap<uint32_t>(p + 0x360);
|
||||
platform = (StfsPlatform)xe::load_and_swap<uint8_t>(p + 0x364);
|
||||
executable_type = xe::load_and_swap<uint8_t>(p + 0x365);
|
||||
disc_number = xe::load_and_swap<uint8_t>(p + 0x366);
|
||||
disc_in_set = xe::load_and_swap<uint8_t>(p + 0x367);
|
||||
save_game_id = xe::load_and_swap<uint32_t>(p + 0x368);
|
||||
std::memcpy(console_id, p + 0x36C, 0x5);
|
||||
std::memcpy(profile_id, p + 0x371, 0x8);
|
||||
data_file_count = xe::load_and_swap<uint32_t>(p + 0x39D);
|
||||
data_file_combined_size = xe::load_and_swap<uint64_t>(p + 0x3A1);
|
||||
descriptor_type = (StfsDescriptorType)xe::load_and_swap<uint32_t>(p + 0x3A9);
|
||||
switch (descriptor_type) {
|
||||
case StfsDescriptorType::kStfs:
|
||||
stfs_volume_descriptor.Read(p + 0x379);
|
||||
break;
|
||||
case StfsDescriptorType::kSvod:
|
||||
svod_volume_descriptor.Read(p + 0x379);
|
||||
break;
|
||||
default:
|
||||
XELOGE("STFS descriptor format not supported: {}", descriptor_type);
|
||||
return false;
|
||||
}
|
||||
memcpy(device_id, p + 0x3FD, 0x14);
|
||||
for (size_t n = 0; n < 0x900 / 2; n++) {
|
||||
display_names[n] = xe::load_and_swap<uint16_t>(p + 0x411 + n * 2);
|
||||
display_descs[n] = xe::load_and_swap<uint16_t>(p + 0xD11 + n * 2);
|
||||
}
|
||||
for (size_t n = 0; n < 0x80 / 2; n++) {
|
||||
publisher_name[n] = xe::load_and_swap<uint16_t>(p + 0x1611 + n * 2);
|
||||
title_name[n] = xe::load_and_swap<uint16_t>(p + 0x1691 + n * 2);
|
||||
}
|
||||
transfer_flags = xe::load_and_swap<uint8_t>(p + 0x1711);
|
||||
thumbnail_image_size = xe::load_and_swap<uint32_t>(p + 0x1712);
|
||||
title_thumbnail_image_size = xe::load_and_swap<uint32_t>(p + 0x1716);
|
||||
std::memcpy(thumbnail_image, p + 0x171A, 0x4000);
|
||||
std::memcpy(title_thumbnail_image, p + 0x571A, 0x4000);
|
||||
|
||||
// Metadata v2 Fields
|
||||
if (metadata_version == 2) {
|
||||
std::memcpy(series_id, p + 0x3B1, 0x10);
|
||||
std::memcpy(season_id, p + 0x3C1, 0x10);
|
||||
season_number = xe::load_and_swap<uint16_t>(p + 0x3D1);
|
||||
episode_number = xe::load_and_swap<uint16_t>(p + 0x3D5);
|
||||
|
||||
for (size_t n = 0; n < 0x300 / 2; n++) {
|
||||
additonal_display_names[n] =
|
||||
xe::load_and_swap<uint16_t>(p + 0x541A + n * 2);
|
||||
additional_display_descriptions[n] =
|
||||
xe::load_and_swap<uint16_t>(p + 0x941A + n * 2);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
uint32_t StfsContainerDevice::ReadMagic(const std::filesystem::path& path) {
|
||||
auto map = MappedMemory::Open(path, MappedMemory::Mode::kRead, 0, 4);
|
||||
return xe::load_and_swap<uint32_t>(map->data());
|
||||
}
|
||||
|
||||
bool StfsContainerDevice::ResolveFromFolder(const std::filesystem::path& path) {
|
||||
|
@ -757,9 +631,11 @@ bool StfsContainerDevice::ResolveFromFolder(const std::filesystem::path& path) {
|
|||
} else {
|
||||
// Try to read the file's magic
|
||||
auto path = current_file.path / current_file.name;
|
||||
auto map = MappedMemory::Open(path, MappedMemory::Mode::kRead, 0, 4);
|
||||
if (map && ReadPackageType(map->data(), map->size(), nullptr) ==
|
||||
Error::kSuccess) {
|
||||
auto magic = ReadMagic(path);
|
||||
|
||||
if (magic == XContentPackageType::kPackageTypeCon ||
|
||||
magic == XContentPackageType::kPackageTypeLive ||
|
||||
magic == XContentPackageType::kPackageTypePirs) {
|
||||
host_path_ = current_file.path / current_file.name;
|
||||
XELOGI("STFS Package found: {}", xe::path_to_utf8(host_path_));
|
||||
return true;
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
#include <string>
|
||||
|
||||
#include "xenia/base/mapped_memory.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
#include "xenia/kernel/util/xex2_info.h"
|
||||
#include "xenia/vfs/device.h"
|
||||
|
||||
namespace xe {
|
||||
|
@ -24,144 +26,377 @@ namespace vfs {
|
|||
|
||||
class StfsContainerEntry;
|
||||
|
||||
enum class StfsPackageType {
|
||||
kCon,
|
||||
kPirs,
|
||||
kLive,
|
||||
enum XContentPackageType : uint32_t {
|
||||
kPackageTypeCon = 0x434F4E20,
|
||||
kPackageTypePirs = 0x50495253,
|
||||
kPackageTypeLive = 0x4C495645,
|
||||
};
|
||||
|
||||
enum class StfsContentType : uint32_t {
|
||||
kArcadeTitle = 0x000D0000,
|
||||
kAvatarItem = 0x00009000,
|
||||
kCacheFile = 0x00040000,
|
||||
kCommunityGame = 0x02000000,
|
||||
kGamesOnDemand = 0x00007000,
|
||||
kGameDemo = 0x00080000,
|
||||
kGamerPicture = 0x00020000,
|
||||
kGameTitle = 0x000A0000,
|
||||
kGameTrailer = 0x000C0000,
|
||||
kGameVideo = 0x00400000,
|
||||
kInstalledGame = 0x00004000,
|
||||
kInstaller = 0x000B0000,
|
||||
kIptvPauseBuffer = 0x00002000,
|
||||
kLicenseStore = 0x000F0000,
|
||||
kMarketplaceContent = 0x00000002,
|
||||
kMovie = 0x00100000,
|
||||
kMusicVideo = 0x00300000,
|
||||
kPodcastVideo = 0x00500000,
|
||||
kProfile = 0x00010000,
|
||||
kPublisher = 0x00000003,
|
||||
enum XContentType : uint32_t {
|
||||
kSavedGame = 0x00000001,
|
||||
kStorageDownload = 0x00050000,
|
||||
kTheme = 0x00030000,
|
||||
kTV = 0x00200000,
|
||||
kVideo = 0x00090000,
|
||||
kViralVideo = 0x00600000,
|
||||
kXboxDownload = 0x00070000,
|
||||
kXboxOriginalGame = 0x00005000,
|
||||
kXboxSavedGame = 0x00060000,
|
||||
kMarketplaceContent = 0x00000002,
|
||||
kPublisher = 0x00000003,
|
||||
kXbox360Title = 0x00001000,
|
||||
kIptvPauseBuffer = 0x00002000,
|
||||
kXNACommunity = 0x00003000,
|
||||
kInstalledGame = 0x00004000,
|
||||
kXboxTitle = 0x00005000,
|
||||
kSocialTitle = 0x00006000,
|
||||
kGamesOnDemand = 0x00007000,
|
||||
kSUStoragePack = 0x00008000,
|
||||
kAvatarItem = 0x00009000,
|
||||
kProfile = 0x00010000,
|
||||
kGamerPicture = 0x00020000,
|
||||
kTheme = 0x00030000,
|
||||
kCacheFile = 0x00040000,
|
||||
kStorageDownload = 0x00050000,
|
||||
kXboxSavedGame = 0x00060000,
|
||||
kXboxDownload = 0x00070000,
|
||||
kGameDemo = 0x00080000,
|
||||
kVideo = 0x00090000,
|
||||
kGameTitle = 0x000A0000,
|
||||
kInstaller = 0x000B0000,
|
||||
kGameTrailer = 0x000C0000,
|
||||
kArcadeTitle = 0x000D0000,
|
||||
kXNA = 0x000E0000,
|
||||
kLicenseStore = 0x000F0000,
|
||||
kMovie = 0x00100000,
|
||||
kTV = 0x00200000,
|
||||
kMusicVideo = 0x00300000,
|
||||
kGameVideo = 0x00400000,
|
||||
kPodcastVideo = 0x00500000,
|
||||
kViralVideo = 0x00600000,
|
||||
kCommunityGame = 0x02000000,
|
||||
};
|
||||
|
||||
enum class StfsPlatform : uint8_t {
|
||||
kXbox360 = 0x02,
|
||||
kPc = 0x04,
|
||||
};
|
||||
|
||||
enum class StfsDescriptorType : uint32_t {
|
||||
enum class XContentVolumeType : uint32_t {
|
||||
kStfs = 0,
|
||||
kSvod = 1,
|
||||
};
|
||||
|
||||
struct StfsVolumeDescriptor {
|
||||
bool Read(const uint8_t* p);
|
||||
|
||||
uint8_t descriptor_size;
|
||||
/* STFS structures */
|
||||
XEPACKEDSTRUCT(StfsVolumeDescriptor, {
|
||||
uint8_t descriptor_length;
|
||||
uint8_t version;
|
||||
uint8_t flags;
|
||||
union {
|
||||
struct {
|
||||
uint8_t read_only_format : 1; // if set, only uses a single backing-block
|
||||
// per hash table (no resiliency),
|
||||
// otherwise uses two
|
||||
uint8_t root_active_index : 1; // if set, uses secondary backing-block
|
||||
// for the highest-level hash table
|
||||
uint8_t directory_overallocated : 1;
|
||||
uint8_t directory_index_bounds_valid : 1;
|
||||
};
|
||||
uint8_t as_byte;
|
||||
} flags;
|
||||
uint16_t file_table_block_count;
|
||||
uint32_t file_table_block_number;
|
||||
uint8_t file_table_block_number_0;
|
||||
uint8_t file_table_block_number_1;
|
||||
uint8_t file_table_block_number_2;
|
||||
uint8_t top_hash_table_hash[0x14];
|
||||
uint32_t total_allocated_block_count;
|
||||
uint32_t total_unallocated_block_count;
|
||||
be<uint32_t> allocated_block_count;
|
||||
be<uint32_t> free_block_count;
|
||||
|
||||
uint32_t file_table_block_number() {
|
||||
return uint32_t(file_table_block_number_0) |
|
||||
(uint32_t(file_table_block_number_1) << 8) |
|
||||
(uint32_t(file_table_block_number_2) << 16);
|
||||
}
|
||||
});
|
||||
static_assert_size(StfsVolumeDescriptor, 0x24);
|
||||
|
||||
struct StfsHashEntry {
|
||||
uint8_t sha1[0x14];
|
||||
|
||||
uint8_t info0; // usually contains flags
|
||||
|
||||
uint8_t info1;
|
||||
uint8_t info2;
|
||||
uint8_t info3;
|
||||
|
||||
// If this is a level0 entry, this points to the next block in the chain
|
||||
uint32_t level0_next_block() {
|
||||
return uint32_t(info3) | (uint32_t(info2) << 8) | (uint32_t(info1) << 16);
|
||||
}
|
||||
|
||||
void level0_next_block(uint32_t value) {
|
||||
info3 = uint8_t(value & 0xFF);
|
||||
info2 = uint8_t((value >> 8) & 0xFF);
|
||||
info1 = uint8_t((value >> 16) & 0xFF);
|
||||
}
|
||||
|
||||
// If this is level 1 or 2, this says whether the hash table this entry refers
|
||||
// to is using the secondary block or not
|
||||
bool levelN_activeindex() { return info0 & 0x40; }
|
||||
|
||||
bool levelN_writeable() { return info0 & 0x80; }
|
||||
};
|
||||
static_assert_size(StfsHashEntry, 0x18);
|
||||
|
||||
enum SvodDeviceFeatures {
|
||||
kFeatureHasEnhancedGDFLayout = 0x40,
|
||||
};
|
||||
|
||||
enum SvodLayoutType {
|
||||
kUnknownLayout = 0x0,
|
||||
kEnhancedGDFLayout = 0x1,
|
||||
kXSFLayout = 0x2,
|
||||
kSingleFileLayout = 0x4,
|
||||
};
|
||||
|
||||
struct SvodVolumeDescriptor {
|
||||
bool Read(const uint8_t* p);
|
||||
|
||||
uint8_t descriptor_size;
|
||||
/* SVOD structures */
|
||||
struct SvodDeviceDescriptor {
|
||||
uint8_t descriptor_length;
|
||||
uint8_t block_cache_element_count;
|
||||
uint8_t worker_thread_processor;
|
||||
uint8_t worker_thread_priority;
|
||||
uint8_t hash[0x14];
|
||||
uint8_t device_features;
|
||||
uint32_t data_block_count;
|
||||
uint32_t data_block_offset;
|
||||
// 0x5 padding bytes...
|
||||
|
||||
SvodLayoutType layout_type;
|
||||
};
|
||||
|
||||
class StfsHeader {
|
||||
public:
|
||||
bool Read(const uint8_t* p);
|
||||
|
||||
uint8_t license_entries[0x100];
|
||||
uint8_t header_hash[0x14];
|
||||
uint32_t header_size;
|
||||
StfsContentType content_type;
|
||||
uint32_t metadata_version;
|
||||
uint64_t content_size;
|
||||
uint32_t media_id;
|
||||
uint32_t version;
|
||||
uint32_t base_version;
|
||||
uint32_t title_id;
|
||||
StfsPlatform platform;
|
||||
uint8_t executable_type;
|
||||
uint8_t disc_number;
|
||||
uint8_t disc_in_set;
|
||||
uint32_t save_game_id;
|
||||
uint8_t console_id[0x5];
|
||||
uint8_t profile_id[0x8];
|
||||
uint8_t first_fragment_hash_entry[0x14];
|
||||
union {
|
||||
StfsVolumeDescriptor stfs_volume_descriptor;
|
||||
SvodVolumeDescriptor svod_volume_descriptor;
|
||||
};
|
||||
uint32_t data_file_count;
|
||||
uint64_t data_file_combined_size;
|
||||
StfsDescriptorType descriptor_type;
|
||||
uint8_t device_id[0x14];
|
||||
char16_t display_names[0x900 / 2];
|
||||
char16_t display_descs[0x900 / 2];
|
||||
char16_t publisher_name[0x80 / 2];
|
||||
char16_t title_name[0x80 / 2];
|
||||
uint8_t transfer_flags;
|
||||
uint32_t thumbnail_image_size;
|
||||
uint32_t title_thumbnail_image_size;
|
||||
uint8_t thumbnail_image[0x4000];
|
||||
uint8_t title_thumbnail_image[0x4000];
|
||||
struct {
|
||||
uint8_t must_be_zero_for_future_usage : 6;
|
||||
uint8_t enhanced_gdf_layout : 1;
|
||||
uint8_t zero_for_downlevel_clients : 1;
|
||||
};
|
||||
uint8_t as_byte;
|
||||
} features;
|
||||
uint8_t num_data_blocks2;
|
||||
uint8_t num_data_blocks1;
|
||||
uint8_t num_data_blocks0;
|
||||
uint8_t start_data_block0;
|
||||
uint8_t start_data_block1;
|
||||
uint8_t start_data_block2;
|
||||
uint8_t reserved[5];
|
||||
|
||||
// Metadata v2 Fields
|
||||
uint32_t num_data_blocks() {
|
||||
return uint32_t(num_data_blocks0) | (uint32_t(num_data_blocks1) << 8) |
|
||||
(uint32_t(num_data_blocks2) << 16);
|
||||
}
|
||||
|
||||
uint32_t start_data_block() {
|
||||
return uint32_t(start_data_block0) | (uint32_t(start_data_block1) << 8) |
|
||||
(uint32_t(start_data_block2) << 16);
|
||||
}
|
||||
};
|
||||
static_assert_size(SvodDeviceDescriptor, 0x24);
|
||||
|
||||
/* XContent structures */
|
||||
struct XContentMediaData {
|
||||
uint8_t series_id[0x10];
|
||||
uint8_t season_id[0x10];
|
||||
int16_t season_number;
|
||||
int16_t episode_number;
|
||||
char16_t additonal_display_names[0x300 / 2];
|
||||
char16_t additional_display_descriptions[0x300 / 2];
|
||||
be<uint16_t> season_number;
|
||||
be<uint16_t> episode_number;
|
||||
};
|
||||
static_assert_size(XContentMediaData, 0x24);
|
||||
|
||||
struct XContentAvatarAssetData {
|
||||
be<uint32_t> sub_category;
|
||||
be<uint32_t> colorizable;
|
||||
uint8_t asset_id[0x10];
|
||||
uint8_t skeleton_version_mask;
|
||||
uint8_t reserved[0xB];
|
||||
};
|
||||
static_assert_size(XContentAvatarAssetData, 0x24);
|
||||
|
||||
struct XContentAttributes {
|
||||
uint8_t profile_transfer : 1;
|
||||
uint8_t device_transfer : 1;
|
||||
uint8_t move_only_transfer : 1;
|
||||
uint8_t kinect_enabled : 1;
|
||||
uint8_t disable_network_storage : 1;
|
||||
uint8_t deep_link_supported : 1;
|
||||
uint8_t reserved : 2;
|
||||
};
|
||||
static_assert_size(XContentAttributes, 1);
|
||||
|
||||
XEPACKEDSTRUCT(XContentMetadata, {
|
||||
static const uint32_t kThumbLengthV1 = 0x4000;
|
||||
static const uint32_t kThumbLengthV2 = 0x3D00;
|
||||
|
||||
static const uint32_t kNumLanguagesV1 = 9;
|
||||
// metadata_version 2 adds 3 languages inside thumbnail/title_thumbnail space
|
||||
static const uint32_t kNumLanguagesV2 = 12;
|
||||
|
||||
be<XContentType> content_type;
|
||||
be<uint32_t> metadata_version;
|
||||
be<uint64_t> content_size;
|
||||
xex2_opt_execution_info execution_info;
|
||||
uint8_t console_id[5];
|
||||
be<uint64_t> profile_id;
|
||||
union {
|
||||
StfsVolumeDescriptor stfs_volume_descriptor;
|
||||
SvodDeviceDescriptor svod_volume_descriptor;
|
||||
};
|
||||
be<uint32_t> data_file_count;
|
||||
be<uint64_t> data_file_size;
|
||||
be<XContentVolumeType> volume_type;
|
||||
be<uint64_t> online_creator;
|
||||
be<uint32_t> category;
|
||||
uint8_t reserved2[0x20];
|
||||
union {
|
||||
XContentMediaData media_data;
|
||||
XContentAvatarAssetData avatar_asset_data;
|
||||
};
|
||||
uint8_t device_id[0x14];
|
||||
union {
|
||||
be<uint16_t> display_name_raw[kNumLanguagesV1][128];
|
||||
char16_t display_name_chars[kNumLanguagesV1][128];
|
||||
};
|
||||
union {
|
||||
be<uint16_t> description_raw[kNumLanguagesV1][128];
|
||||
char16_t description_chars[kNumLanguagesV1][128];
|
||||
};
|
||||
union {
|
||||
be<uint16_t> publisher_raw[64];
|
||||
char16_t publisher_chars[64];
|
||||
};
|
||||
union {
|
||||
be<uint16_t> title_name_raw[64];
|
||||
char16_t title_name_chars[64];
|
||||
};
|
||||
union {
|
||||
XContentAttributes bits;
|
||||
uint8_t as_byte;
|
||||
} flags;
|
||||
be<uint32_t> thumbnail_size;
|
||||
be<uint32_t> title_thumbnail_size;
|
||||
uint8_t thumbnail[kThumbLengthV2];
|
||||
union {
|
||||
be<uint16_t> display_name_ex_raw[kNumLanguagesV2 - kNumLanguagesV1][128];
|
||||
char16_t display_name_ex_chars[kNumLanguagesV2 - kNumLanguagesV1][128];
|
||||
};
|
||||
uint8_t title_thumbnail[kThumbLengthV2];
|
||||
union {
|
||||
be<uint16_t> description_ex_raw[kNumLanguagesV2 - kNumLanguagesV1][128];
|
||||
char16_t description_ex_chars[kNumLanguagesV2 - kNumLanguagesV1][128];
|
||||
};
|
||||
|
||||
std::u16string display_name(uint32_t lang_id) const {
|
||||
lang_id--;
|
||||
if (lang_id >= kNumLanguagesV2) {
|
||||
assert_always();
|
||||
lang_id = 0; // no room for this lang, read from english slot..
|
||||
}
|
||||
|
||||
const be<uint16_t>* str = 0;
|
||||
if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
|
||||
str = display_name_raw[lang_id];
|
||||
} else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
|
||||
metadata_version >= 2) {
|
||||
str = display_name_ex_raw[lang_id - kNumLanguagesV1];
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return u"";
|
||||
}
|
||||
|
||||
return load_and_swap<std::u16string>(str);
|
||||
}
|
||||
|
||||
std::u16string description(uint32_t lang_id) const {
|
||||
lang_id--;
|
||||
if (lang_id >= kNumLanguagesV2) {
|
||||
assert_always();
|
||||
lang_id = 0; // no room for this lang, read from english slot..
|
||||
}
|
||||
|
||||
const be<uint16_t>* str = 0;
|
||||
if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
|
||||
str = description_raw[lang_id];
|
||||
} else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
|
||||
metadata_version >= 2) {
|
||||
str = description_ex_raw[lang_id - kNumLanguagesV1];
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return u"";
|
||||
}
|
||||
|
||||
return load_and_swap<std::u16string>(str);
|
||||
}
|
||||
|
||||
std::u16string publisher() const {
|
||||
return load_and_swap<std::u16string>(publisher_raw);
|
||||
}
|
||||
|
||||
std::u16string title_name() const {
|
||||
return load_and_swap<std::u16string>(title_name_raw);
|
||||
}
|
||||
|
||||
bool set_display_name(uint32_t lang_id, const std::u16string_view value) {
|
||||
lang_id--;
|
||||
if (lang_id >= kNumLanguagesV2) {
|
||||
assert_always();
|
||||
lang_id = 0; // no room for this lang, store in english slot..
|
||||
}
|
||||
|
||||
char16_t* str = 0;
|
||||
if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
|
||||
str = display_name_chars[lang_id];
|
||||
} else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
|
||||
metadata_version >= 2) {
|
||||
str = display_name_ex_chars[lang_id - kNumLanguagesV1];
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string_util::copy_and_swap_truncating(str, value,
|
||||
countof(display_name_chars[0]));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_description(uint32_t lang_id, const std::u16string_view value) {
|
||||
lang_id--;
|
||||
if (lang_id >= kNumLanguagesV2) {
|
||||
assert_always();
|
||||
lang_id = 0; // no room for this lang, store in english slot..
|
||||
}
|
||||
|
||||
char16_t* str = 0;
|
||||
if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
|
||||
str = description_chars[lang_id];
|
||||
} else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
|
||||
metadata_version >= 2) {
|
||||
str = description_ex_chars[lang_id - kNumLanguagesV1];
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string_util::copy_and_swap_truncating(str, value,
|
||||
countof(description_chars[0]));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_publisher(const std::u16string_view value) {
|
||||
string_util::copy_and_swap_truncating(publisher_chars, value,
|
||||
countof(publisher_chars));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_title_name(const std::u16string_view value) {
|
||||
string_util::copy_and_swap_truncating(title_name_chars, value,
|
||||
countof(title_name_chars));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
static_assert_size(XContentMetadata, 0x93D6);
|
||||
|
||||
struct XContentLicense {
|
||||
be<uint64_t> licensee_id;
|
||||
be<uint32_t> license_bits;
|
||||
be<uint32_t> license_flags;
|
||||
};
|
||||
static_assert_size(XContentLicense, 0x10);
|
||||
|
||||
XEPACKEDSTRUCT(XContentHeader, {
|
||||
be<XContentPackageType> magic;
|
||||
uint8_t signature[0x228];
|
||||
XContentLicense licenses[0x10];
|
||||
uint8_t content_id[0x14];
|
||||
be<uint32_t> header_size;
|
||||
});
|
||||
static_assert_size(XContentHeader, 0x344);
|
||||
|
||||
struct StfsHeader {
|
||||
XContentHeader header;
|
||||
XContentMetadata metadata;
|
||||
// TODO: title/system updates contain more data after XContentMetadata, seems
|
||||
// to affect header.header_size
|
||||
};
|
||||
static_assert_size(StfsHeader, 0x971A);
|
||||
|
||||
class StfsContainerDevice : public Device {
|
||||
public:
|
||||
|
@ -187,6 +422,7 @@ class StfsContainerDevice : public Device {
|
|||
|
||||
private:
|
||||
const uint32_t kSectorSize = 0x1000;
|
||||
const uint32_t kBlocksPerHashLevel[3] = {170, 28900, 4913000};
|
||||
|
||||
enum class Error {
|
||||
kSuccess = 0,
|
||||
|
@ -196,18 +432,17 @@ class StfsContainerDevice : public Device {
|
|||
kErrorDamagedFile = -31,
|
||||
};
|
||||
|
||||
struct BlockHash {
|
||||
uint32_t next_block_index;
|
||||
uint32_t info;
|
||||
enum class SvodLayoutType {
|
||||
kUnknown = 0x0,
|
||||
kEnhancedGDF = 0x1,
|
||||
kXSF = 0x2,
|
||||
kSingleFile = 0x4,
|
||||
};
|
||||
|
||||
const uint32_t kSTFSHashSpacing = 170;
|
||||
|
||||
uint32_t ReadMagic(const std::filesystem::path& path);
|
||||
bool ResolveFromFolder(const std::filesystem::path& path);
|
||||
|
||||
Error MapFiles();
|
||||
static Error ReadPackageType(const uint8_t* map_ptr, size_t map_size,
|
||||
StfsPackageType* package_type_out);
|
||||
Error ReadHeaderAndVerify(const uint8_t* map_ptr, size_t map_size);
|
||||
|
||||
Error ReadSVOD();
|
||||
|
@ -218,8 +453,8 @@ class StfsContainerDevice : public Device {
|
|||
Error ReadSTFS();
|
||||
size_t BlockToOffsetSTFS(uint64_t block);
|
||||
|
||||
BlockHash GetBlockHash(const uint8_t* map_ptr, uint32_t block_index,
|
||||
uint32_t table_offset);
|
||||
StfsHashEntry GetBlockHash(const uint8_t* map_ptr, uint32_t block_index,
|
||||
uint32_t table_offset);
|
||||
|
||||
std::string name_;
|
||||
std::filesystem::path host_path_;
|
||||
|
@ -229,8 +464,8 @@ class StfsContainerDevice : public Device {
|
|||
size_t base_offset_;
|
||||
size_t magic_offset_;
|
||||
std::unique_ptr<Entry> root_entry_;
|
||||
StfsPackageType package_type_;
|
||||
StfsHeader header_;
|
||||
SvodLayoutType svod_layout_;
|
||||
uint32_t table_size_shift_;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue