using System; using System.IO; using System.Text; using SabreTools.Data.Models.MoPaQ; using SabreTools.IO.Extensions; using SabreTools.Numerics.Extensions; using SabreTools.Security.Cryptography; using static SabreTools.Data.Models.MoPaQ.Constants; #pragma warning disable IDE0017 // Simplify object initialization namespace SabreTools.Serialization.Readers { public class MoPaQ : BaseBinaryReader { /// public override Archive? Deserialize(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long initialOffset = data.Position; // Prepare the crypt table var decrypter = new MoPaQDecrypter(); // Create a new archive to fill var archive = new Archive(); #region User Data // Check for User Data uint possibleSignature = data.PeekUInt32LittleEndian(); if (possibleSignature == UserDataSignatureUInt32) { // Deserialize the user data, returning null if invalid var userData = ParseUserData(data); if (userData?.Signature != UserDataSignatureString) return null; // Set the user data archive.UserData = userData; // Set the starting position according to the header offset data.SeekIfPossible(initialOffset + archive.UserData.HeaderOffset, SeekOrigin.Begin); } #endregion #region Archive Header // Try to parse the archive header var archiveHeader = ParseArchiveHeader(data); if (archiveHeader is null) return null; // Set the archive header archive.ArchiveHeader = archiveHeader; #endregion #region Hash Table // Set the hash table, if possible var hashTable = ParseHashTable(data, initialOffset, archive.ArchiveHeader, decrypter); if (hashTable is not null) archive.HashTable = hashTable; #endregion #region Block Table // Set the block table, if possible var blockTable = ParseBlockTable(data, initialOffset, archive.ArchiveHeader, decrypter); if (blockTable is not null) archive.BlockTable = blockTable; #endregion #region Hi-Block Table // Set the hi-block table, if possible var hiBlockTable = ParseHiBlockTable(data, initialOffset, archive.ArchiveHeader); if (hiBlockTable is not null) archive.HiBlockTable = hiBlockTable; #endregion #region BET Table // Set the BET table, if possible var betTable = ParseBetTable(data, initialOffset, archive.ArchiveHeader, decrypter); if (betTable is not null) archive.BetTable = betTable; #endregion #region HET Table // Set the HET table, if possible var hetTable = ParseHetTable(data, initialOffset, archive.ArchiveHeader, decrypter); if (hetTable is not null) archive.HetTable = hetTable; #endregion return archive; } catch { // Ignore the actual error return null; } } /// /// Parse a Stream into an ArchiveHeader /// /// Stream to parse /// Filled ArchiveHeader on success, null on error public static ArchiveHeader? ParseArchiveHeader(Stream data) { var obj = new ArchiveHeader(); // V1 - Common byte[] signature = data.ReadBytes(4); obj.Signature = Encoding.ASCII.GetString(signature); if (obj.Signature != ArchiveHeaderSignatureString) return null; obj.HeaderSize = data.ReadUInt32LittleEndian(); obj.ArchiveSize = data.ReadUInt32LittleEndian(); obj.FormatVersion = (FormatVersion)data.ReadUInt16LittleEndian(); obj.BlockSize = data.ReadUInt16LittleEndian(); obj.HashTablePosition = data.ReadUInt32LittleEndian(); obj.BlockTablePosition = data.ReadUInt32LittleEndian(); obj.HashTableSize = data.ReadUInt32LittleEndian(); obj.BlockTableSize = data.ReadUInt32LittleEndian(); // V2 if (obj.FormatVersion >= FormatVersion.Format2) { obj.HiBlockTablePosition = data.ReadUInt64LittleEndian(); obj.HashTablePositionHi = data.ReadUInt16LittleEndian(); obj.BlockTablePositionHi = data.ReadUInt16LittleEndian(); } // V3 if (obj.FormatVersion >= FormatVersion.Format3) { obj.ArchiveSizeLong = data.ReadUInt64LittleEndian(); obj.BetTablePosition = data.ReadUInt64LittleEndian(); obj.HetTablePosition = data.ReadUInt64LittleEndian(); } // V4 if (obj.FormatVersion >= FormatVersion.Format4) { obj.HashTableSizeLong = data.ReadUInt64LittleEndian(); obj.BlockTableSizeLong = data.ReadUInt64LittleEndian(); obj.HiBlockTableSize = data.ReadUInt64LittleEndian(); obj.HetTableSize = data.ReadUInt64LittleEndian(); obj.BetTableSize = data.ReadUInt64LittleEndian(); obj.RawChunkSize = data.ReadUInt32LittleEndian(); obj.BlockTableMD5 = data.ReadBytes(0x10); obj.HashTableMD5 = data.ReadBytes(0x10); obj.HiBlockTableMD5 = data.ReadBytes(0x10); obj.BetTableMD5 = data.ReadBytes(0x10); obj.HetTableMD5 = data.ReadBytes(0x10); obj.HetTableMD5 = data.ReadBytes(0x10); } return obj; } /// /// Parse a Stream into a BetTable /// /// Stream to parse /// Initial offset to use in address comparisons /// Archive header to get information from /// Decrypter for processing table data /// Filled BetTable on success, null on error public static BetTable? ParseBetTable(Stream data, long initialOffset, ArchiveHeader header, MoPaQDecrypter decrypter) { // Get the BET table offset long offset = initialOffset + (long)header.BetTablePosition; if (offset <= initialOffset || offset >= data.Length) return null; // Preprocess the table byte[]? tableBytes = decrypter.LoadTable(data, offset, header.BetTableMD5, (uint)header.BetTableSize, 92, MPQ_KEY_BLOCK_TABLE, out _); if (tableBytes is null) return null; // Read in the BET table int tableOffset = 0; return ParseBetTable(tableBytes, ref tableOffset); } /// /// Parse a Stream into a BlockTable /// /// Stream to parse /// Initial offset to use in address comparisons /// Archive header to get information from /// Decrypter for processing table data /// Filled BlockTable on success, null on error public static BlockEntry[]? ParseBlockTable(Stream data, long initialOffset, ArchiveHeader header, MoPaQDecrypter decrypter) { // Get the block table offset long offset = initialOffset + (((uint)header.BlockTablePositionHi << 23) | header.BlockTablePosition); if (offset <= initialOffset || offset >= data.Length) return null; // Get the entry count uint entryCount = header.BlockTableSize; // Preprocess the table byte[]? tableBytes = decrypter.LoadTable(data, offset, header.BlockTableMD5, (uint)header.BlockTableSizeLong, entryCount * 16, MPQ_KEY_BLOCK_TABLE, out _); if (tableBytes is null) return null; // Read in the block table int tableOffset = 0; var blockTable = new BlockEntry[entryCount]; for (int i = 0; i < blockTable.Length; i++) { var blockEntry = ParseBlockEntry(tableBytes, ref tableOffset); blockTable[i] = blockEntry; } return blockTable; } /// /// Parse a Stream into a HashTable /// /// Stream to parse /// Initial offset to use in address comparisons /// Archive header to get information from /// Decrypter for processing table data /// Filled HashTable on success, null on error public static HashEntry[]? ParseHashTable(Stream data, long initialOffset, ArchiveHeader header, MoPaQDecrypter decrypter) { // Get the hash table offset long offset = initialOffset + (((uint)header.HashTablePositionHi << 23) | header.HashTablePosition); if (offset <= initialOffset || offset >= data.Length) return null; // Get the entry count uint entryCount = header.HashTableSize; // Preprocess the table byte[]? tableBytes = decrypter.LoadTable(data, offset, header.HashTableMD5, (uint)header.HashTableSizeLong, entryCount * 16, MPQ_KEY_HASH_TABLE, out _); if (tableBytes is null) return null; // Read in the hash table int tableOffset = 0; var hashTable = new HashEntry[entryCount]; for (int i = 0; i < hashTable.Length; i++) { var hashEntry = ParseHashEntry(tableBytes, ref tableOffset); hashTable[i] = hashEntry; } return hashTable; } /// /// Parse a Stream into a HetTable /// /// Stream to parse /// Initial offset to use in address comparisons /// Archive header to get information from /// Decrypter for processing table data /// Filled HetTable on success, null on error public static HetTable? ParseHetTable(Stream data, long initialOffset, ArchiveHeader header, MoPaQDecrypter decrypter) { // Get the HET table offset long offset = initialOffset + (long)header.HetTablePosition; if (offset <= initialOffset || offset >= data.Length) return null; // Preprocess the table byte[]? tableBytes = decrypter.LoadTable(data, offset, header.HetTableMD5, (uint)header.HetTableSize, 44, MPQ_KEY_HASH_TABLE, out _); if (tableBytes is null) return null; // Read in the HET table int tableOffset = 0; return ParseHetTable(tableBytes, ref tableOffset); } /// /// Parse a Stream into a HiBlockTable /// /// Stream to parse /// Initial offset to use in address comparisons /// Archive header to get information from /// Filled HiBlockTable on success, null on error /// TODO: Add MD5 validation of contents /// TODO: The table has to be be decompressed before reading for V4(?) public static short[]? ParseHiBlockTable(Stream data, long initialOffset, ArchiveHeader header) { // Get the hi-block table offset long offset = initialOffset + (long)header.HiBlockTablePosition; if (offset <= initialOffset || offset >= data.Length) return null; // Get the entry count ulong entryCount = header.HiBlockTableSize >> 1; // Seek to the offset data.SeekIfPossible(offset, SeekOrigin.Begin); // Read in the hi-block table var hiBlockTable = new short[entryCount]; for (int i = 0; i < hiBlockTable.Length; i++) { var hiBlockEntry = data.ReadInt16LittleEndian(); hiBlockTable[i] = hiBlockEntry; } return hiBlockTable; } /// /// Parse a Stream into a UserData /// /// Stream to parse /// Filled UserData on success, null on error public static UserData ParseUserData(Stream data) { var obj = new UserData(); byte[] signature = data.ReadBytes(4); obj.Signature = Encoding.ASCII.GetString(signature); obj.UserDataSize = data.ReadUInt32LittleEndian(); obj.HeaderOffset = data.ReadUInt32LittleEndian(); obj.UserDataHeaderSize = data.ReadUInt32LittleEndian(); return obj; } /// /// Parse a Stream into a BetTable /// /// Stream to parse /// Filled BetTable on success, null on error private static BetTable ParseBetTable(byte[] data, ref int offset) { var obj = new BetTable(); // Common Headers byte[] signature = data.ReadBytes(ref offset, 4); obj.Signature = Encoding.ASCII.GetString(signature); obj.Version = data.ReadUInt32LittleEndian(ref offset); obj.DataSize = data.ReadUInt32LittleEndian(ref offset); // BET-Specific obj.TableSize = data.ReadUInt32LittleEndian(ref offset); obj.FileCount = data.ReadUInt32LittleEndian(ref offset); obj.Unknown = data.ReadUInt32LittleEndian(ref offset); obj.TableEntrySize = data.ReadUInt32LittleEndian(ref offset); obj.FilePositionBitIndex = data.ReadUInt32LittleEndian(ref offset); obj.FileSizeBitIndex = data.ReadUInt32LittleEndian(ref offset); obj.CompressedSizeBitIndex = data.ReadUInt32LittleEndian(ref offset); obj.FlagIndexBitIndex = data.ReadUInt32LittleEndian(ref offset); obj.UnknownBitIndex = data.ReadUInt32LittleEndian(ref offset); obj.FilePositionBitCount = data.ReadUInt32LittleEndian(ref offset); obj.FileSizeBitCount = data.ReadUInt32LittleEndian(ref offset); obj.CompressedSizeBitCount = data.ReadUInt32LittleEndian(ref offset); obj.FlagIndexBitCount = data.ReadUInt32LittleEndian(ref offset); obj.UnknownBitCount = data.ReadUInt32LittleEndian(ref offset); obj.TotalBetHashSize = data.ReadUInt32LittleEndian(ref offset); obj.BetHashSizeExtra = data.ReadUInt32LittleEndian(ref offset); obj.BetHashSize = data.ReadUInt32LittleEndian(ref offset); obj.BetHashArraySize = data.ReadUInt32LittleEndian(ref offset); obj.FlagCount = data.ReadUInt32LittleEndian(ref offset); obj.FlagsArray = new uint[obj.FlagCount]; byte[] flagsArray = data.ReadBytes(ref offset, (int)obj.FlagCount * 4); Buffer.BlockCopy(flagsArray, 0, obj.FlagsArray, 0, (int)obj.FlagCount * 4); // TODO: Populate the file table // TODO: Populate the hash table return obj; } /// /// Parse decrypted data into an BlockEntry /// /// Decrypted data to parse /// Filled BlockEntry on success, null on error private static BlockEntry ParseBlockEntry(byte[] data, ref int offset) { var obj = new BlockEntry(); obj.FilePosition = data.ReadUInt32LittleEndian(ref offset); obj.CompressedSize = data.ReadUInt32LittleEndian(ref offset); obj.UncompressedSize = data.ReadUInt32LittleEndian(ref offset); obj.Flags = (FileFlags)data.ReadUInt32LittleEndian(ref offset); return obj; } /// /// Parse decrypted data into an HashEntry /// /// Decrypted data to parse /// Filled HashEntry on success, null on error private static HashEntry ParseHashEntry(byte[] data, ref int offset) { var obj = new HashEntry(); obj.NameHashPartA = data.ReadUInt32LittleEndian(ref offset); obj.NameHashPartB = data.ReadUInt32LittleEndian(ref offset); obj.Locale = (Locale)data.ReadInt16LittleEndian(ref offset); obj.Platform = data.ReadUInt16LittleEndian(ref offset); obj.BlockIndex = data.ReadUInt32LittleEndian(ref offset); return obj; } /// /// Parse a decrypted data into a HetTable /// /// Decrypted data to parse /// Filled HetTable on success, null on error private static HetTable ParseHetTable(byte[] data, ref int offset) { var obj = new HetTable(); // Common Headers byte[] signature = data.ReadBytes(ref offset, 4); obj.Signature = Encoding.ASCII.GetString(signature); obj.Version = data.ReadUInt32LittleEndian(ref offset); obj.DataSize = data.ReadUInt32LittleEndian(ref offset); // HET-Specific obj.TableSize = data.ReadUInt32LittleEndian(ref offset); obj.MaxFileCount = data.ReadUInt32LittleEndian(ref offset); obj.HashTableSize = data.ReadUInt32LittleEndian(ref offset); obj.TotalIndexSize = data.ReadUInt32LittleEndian(ref offset); obj.IndexSizeExtra = data.ReadUInt32LittleEndian(ref offset); obj.IndexSize = data.ReadUInt32LittleEndian(ref offset); obj.BlockTableSize = data.ReadUInt32LittleEndian(ref offset); obj.HashTable = data.ReadBytes(ref offset, (int)obj.HashTableSize); // TODO: Populate the file indexes array obj.FileIndexes = new byte[(int)obj.HashTableSize][]; return obj; } } }