// Referenced from: https://github.com/xenia-canary/xenia-canary/blob/canary_experimental/src/xenia/vfs/devices/xcontent_container_device.cc /** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2023 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xcontent_file_system.h" #include #include #include enum class XContentPackageType { CON = 0x434F4E20, PIRS = 0x50495253, LIVE = 0x4C495645, }; struct XContentLicense { be licenseId; be licenseBits; be licenseFlags; }; #pragma pack(push, 1) struct XContentHeader { be magic; uint8_t signature[0x228]; XContentLicense licenses[0x10]; uint8_t contentId[0x14]; be headerSize; }; static_assert(sizeof(XContentHeader) == 0x344); struct StfsVolumeDescriptor { uint8_t descriptorLength; uint8_t version; union { uint8_t asByte; struct { uint8_t readOnlyFormat : 1; uint8_t rootActiveIndex : 1; uint8_t directoryOverallocated : 1; uint8_t directoryIndexBoundsValid : 1; } bits; } flags; uint16_t fileTableBlockCount; uint8_t fileTableBlockNumberRaw[3]; uint8_t topHashTableHash[0x14]; be totalBlockCount; be freeBlockCount; }; static_assert(sizeof(StfsVolumeDescriptor) == 0x24); struct StfsDirectoryEntry { char name[40]; struct { uint8_t nameLength : 6; uint8_t contiguous : 1; uint8_t directory : 1; } flags; uint8_t validDataBlocksRaw[3]; uint8_t allocatedDataBlocksRaw[3]; uint8_t startBlockNumberRaw[3]; be directoryIndex; be length; be createDate; be createTime; be modifiedDate; be modifiedTime; }; static_assert(sizeof(StfsDirectoryEntry) == 0x40); struct StfsDirectoryBlock { StfsDirectoryEntry entries[0x40]; }; static_assert(sizeof(StfsDirectoryBlock) == 0x1000); struct StfsHashEntry { uint8_t sha1[0x14]; be infoRaw; }; static_assert(sizeof(StfsHashEntry) == 0x18); struct StfsHashTable { StfsHashEntry entries[170]; be numBlocks; uint8_t padding[12]; }; static_assert(sizeof(StfsHashTable) == 0x1000); struct SvodDeviceDescriptor { uint8_t descriptorLength; uint8_t blockCacheElementCount; uint8_t workerThreadProcessor; uint8_t workerThreadPriority; uint8_t firstFragmentHashEntry[0x14]; union { uint8_t asByte; struct { uint8_t mustBeZeroForFutureUsage : 6; uint8_t enhancedGdfLayout : 1; uint8_t zeroForDownlevelClients : 1; } bits; } features; uint8_t numDataBlocksRaw[3]; uint8_t startDataBlockRaw[3]; uint8_t reserved[5]; }; static_assert(sizeof(SvodDeviceDescriptor) == 0x24); struct SvodDirectoryEntry { uint16_t nodeL; uint16_t nodeR; uint32_t dataBlock; uint32_t length; uint8_t attributes; uint8_t nameLength; }; static_assert(sizeof(SvodDirectoryEntry) == 0xE); struct XContentMetadata { be contentType; be metadataVersion; be contentSize; uint8_t executionInfo[24]; uint8_t consoleId[5]; be profileId; union { StfsVolumeDescriptor stfsVolumeDescriptor; SvodDeviceDescriptor svodDeviceDescriptor; }; be dataFileCount; be dataFileSize; be volumeType; be onlineCreator; be category; }; static_assert(sizeof(XContentMetadata) == 0x75); #pragma pack(pop) struct XContentContainerHeader { XContentHeader contentHeader; XContentMetadata contentMetadata; }; const uint32_t StfsBlockSize = 0x1000; const uint32_t StfsBlocksHashLevelAmount = 3; const uint32_t StfsBlocksPerHashLevel[StfsBlocksHashLevelAmount] = { 170, 28900, 4913000 }; const uint32_t StfsEndOfChain = 0xFFFFFF; const uint32_t StfsEntriesPerDirectoryBlock = StfsBlockSize / sizeof(StfsDirectoryEntry); uint32_t parseUint24(const uint8_t *bytes) { return bytes[0] | (bytes[1] << 8U) | (bytes[2] << 16U); } size_t blockIndexToOffset(uint64_t baseOffset, uint64_t blockIndex) { uint64_t block = blockIndex; for (uint32_t i = 0; i < StfsBlocksHashLevelAmount; i++) { uint32_t levelBase = StfsBlocksPerHashLevel[i]; block += ((blockIndex + levelBase) / levelBase); if (blockIndex < levelBase) { break; } } return baseOffset + (block << 12); } uint32_t blockIndexToHashBlockNumber(uint32_t blockIndex) { if (blockIndex < StfsBlocksPerHashLevel[0]) { return 0; } uint32_t block = (blockIndex / StfsBlocksPerHashLevel[0]) * (StfsBlocksPerHashLevel[0] + 1); block += ((blockIndex / StfsBlocksPerHashLevel[1]) + 1); if (blockIndex < StfsBlocksPerHashLevel[1]) { return block; } return block + 1; } size_t blockIndexToHashBlockOffset(uint64_t baseOffset, uint32_t blockIndex) { size_t blockNumber = blockIndexToHashBlockNumber(blockIndex); return baseOffset + (blockNumber << 12); } const StfsHashEntry *hashEntryFromBlockIndex(const uint8_t *fileData, uint64_t baseOffset, uint64_t blockIndex) { size_t hashOffset = blockIndexToHashBlockOffset(baseOffset, blockIndex); const StfsHashTable *hashTable = (const StfsHashTable *)(&fileData[hashOffset]); return &hashTable->entries[blockIndex % StfsBlocksPerHashLevel[0]]; } void blockToOffsetAndFile(SvodLayoutType svodLayoutType, size_t svodStartDataBlock, size_t svodBaseOffset, size_t block, size_t &outOffset, size_t &outFileIndex) { const size_t BlockSize = 0x800; const size_t HashBlockSize = 0x1000; const size_t BlocksPerL0Hash = 0x198; const size_t HashesPerL1Hash = 0xA1C4; const size_t BlocksPerFile = 0x14388; const size_t MaxFileSize = 0xA290000; size_t trueBlock = block - (svodStartDataBlock * 2); if (svodLayoutType == SvodLayoutType::EnhancedGDF) { trueBlock += 0x2; } size_t fileBlock = trueBlock % BlocksPerFile; outFileIndex = trueBlock / BlocksPerFile; size_t offset = 0; size_t level0TableCount = (fileBlock / BlocksPerL0Hash) + 1; offset += level0TableCount * HashBlockSize; size_t level1TableCount = (level0TableCount / HashesPerL1Hash) + 1; offset += level1TableCount * HashBlockSize; if (svodLayoutType == SvodLayoutType::SingleFile) { offset += svodBaseOffset; } outOffset = (fileBlock * BlockSize) + offset; if (outOffset >= MaxFileSize) { outOffset = (outOffset % MaxFileSize) + 0x2000; outFileIndex++; } } XContentFileSystem::XContentFileSystem(const std::filesystem::path &contentPath) { mappedFiles.emplace_back(); MemoryMappedFile &rootMappedFile = mappedFiles.back(); rootMappedFile.open(contentPath); if (!rootMappedFile.isOpen()) { return; } name = (const char *)(contentPath.filename().u8string().data()); const uint8_t *rootMappedFileData = rootMappedFile.data(); if (sizeof(XContentContainerHeader) > rootMappedFile.size()) { mappedFiles.clear(); return; } XContentContainerHeader contentContainerHeader = *(const XContentContainerHeader *)(rootMappedFileData); XContentPackageType packageType = XContentPackageType(contentContainerHeader.contentHeader.magic.get()); if (packageType != XContentPackageType::CON && packageType != XContentPackageType::LIVE && packageType != XContentPackageType::PIRS) { mappedFiles.clear(); return; } const XContentMetadata &metadata = contentContainerHeader.contentMetadata; volumeType = XContentVolumeType(metadata.volumeType.get()); if (volumeType == XContentVolumeType::STFS) { const StfsVolumeDescriptor &descriptor = metadata.stfsVolumeDescriptor; if (descriptor.descriptorLength != sizeof(StfsVolumeDescriptor) || !descriptor.flags.bits.readOnlyFormat) { mappedFiles.clear(); return; } baseOffset = ((contentContainerHeader.contentHeader.headerSize + StfsBlockSize - 1) / StfsBlockSize) * StfsBlockSize; uint32_t entryCount = 0; uint32_t tableBlockIndex = parseUint24(descriptor.fileTableBlockNumberRaw); uint32_t tableBlockCount = descriptor.fileTableBlockCount; std::map directoryNames; for (uint32_t i = 0; i < tableBlockCount; i++) { size_t offset = blockIndexToOffset(baseOffset, tableBlockIndex); if (offset + sizeof(StfsDirectoryBlock) > rootMappedFile.size()) { mappedFiles.clear(); return; } StfsDirectoryBlock *directoryBlock = (StfsDirectoryBlock *)(&rootMappedFileData[offset]); for (uint32_t j = 0; j < StfsEntriesPerDirectoryBlock; j++) { const StfsDirectoryEntry &directoryEntry = directoryBlock->entries[j]; if (directoryEntry.name[0] == '\0') { break; } std::string fileNameBase = directoryNames[directoryEntry.directoryIndex]; std::string fileName(directoryEntry.name, directoryEntry.flags.nameLength & 0x3F); if (directoryEntry.flags.directory) { directoryNames[entryCount++] = fileNameBase + fileName + "/"; continue; } uint32_t fileBlockIndex = parseUint24(directoryEntry.startBlockNumberRaw); uint32_t fileBlockCount = parseUint24(directoryEntry.allocatedDataBlocksRaw); fileMap[fileNameBase + fileName] = { directoryEntry.length, fileBlockIndex, fileBlockCount }; entryCount++; } const StfsHashEntry *hashEntry = hashEntryFromBlockIndex(rootMappedFileData, baseOffset, tableBlockIndex); tableBlockIndex = hashEntry->infoRaw & 0xFFFFFF; if (tableBlockIndex == StfsEndOfChain) { break; } } } else if (volumeType == XContentVolumeType::SVOD) { mappedFiles.clear(); // Close the root file and open all the files inside the directory with the same name instead. std::filesystem::path dataDirectory(contentPath.u8string() + u8".data"); if (!std::filesystem::is_directory(dataDirectory)) { return; } // Find all data files inside the directory. std::set orderedPaths; for (auto &entry : std::filesystem::directory_iterator(dataDirectory)) { if (!entry.is_regular_file()) { continue; } orderedPaths.emplace(entry.path()); } // Memory map all the files that were found. for (auto &path : orderedPaths) { mappedFiles.emplace_back(); if (!mappedFiles.back().open(path)) { mappedFiles.clear(); return; } } if (mappedFiles.empty()) { return; } // Determine the layout of the SVOD from the first file. MemoryMappedFile &firstMappedFile = mappedFiles.front(); const uint8_t *firstMappedFileData = firstMappedFile.data(); const char *RefMagic = "MICROSOFT*XBOX*MEDIA"; size_t RefXSFMagicOffset = 0x12000; size_t SingleFileMagicOffset = 0xD000; if (metadata.svodDeviceDescriptor.features.bits.enhancedGdfLayout) { size_t EGDFMagicOffset = 0x2000; if (EGDFMagicOffset >= firstMappedFile.size() || std::memcmp(&firstMappedFileData[EGDFMagicOffset], RefMagic, strlen(RefMagic)) != 0) { mappedFiles.clear(); return; } svodBaseOffset = 0; svodMagicOffset = EGDFMagicOffset; svodLayoutType = SvodLayoutType::EnhancedGDF; } else if (RefXSFMagicOffset < firstMappedFile.size() && std::memcmp(&firstMappedFileData[RefXSFMagicOffset], RefMagic, strlen(RefMagic)) == 0) { const char *XSFMagic = "XSF"; size_t XSFMagicOffset = 0x2000; svodBaseOffset = 0x10000; svodMagicOffset = 0x12000; if (std::memcmp(&firstMappedFileData[XSFMagicOffset], XSFMagic, strlen(XSFMagic)) == 0) { svodLayoutType = SvodLayoutType::XSF; } else { svodLayoutType = SvodLayoutType::Unknown; } } else if (SingleFileMagicOffset < firstMappedFile.size() && std::memcmp(&firstMappedFileData[SingleFileMagicOffset], RefMagic, strlen(RefMagic)) == 0) { svodBaseOffset = 0xB000; svodMagicOffset = 0xD000; svodLayoutType = SvodLayoutType::SingleFile; } else { mappedFiles.clear(); return; } svodStartDataBlock = parseUint24(metadata.svodDeviceDescriptor.startDataBlockRaw); struct IterationStep { std::string fileNameBase; uint32_t blockIndex = 0; uint32_t ordinalIndex = 0; IterationStep() = default; IterationStep(std::string fileNameBase, uint32_t blockIndex, uint32_t ordinalIndex) : fileNameBase(fileNameBase), blockIndex(blockIndex), ordinalIndex(ordinalIndex) { } }; std::stack iterationStack; uint32_t rootBlock = *(uint32_t *)(&firstMappedFileData[svodMagicOffset + 0x14]); iterationStack.emplace("", rootBlock, 0); IterationStep step; size_t fileOffset, fileIndex; char fileName[256]; const uint8_t FileAttributeDirectory = 0x10; while (!iterationStack.empty()) { step = iterationStack.top(); iterationStack.pop(); size_t ordinalOffset = step.ordinalIndex * 0x4; size_t blockOffset = ordinalOffset / 0x800; size_t trueOrdinalOffset = ordinalOffset % 0x800; blockToOffsetAndFile(svodLayoutType, svodStartDataBlock, svodBaseOffset, step.blockIndex + blockOffset, fileOffset, fileIndex); fileOffset += trueOrdinalOffset; if (fileIndex >= mappedFiles.size()) { mappedFiles.clear(); return; } const MemoryMappedFile &mappedFile = mappedFiles[fileIndex]; if ((fileOffset + sizeof(SvodDirectoryEntry)) > mappedFile.size()) { mappedFiles.clear(); return; } const uint8_t *mappedFileData = mappedFile.data(); const SvodDirectoryEntry *directoryEntry = (const SvodDirectoryEntry *)(&mappedFileData[fileOffset]); size_t nameOffset = fileOffset + sizeof(SvodDirectoryEntry); if ((nameOffset + directoryEntry->nameLength) > mappedFile.size()) { mappedFiles.clear(); return; } memcpy(fileName, &mappedFileData[nameOffset], directoryEntry->nameLength); fileName[directoryEntry->nameLength] = '\0'; if (directoryEntry->nodeL) { iterationStack.emplace(step.fileNameBase, step.blockIndex, directoryEntry->nodeL); } if (directoryEntry->nodeR) { iterationStack.emplace(step.fileNameBase, step.blockIndex, directoryEntry->nodeR); } std::string fileNameUTF8 = step.fileNameBase + fileName; if (directoryEntry->attributes & FileAttributeDirectory) { if (directoryEntry->length > 0) { iterationStack.emplace(fileNameUTF8 + "/", directoryEntry->dataBlock, 0); } } else { fileMap[fileNameUTF8] = { directoryEntry->length, directoryEntry->dataBlock, 0 }; } } } else { mappedFiles.clear(); } } bool XContentFileSystem::load(const std::string &path, uint8_t *fileData, size_t fileDataMaxByteCount) const { auto it = fileMap.find(path); if (it != fileMap.end()) { if (fileDataMaxByteCount < it->second.size) { return false; } if (volumeType == XContentVolumeType::STFS) { const MemoryMappedFile &rootMappedFile = mappedFiles.back(); const uint8_t *rootMappedFileData = rootMappedFile.data(); size_t fileDataOffset = 0; size_t remainingSize = it->second.size; uint32_t fileBlockIndex = it->second.blockIndex; for (uint32_t i = 0; i < it->second.blockCount && fileBlockIndex != StfsEndOfChain; i++) { size_t blockSize = std::min(size_t(StfsBlockSize), remainingSize); size_t blockOffset = blockIndexToOffset(baseOffset, fileBlockIndex); if (blockOffset + blockSize > rootMappedFile.size()) { return false; } memcpy(&fileData[fileDataOffset], &rootMappedFileData[blockOffset], blockSize); const StfsHashEntry *hashEntry = hashEntryFromBlockIndex(rootMappedFileData, baseOffset, fileBlockIndex); fileBlockIndex = hashEntry->infoRaw & 0xFFFFFF; fileDataOffset += blockSize; remainingSize -= blockSize; } return remainingSize == 0; } else if (volumeType == XContentVolumeType::SVOD) { size_t fileDataOffset = 0; size_t remainingSize = it->second.size; size_t currentBlock = it->second.blockIndex; while (remainingSize > 0) { size_t blockFileOffset, blockFileIndex; blockToOffsetAndFile(svodLayoutType, svodStartDataBlock, svodBaseOffset, currentBlock, blockFileOffset, blockFileIndex); if (blockFileIndex >= mappedFiles.size()) { return false; } const MemoryMappedFile &mappedFile = mappedFiles[blockFileIndex]; const uint8_t *mappedFileData = mappedFile.data(); size_t blockSize = std::min(size_t(0x800), remainingSize); if (blockFileOffset + blockSize > mappedFile.size()) { return false; } memcpy(&fileData[fileDataOffset], &mappedFileData[blockFileOffset], blockSize); fileDataOffset += blockSize; remainingSize -= blockSize; currentBlock++; } return remainingSize == 0; } else { return false; } } else { return false; } } size_t XContentFileSystem::getSize(const std::string &path) const { auto it = fileMap.find(path); if (it != fileMap.end()) { return it->second.size; } else { return 0; } } bool XContentFileSystem::exists(const std::string &path) const { return fileMap.find(path) != fileMap.end(); } const std::string &XContentFileSystem::getName() const { return name; } bool XContentFileSystem::empty() const { return mappedFiles.empty(); } std::unique_ptr XContentFileSystem::create(const std::filesystem::path &contentPath) { std::unique_ptr xContentFS = std::make_unique(contentPath); if (!xContentFS->empty()) { return xContentFS; } else { return nullptr; } } bool XContentFileSystem::check(const std::filesystem::path &contentPath) { std::ifstream contentStream(contentPath, std::ios::binary); if (!contentStream.is_open()) { return false; } uint32_t packageTypeUint = 0; contentStream.read((char *)(&packageTypeUint), sizeof(uint32_t)); packageTypeUint = ByteSwap(packageTypeUint); XContentPackageType packageType = XContentPackageType(packageTypeUint); return packageType == XContentPackageType::CON || packageType == XContentPackageType::LIVE || packageType == XContentPackageType::PIRS; }