using System; using System.Collections.Generic; using System.IO; using BurnOutSharp.Models.MoPaQ; using BurnOutSharp.Utilities; namespace BurnOutSharp.Builders { public class MoPaQ { #region Constants #region Encryption private const uint MPQ_HASH_KEY2_MIX = 0x400; /// /// Obtained by HashString("(block table)", MPQ_HASH_FILE_KEY) /// private const uint MPQ_KEY_BLOCK_TABLE = 0xEC83B3A3; /// /// Obtained by HashString("(hash table)", MPQ_HASH_FILE_KEY) /// private const uint MPQ_KEY_HASH_TABLE = 0xC3AF3770; private const uint STORM_BUFFER_SIZE = 0x500; #endregion #region Patch Header /// /// Signature as an unsigned Int32 value /// public const uint PatchSignatureValue = 0x48435450; /// /// Signature as an unsigned Int32 value /// public const uint Md5SignatureValue = 0x5F35444D; /// /// Signature as an unsigned Int32 value /// public const uint XFRMSignatureValue = 0x4D524658; /// /// Signature as an unsigned Int64 value /// public const ulong BSDIFF40SignatureValue = 0x3034464649445342; #endregion #endregion #region Byte Data /// /// Parse a byte array into a MoPaQ archive /// /// Byte array to parse /// Offset into the byte array /// Filled archive on success, null on error public static Archive ParseArchive(byte[] data, int offset) { // If the data is invalid if (data == null) return null; // If the offset is out of bounds if (offset < 0 || offset >= data.Length) return null; // Create a memory stream and parse that MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); return ParseArchive(dataStream); } #endregion #region Stream Data /// /// Parse a Stream into a MoPaQ archive /// /// Stream to parse /// Filled archive on success, null on error public static Archive ParseArchive(Stream data) { // If the data is invalid if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) return null; // If the offset is out of bounds if (data.Position < 0 || data.Position >= data.Length) return null; // Cache the current offset int initialOffset = (int)data.Position; // Create a new archive to fill var archive = new Archive(); #region User Data // Check for User Data uint possibleSignature = data.ReadUInt32(); data.Seek(-4, SeekOrigin.Current); if (possibleSignature == 0x1B51504D) { // Save the current position for offset correction long basePtr = data.Position; // Deserialize the user data, returning null if invalid var userData = ParseUserData(data); if (userData == null) return null; // Set the user data archive.UserData = userData; // Set the starting position according to the header offset data.Seek(basePtr + (int)archive.UserData.HeaderOffset, SeekOrigin.Begin); } #endregion #region Archive Header // Check for the Header possibleSignature = data.ReadUInt32(); data.Seek(-4, SeekOrigin.Current); if (possibleSignature == 0x1A51504D) { // Try to parse the archive header var archiveHeader = ParseArchiveHeader(data); if (archiveHeader == null) return null; // Set the archive header archive.ArchiveHeader = archiveHeader; } else { return null; } #endregion #region Hash Table // TODO: The hash table has to be be decrypted before reading // Version 1 if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1) { // If we have a hash table long hashTableOffset = archive.ArchiveHeader.HashTablePosition; if (hashTableOffset != 0) { // Seek to the offset data.Seek(hashTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize; // Read in the hash table var hashTable = new List(); while (data.Position < hashTableEnd) { var hashEntry = ParseHashEntry(data); if (hashEntry == null) return null; hashTable.Add(hashEntry); } archive.HashTable = hashTable.ToArray(); } } // Version 2 and 3 else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3) { // If we have a hash table long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition; if (hashTableOffset != 0) { // Seek to the offset data.Seek(hashTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize; // Read in the hash table var hashTable = new List(); while (data.Position < hashTableEnd) { var hashEntry = ParseHashEntry(data); if (hashEntry == null) return null; hashTable.Add(hashEntry); } archive.HashTable = hashTable.ToArray(); } } // Version 4 else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4) { // If we have a hash table long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition; if (hashTableOffset != 0) { // Seek to the offset data.Seek(hashTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long hashTableEnd = hashTableOffset + (long)archive.ArchiveHeader.HashTableSizeLong; // Read in the hash table var hashTable = new List(); while (data.Position < hashTableEnd) { var hashEntry = ParseHashEntry(data); if (hashEntry == null) return null; hashTable.Add(hashEntry); } archive.HashTable = hashTable.ToArray(); } } #endregion #region Block Table // Version 1 if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1) { // If we have a block table long blockTableOffset = archive.ArchiveHeader.BlockTablePosition; if (blockTableOffset != 0) { // Seek to the offset data.Seek(blockTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize; // Read in the block table var blockTable = new List(); while (data.Position < blockTableEnd) { var blockEntry = ParseBlockEntry(data); if (blockEntry == null) return null; blockTable.Add(blockEntry); } archive.BlockTable = blockTable.ToArray(); } } // Version 2 and 3 else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3) { // If we have a block table long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition; if (blockTableOffset != 0) { // Seek to the offset data.Seek(blockTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize; // Read in the block table var blockTable = new List(); while (data.Position < blockTableEnd) { var blockEntry = ParseBlockEntry(data); if (blockEntry == null) return null; blockTable.Add(blockEntry); } archive.BlockTable = blockTable.ToArray(); } } // Version 4 else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4) { // If we have a block table long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition; if (blockTableOffset != 0) { // Seek to the offset data.Seek(blockTableOffset, SeekOrigin.Begin); // Find the ending offset based on size long blockTableEnd = blockTableOffset + (long)archive.ArchiveHeader.BlockTableSizeLong; // Read in the block table var blockTable = new List(); while (data.Position < blockTableEnd) { var blockEntry = ParseBlockEntry(data); if (blockEntry == null) return null; blockTable.Add(blockEntry); } archive.BlockTable = blockTable.ToArray(); } } #endregion #region Hi-Block Table // Version 2, 3, and 4 if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format2) { // If we have a hi-block table long hiBlockTableOffset = (long)archive.ArchiveHeader.HiBlockTablePosition; if (hiBlockTableOffset != 0) { // Seek to the offset data.Seek(hiBlockTableOffset, SeekOrigin.Begin); // Read in the hi-block table var hiBlockTable = new List(); for (int i = 0; i < archive.BlockTable.Length; i++) { short hiBlockEntry = data.ReadInt16(); hiBlockTable.Add(hiBlockEntry); } archive.HiBlockTable = hiBlockTable.ToArray(); } } #endregion #region BET Table // Version 3 and 4 if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3) { // If we have a BET table long betTableOffset = (long)archive.ArchiveHeader.BetTablePosition; if (betTableOffset != 0) { // Seek to the offset data.Seek(betTableOffset, SeekOrigin.Begin); // Read in the BET table var betTable = ParseBetTable(data); if (betTable != null) return null; archive.BetTable = betTable; } } #endregion #region HET Table // Version 3 and 4 if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3) { // If we have a HET table long hetTableOffset = (long)archive.ArchiveHeader.HetTablePosition; if (hetTableOffset != 0) { // Seek to the offset data.Seek(hetTableOffset, SeekOrigin.Begin); // Read in the HET table var hetTable = ParseHetTable(data); if (hetTable != null) return null; archive.HetTable = hetTable; } } #endregion return archive; } /// /// Parse a Stream into a archive header /// /// Stream to parse /// Filled archive header on success, null on error private static ArchiveHeader ParseArchiveHeader(Stream data) { ArchiveHeader archiveHeader = new ArchiveHeader(); // V1 - Common archiveHeader.Signature = data.ReadUInt32(); if (archiveHeader.Signature != 0x1A51504D) return null; archiveHeader.HeaderSize = data.ReadUInt32(); archiveHeader.ArchiveSize = data.ReadUInt32(); archiveHeader.FormatVersion = (FormatVersion)data.ReadUInt16(); archiveHeader.BlockSize = data.ReadUInt16(); archiveHeader.HashTablePosition = data.ReadUInt32(); archiveHeader.BlockTablePosition = data.ReadUInt32(); archiveHeader.HashTableSize = data.ReadUInt32(); archiveHeader.BlockTableSize = data.ReadUInt32(); // V2 if (archiveHeader.FormatVersion >= FormatVersion.Format2) { archiveHeader.HiBlockTablePosition = data.ReadUInt64(); archiveHeader.HashTablePositionHi = data.ReadUInt16(); archiveHeader.BlockTablePositionHi = data.ReadUInt16(); } // V3 if (archiveHeader.FormatVersion >= FormatVersion.Format3) { archiveHeader.ArchiveSizeLong = data.ReadUInt64(); archiveHeader.BetTablePosition = data.ReadUInt64(); archiveHeader.HetTablePosition = data.ReadUInt64(); } // V4 if (archiveHeader.FormatVersion >= FormatVersion.Format4) { archiveHeader.HashTableSizeLong = data.ReadUInt64(); archiveHeader.BlockTableSizeLong = data.ReadUInt64(); archiveHeader.HiBlockTableSize = data.ReadUInt64(); archiveHeader.HetTableSize = data.ReadUInt64(); archiveHeader.BetTablesize = data.ReadUInt64(); archiveHeader.RawChunkSize = data.ReadUInt32(); archiveHeader.BlockTableMD5 = data.ReadBytes(0x10); archiveHeader.HashTableMD5 = data.ReadBytes(0x10); archiveHeader.HiBlockTableMD5 = data.ReadBytes(0x10); archiveHeader.BetTableMD5 = data.ReadBytes(0x10); archiveHeader.HetTableMD5 = data.ReadBytes(0x10); archiveHeader.HetTableMD5 = data.ReadBytes(0x10); } return archiveHeader; } /// /// Parse a Stream into a user data object /// /// Stream to parse /// Filled user data on success, null on error private static UserData ParseUserData(Stream data) { UserData userData = new UserData(); userData.Signature = data.ReadUInt32(); if (userData.Signature != 0x1B51504D) return null; userData.UserDataSize = data.ReadUInt32(); userData.HeaderOffset = data.ReadUInt32(); userData.UserDataHeaderSize = data.ReadUInt32(); return userData; } /// /// Parse a Stream into a HET table /// /// Stream to parse /// Filled HET table on success, null on error private static HetTable ParseHetTable(Stream data) { HetTable hetTable = new HetTable(); // Common Headers hetTable.Signature = data.ReadUInt32(); if (hetTable.Signature != 0x1A544548) return null; hetTable.Version = data.ReadUInt32(); hetTable.DataSize = data.ReadUInt32(); // HET-Specific hetTable.TableSize = data.ReadUInt32(); hetTable.MaxFileCount = data.ReadUInt32(); hetTable.HashTableSize = data.ReadUInt32(); hetTable.TotalIndexSize = data.ReadUInt32(); hetTable.IndexSizeExtra = data.ReadUInt32(); hetTable.IndexSize = data.ReadUInt32(); hetTable.BlockTableSize = data.ReadUInt32(); hetTable.HashTable = data.ReadBytes((int)hetTable.HashTableSize); // TODO: Populate the file indexes array hetTable.FileIndexes = new byte[(int)hetTable.HashTableSize][]; return hetTable; } /// /// Parse a Stream into a BET table /// /// Stream to parse /// Filled BET table on success, null on error private static BetTable ParseBetTable(Stream data) { BetTable betTable = new BetTable(); // Common Headers betTable.Signature = data.ReadUInt32(); if (betTable.Signature != 0x1A544542) return null; betTable.Version = data.ReadUInt32(); betTable.DataSize = data.ReadUInt32(); // BET-Specific betTable.TableSize = data.ReadUInt32(); betTable.FileCount = data.ReadUInt32(); betTable.Unknown = data.ReadUInt32(); betTable.TableEntrySize = data.ReadUInt32(); betTable.FilePositionBitIndex = data.ReadUInt32(); betTable.FileSizeBitIndex = data.ReadUInt32(); betTable.CompressedSizeBitIndex = data.ReadUInt32(); betTable.FlagIndexBitIndex = data.ReadUInt32(); betTable.UnknownBitIndex = data.ReadUInt32(); betTable.FilePositionBitCount = data.ReadUInt32(); betTable.FileSizeBitCount = data.ReadUInt32(); betTable.CompressedSizeBitCount = data.ReadUInt32(); betTable.FlagIndexBitCount = data.ReadUInt32(); betTable.UnknownBitCount = data.ReadUInt32(); betTable.TotalBetHashSize = data.ReadUInt32(); betTable.BetHashSizeExtra = data.ReadUInt32(); betTable.BetHashSize = data.ReadUInt32(); betTable.BetHashArraySize = data.ReadUInt32(); betTable.FlagCount = data.ReadUInt32(); betTable.FlagsArray = new uint[betTable.FlagCount]; byte[] flagsArray = data.ReadBytes((int)betTable.FlagCount * 4); Buffer.BlockCopy(flagsArray, 0, betTable.FlagsArray, 0, (int)betTable.FlagCount * 4); // TODO: Populate the file table // TODO: Populate the hash table return betTable; } /// /// Parse a Stream into a hash entry /// /// Stream to parse /// Filled hash entry on success, null on error private static HashEntry ParseHashEntry(Stream data) { // TODO: Use marshalling here instead of building HashEntry hashEntry = new HashEntry(); hashEntry.NameHashPartA = data.ReadUInt32(); hashEntry.NameHashPartB = data.ReadUInt32(); hashEntry.Locale = (Locale)data.ReadUInt16(); hashEntry.Platform = data.ReadUInt16(); hashEntry.BlockIndex = data.ReadUInt32(); return hashEntry; } /// /// Parse a Stream into a block entry /// /// Stream to parse /// Filled block entry on success, null on error private static BlockEntry ParseBlockEntry(Stream data) { BlockEntry blockEntry = new BlockEntry(); blockEntry.FilePosition = data.ReadUInt32(); blockEntry.CompressedSize = data.ReadUInt32(); blockEntry.UncompressedSize = data.ReadUInt32(); blockEntry.Flags = (FileFlags)data.ReadUInt32(); return blockEntry; } /// /// Parse a Stream into a patch info /// /// Stream to parse /// Filled patch info on success, null on error private static PatchInfo ParsePatchInfo(Stream data) { // TODO: Use marshalling here instead of building PatchInfo patchInfo = new PatchInfo(); patchInfo.Length = data.ReadUInt32(); patchInfo.Flags = data.ReadUInt32(); patchInfo.DataSize = data.ReadUInt32(); patchInfo.MD5 = data.ReadBytes(0x10); // TODO: Fill the sector offset table return patchInfo; } #endregion #region Helpers /// /// Buffer for encryption and decryption /// private uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE]; /// /// Prepare the encryption table /// private void PrepareCryptTable() { uint seed = 0x00100001; for (uint index1 = 0; index1 < 0x100; index1++) { for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100) { seed = (seed * 125 + 3) % 0x2AAAAB; uint temp1 = (seed & 0xFFFF) << 0x10; seed = (seed * 125 + 3) % 0x2AAAAB; uint temp2 = (seed & 0xFFFF); _stormBuffer[index2] = (temp1 | temp2); } } } /// /// Decrypt a single block of data /// private unsafe byte[] DecryptBlock(byte[] block, uint length, uint key) { uint seed = 0xEEEEEEEE; uint[] castBlock = new uint[length / 4]; Buffer.BlockCopy(block, 0, castBlock, 0, (int)length); int castBlockPtr = 0; // Round to uints length >>= 2; while (length-- > 0) { seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)]; uint ch = castBlock[castBlockPtr] ^ (key + seed); key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B); seed = ch + seed + (seed << 5) + 3; castBlock[castBlockPtr++] = ch; } Buffer.BlockCopy(castBlock, 0, block, 0, (int)length); return block; } #endregion } }