[VFS] STFS/XContent structure improvements

Now makes use of xe::be<T> and removes need for read() method
This commit is contained in:
emoose 2021-04-17 13:44:54 +01:00 committed by Rick Gibbed
parent 010e14b4cd
commit 84f566d92b
2 changed files with 419 additions and 308 deletions

View File

@ -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;

View File

@ -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_;
};