/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #ifndef XENIA_VFS_DEVICES_STFS_XBOX_H_ #define XENIA_VFS_DEVICES_STFS_XBOX_H_ #include "xenia/base/string_util.h" #include "xenia/kernel/util/xex2_info.h" namespace xe { namespace vfs { // Structs used for interchange between Xenia and actual Xbox360 kernel/XAM inline uint32_t load_uint24_be(const uint8_t* p) { return (uint32_t(p[0]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[2]); } inline uint32_t load_uint24_le(const uint8_t* p) { return (uint32_t(p[2]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[0]); } inline void store_uint24_le(uint8_t* p, uint32_t value) { p[2] = uint8_t((value >> 16) & 0xFF); p[1] = uint8_t((value >> 8) & 0xFF); p[0] = uint8_t(value & 0xFF); } enum class XContentPackageType : uint32_t { kCon = 0x434F4E20, kPirs = 0x50495253, kLive = 0x4C495645, }; enum XContentType : uint32_t { kSavedGame = 0x00000001, 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 XContentVolumeType : uint32_t { kStfs = 0, kSvod = 1, }; /* STFS structures */ #pragma pack(push, 1) struct StfsVolumeDescriptor { uint8_t descriptor_length; uint8_t version; 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; } bits; uint8_t as_byte; } flags; uint16_t file_table_block_count; uint8_t file_table_block_number_raw[3]; uint8_t top_hash_table_hash[0x14]; be total_block_count; be free_block_count; uint32_t file_table_block_number() const { return load_uint24_le(file_table_block_number_raw); } void set_file_table_block_number(uint32_t value) { store_uint24_le(file_table_block_number_raw, value); } bool is_valid() const { return descriptor_length == sizeof(StfsVolumeDescriptor); } }; static_assert_size(StfsVolumeDescriptor, 0x24); #pragma pack(pop) enum class StfsHashState : uint8_t { kFree = 0, // unallocated but doesn't exist in package (needs to expand)? kFree2 = 1, // unallocated but exists in package? kInUse = 2, }; struct StfsHashEntry { uint8_t sha1[0x14]; xe::be info_raw; uint32_t level0_next_block() const { return info_raw & 0xFFFFFF; } void set_level0_next_block(uint32_t value) { info_raw = (info_raw & ~0xFFFFFF) | (value & 0xFFFFFF); } StfsHashState level0_allocation_state() const { return StfsHashState(uint8_t(((info_raw & 0xC0000000) >> 30) & 0xFF)); } void set_level0_allocation_state(StfsHashState value) { info_raw = (info_raw & ~0xC0000000) | (uint32_t(value) << 30); } uint32_t levelN_num_blocks_free() const { return info_raw & 0x7FFF; } void set_levelN_num_blocks_free(uint32_t value) { info_raw = (info_raw & ~0x7FFF) | (value & 0x7FFF); } uint32_t levelN_num_blocks_unk() const { return ((info_raw & 0x3FFF8000) >> 15) & 0x7FFF; } void set_levelN_num_blocks_unk(uint32_t value) { info_raw = (info_raw & ~0x3FFF8000) | ((value & 0x7FFF) << 15); } bool levelN_active_index() const { return (info_raw & 0x40000000) != 0; } void set_levelN_active_index(bool value) { info_raw = (info_raw & ~0x40000000) | (value ? 0x40000000 : 0); } bool levelN_writeable() const { return (info_raw & 0x80000000) != 0; } void set_levelN_writeable(bool value) { info_raw = (info_raw & ~0x80000000) | (value ? 0x80000000 : 0); } }; static_assert_size(StfsHashEntry, 0x18); struct StfsHashTable { StfsHashEntry entries[170]; xe::be num_blocks; // num L0 blocks covered by this table? uint8_t padding[12]; }; static_assert_size(StfsHashTable, 0x1000); struct StfsDirectoryEntry { char name[40]; struct { uint8_t name_length : 6; uint8_t contiguous : 1; uint8_t directory : 1; } flags; uint8_t valid_data_blocks_raw[3]; uint8_t allocated_data_blocks_raw[3]; uint8_t start_block_number_raw[3]; be directory_index; be length; be create_date; be create_time; be modified_date; be modified_time; uint32_t valid_data_blocks() const { return load_uint24_le(valid_data_blocks_raw); } void set_valid_data_blocks(uint32_t value) { store_uint24_le(valid_data_blocks_raw, value); } uint32_t allocated_data_blocks() const { return load_uint24_le(allocated_data_blocks_raw); } void set_allocated_data_blocks(uint32_t value) { store_uint24_le(allocated_data_blocks_raw, value); } uint32_t start_block_number() const { return load_uint24_le(start_block_number_raw); } void set_start_block_number(uint32_t value) { store_uint24_le(start_block_number_raw, value); } }; static_assert_size(StfsDirectoryEntry, 0x40); struct StfsDirectoryBlock { StfsDirectoryEntry entries[0x40]; }; static_assert_size(StfsDirectoryBlock, 0x1000); /* 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 first_fragment_hash_entry[0x14]; union { struct { uint8_t must_be_zero_for_future_usage : 6; uint8_t enhanced_gdf_layout : 1; uint8_t zero_for_downlevel_clients : 1; } bits; uint8_t as_byte; } features; uint8_t num_data_blocks_raw[3]; uint8_t start_data_block_raw[3]; uint8_t reserved[5]; uint32_t num_data_blocks() { return load_uint24_le(num_data_blocks_raw); } uint32_t start_data_block() { return load_uint24_le(start_data_block_raw); } }; static_assert_size(SvodDeviceDescriptor, 0x24); /* XContent structures */ struct XContentLicense { be licensee_id; be license_bits; be license_flags; }; static_assert_size(XContentLicense, 0x10); struct XContentMediaData { uint8_t series_id[0x10]; uint8_t season_id[0x10]; 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); #pragma pack(push, 1) struct 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; 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; } metadata_v2; uint8_t device_id[0x14]; union { be uint[kNumLanguagesV1][128]; char16_t chars[kNumLanguagesV1][128]; } display_name_raw; union { be uint[kNumLanguagesV1][128]; char16_t chars[kNumLanguagesV1][128]; } description_raw; union { be uint[64]; char16_t chars[64]; } publisher_raw; union { be uint[64]; char16_t chars[64]; } title_name_raw; union { XContentAttributes bits; uint8_t as_byte; } flags; be thumbnail_size; be title_thumbnail_size; uint8_t thumbnail[kThumbLengthV2]; union { be uint[kNumLanguagesV2 - kNumLanguagesV1][128]; char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128]; } display_name_ex_raw; uint8_t title_thumbnail[kThumbLengthV2]; union { be uint[kNumLanguagesV2 - kNumLanguagesV1][128]; char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128]; } description_ex_raw; std::u16string display_name(XLanguage language) const { uint32_t lang_id = uint32_t(language) - 1; if (lang_id >= kNumLanguagesV2) { assert_always(); // no room for this lang, read from english slot.. lang_id = uint32_t(XLanguage::kEnglish) - 1; } const be* str = 0; if (lang_id >= 0 && lang_id < kNumLanguagesV1) { str = display_name_raw.uint[lang_id]; } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { str = display_name_ex_raw.uint[lang_id - kNumLanguagesV1]; } if (!str) { // Invalid language ID? assert_always(); return u""; } return load_and_swap(str); } std::u16string description(XLanguage language) const { uint32_t lang_id = uint32_t(language) - 1; if (lang_id >= kNumLanguagesV2) { assert_always(); // no room for this lang, read from english slot.. lang_id = uint32_t(XLanguage::kEnglish) - 1; } const be* str = 0; if (lang_id >= 0 && lang_id < kNumLanguagesV1) { str = description_raw.uint[lang_id]; } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { str = description_ex_raw.uint[lang_id - kNumLanguagesV1]; } if (!str) { // Invalid language ID? assert_always(); return u""; } return load_and_swap(str); } std::u16string publisher() const { return load_and_swap(publisher_raw.uint); } std::u16string title_name() const { return load_and_swap(title_name_raw.uint); } bool set_display_name(XLanguage language, const std::u16string_view value) { uint32_t lang_id = uint32_t(language) - 1; if (lang_id >= kNumLanguagesV2) { assert_always(); // no room for this lang, store in english slot.. lang_id = uint32_t(XLanguage::kEnglish) - 1; } char16_t* str = 0; if (lang_id >= 0 && lang_id < kNumLanguagesV1) { str = display_name_raw.chars[lang_id]; } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { str = display_name_ex_raw.chars[lang_id - kNumLanguagesV1]; } if (!str) { // Invalid language ID? assert_always(); return false; } string_util::copy_and_swap_truncating(str, value, countof(display_name_raw.chars[0])); return true; } bool set_description(XLanguage language, const std::u16string_view value) { uint32_t lang_id = uint32_t(language) - 1; if (lang_id >= kNumLanguagesV2) { assert_always(); // no room for this lang, store in english slot.. lang_id = uint32_t(XLanguage::kEnglish) - 1; } char16_t* str = 0; if (lang_id >= 0 && lang_id < kNumLanguagesV1) { str = description_raw.chars[lang_id]; } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { str = description_ex_raw.chars[lang_id - kNumLanguagesV1]; } if (!str) { // Invalid language ID? assert_always(); return false; } string_util::copy_and_swap_truncating(str, value, countof(description_raw.chars[0])); return true; } void set_publisher(const std::u16string_view value) { string_util::copy_and_swap_truncating(publisher_raw.chars, value, countof(publisher_raw.chars)); } void set_title_name(const std::u16string_view value) { string_util::copy_and_swap_truncating(title_name_raw.chars, value, countof(title_name_raw.chars)); } }; static_assert_size(XContentMetadata, 0x93D6); struct XContentHeader { be magic; uint8_t signature[0x228]; XContentLicense licenses[0x10]; uint8_t content_id[0x14]; be header_size; bool is_magic_valid() const { return magic == XContentPackageType::kCon || magic == XContentPackageType::kLive || magic == XContentPackageType::kPirs; } }; static_assert_size(XContentHeader, 0x344); #pragma pack(pop) 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); } // namespace vfs } // namespace xe #endif // XENIA_VFS_DEVICES_STFS_XBOX_H_