diff --git a/src/xenia/vfs/devices/stfs_container_device.cc b/src/xenia/vfs/devices/stfs_container_device.cc index 22499e057..52c7b3a3f 100644 --- a/src/xenia/vfs/devices/stfs_container_device.cc +++ b/src/xenia/vfs/devices/stfs_container_device.cc @@ -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 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(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(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(hash_data + record * 0x18); + + return *record_data; } -bool StfsVolumeDescriptor::Read(const uint8_t* p) { - descriptor_size = xe::load_and_swap(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(p + 0x01); - flags = xe::load_and_swap(p + 0x02); - file_table_block_count = xe::load_and_swap(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(p + 0x1C); - total_unallocated_block_count = xe::load_and_swap(p + 0x20); - return true; -} - -bool SvodVolumeDescriptor::Read(const uint8_t* p) { - descriptor_size = xe::load(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(p + 0x01); - worker_thread_processor = xe::load(p + 0x02); - worker_thread_priority = xe::load(p + 0x03); - std::memcpy(hash, p + 0x04, 0x14); - device_features = xe::load(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(p + 0x340); - content_type = (StfsContentType)xe::load_and_swap(p + 0x344); - metadata_version = xe::load_and_swap(p + 0x348); - content_size = xe::load_and_swap(p + 0x34C); - media_id = xe::load_and_swap(p + 0x354); - version = xe::load_and_swap(p + 0x358); - base_version = xe::load_and_swap(p + 0x35C); - title_id = xe::load_and_swap(p + 0x360); - platform = (StfsPlatform)xe::load_and_swap(p + 0x364); - executable_type = xe::load_and_swap(p + 0x365); - disc_number = xe::load_and_swap(p + 0x366); - disc_in_set = xe::load_and_swap(p + 0x367); - save_game_id = xe::load_and_swap(p + 0x368); - std::memcpy(console_id, p + 0x36C, 0x5); - std::memcpy(profile_id, p + 0x371, 0x8); - data_file_count = xe::load_and_swap(p + 0x39D); - data_file_combined_size = xe::load_and_swap(p + 0x3A1); - descriptor_type = (StfsDescriptorType)xe::load_and_swap(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(p + 0x411 + n * 2); - display_descs[n] = xe::load_and_swap(p + 0xD11 + n * 2); - } - for (size_t n = 0; n < 0x80 / 2; n++) { - publisher_name[n] = xe::load_and_swap(p + 0x1611 + n * 2); - title_name[n] = xe::load_and_swap(p + 0x1691 + n * 2); - } - transfer_flags = xe::load_and_swap(p + 0x1711); - thumbnail_image_size = xe::load_and_swap(p + 0x1712); - title_thumbnail_image_size = xe::load_and_swap(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(p + 0x3D1); - episode_number = xe::load_and_swap(p + 0x3D5); - - for (size_t n = 0; n < 0x300 / 2; n++) { - additonal_display_names[n] = - xe::load_and_swap(p + 0x541A + n * 2); - additional_display_descriptions[n] = - xe::load_and_swap(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(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; diff --git a/src/xenia/vfs/devices/stfs_container_device.h b/src/xenia/vfs/devices/stfs_container_device.h index f70c78b08..eacd422fc 100644 --- a/src/xenia/vfs/devices/stfs_container_device.h +++ b/src/xenia/vfs/devices/stfs_container_device.h @@ -15,6 +15,8 @@ #include #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 allocated_block_count; + be 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 season_number; + be episode_number; }; +static_assert_size(XContentMediaData, 0x24); + +struct XContentAvatarAssetData { + be sub_category; + be 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 content_type; + be metadata_version; + be content_size; + xex2_opt_execution_info execution_info; + uint8_t console_id[5]; + be profile_id; + union { + StfsVolumeDescriptor stfs_volume_descriptor; + SvodDeviceDescriptor svod_volume_descriptor; + }; + be data_file_count; + be data_file_size; + be volume_type; + be online_creator; + be category; + uint8_t reserved2[0x20]; + union { + XContentMediaData media_data; + XContentAvatarAssetData avatar_asset_data; + }; + uint8_t device_id[0x14]; + union { + be display_name_raw[kNumLanguagesV1][128]; + char16_t display_name_chars[kNumLanguagesV1][128]; + }; + union { + be description_raw[kNumLanguagesV1][128]; + char16_t description_chars[kNumLanguagesV1][128]; + }; + union { + be publisher_raw[64]; + char16_t publisher_chars[64]; + }; + union { + be title_name_raw[64]; + char16_t title_name_chars[64]; + }; + union { + XContentAttributes bits; + uint8_t as_byte; + } flags; + be thumbnail_size; + be title_thumbnail_size; + uint8_t thumbnail[kThumbLengthV2]; + union { + be display_name_ex_raw[kNumLanguagesV2 - kNumLanguagesV1][128]; + char16_t display_name_ex_chars[kNumLanguagesV2 - kNumLanguagesV1][128]; + }; + uint8_t title_thumbnail[kThumbLengthV2]; + union { + be 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* 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(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* 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(str); + } + + std::u16string publisher() const { + return load_and_swap(publisher_raw); + } + + std::u16string title_name() const { + return load_and_swap(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 licensee_id; + be license_bits; + be license_flags; +}; +static_assert_size(XContentLicense, 0x10); + +XEPACKEDSTRUCT(XContentHeader, { + be magic; + uint8_t signature[0x228]; + XContentLicense licenses[0x10]; + uint8_t content_id[0x14]; + be 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 root_entry_; - StfsPackageType package_type_; StfsHeader header_; + SvodLayoutType svod_layout_; uint32_t table_size_shift_; };