diff --git a/BurnOutSharp.Builder/MoPaQ.cs b/BurnOutSharp.Builder/MoPaQ.cs index 74dcf296..6bbc2556 100644 --- a/BurnOutSharp.Builder/MoPaQ.cs +++ b/BurnOutSharp.Builder/MoPaQ.cs @@ -10,198 +10,46 @@ namespace BurnOutSharp.Builder { #region Constants - #region User Data + #region Encryption - public const int UserDataSize = 0x10; + private const uint MPQ_HASH_KEY2_MIX = 0x400; /// - /// Human-readable signature + /// Obtained by HashString("(block table)", MPQ_HASH_FILE_KEY) /// - public static readonly string UserDataSignatureString = $"MPQ{(char)0x1B}"; + private const uint MPQ_KEY_BLOCK_TABLE = 0xEC83B3A3; /// - /// Signature as an unsigned Int32 value + /// Obtained by HashString("(hash table)", MPQ_HASH_FILE_KEY) /// - public const uint UserDataSignatureValue = 0x1B51504D; + private const uint MPQ_KEY_HASH_TABLE = 0xC3AF3770; - /// - /// Signature as a byte array - /// - public static readonly byte[] UserDataSignatureBytes = new byte[] { 0x4D, 0x50, 0x51, 0x1B }; - - #endregion - - #region Archive Header - - #region Signatures - - /// - /// Human-readable signature - /// - public static readonly string ArchiveHeaderSignatureString = $"MPQ{(char)0x1A}"; - - /// - /// Signature as an unsigned Int32 value - /// - public const uint ArchiveHeaderSignatureValue = 0x1A51504D; - - /// - /// Signature as a byte array - /// - public static readonly byte[] ArchiveHeaderSignatureBytes = new byte[] { 0x4D, 0x50, 0x51, 0x1A }; - - #endregion - - #region Header Sizes - - public const int ArchiveHeaderHeaderVersion1Size = 0x20; - - public const int ArchiveHeaderHeaderVersion2Size = 0x2C; - - public const int ArchiveHeaderHeaderVersion3Size = 0x44; - - public const int ArchiveHeaderHeaderVersion4Size = 0xD0; - - #endregion - - #endregion - - #region HET Table - - public const int HetTableSize = 0x44; - - /// - /// Human-readable signature - /// - public static readonly string HetTableSignatureString = $"HET{(char)0x1A}"; - - /// - /// Signature as an unsigned Int32 value - /// - public const uint HetTableSignatureValue = 0x1A544548; - - /// - /// Signature as a byte array - /// - public static readonly byte[] HetTableSignatureBytes = new byte[] { 0x48, 0x45, 0x54, 0x1A }; - - #endregion - - #region BET Table - - public const int BetTableSize = 0x88; - - /// - /// Human-readable signature - /// - public static readonly string BetTableSignatureString = $"BET{(char)0x1A}"; - - /// - /// Signature as an unsigned Int32 value - /// - public const uint BetTableSignatureValue = 0x1A544542; - - /// - /// Signature as a byte array - /// - public static readonly byte[] BetTableSignatureBytes = new byte[] { 0x42, 0x45, 0x54, 0x1A }; - - #endregion - - #region Hash Entry - - public const int HashEntrySize = 0x10; - - #endregion - - #region Block Entry - - public const int BlockEntrySize = 0x10; + private const uint STORM_BUFFER_SIZE = 0x500; #endregion #region Patch Header - #region Signatures - - #region Patch Header - - /// - /// Human-readable signature - /// - public static readonly string PatchSignatureString = $"PTCH"; - /// /// Signature as an unsigned Int32 value /// public const uint PatchSignatureValue = 0x48435450; - /// - /// Signature as a byte array - /// - public static readonly byte[] PatchSignatureBytes = new byte[] { 0x50, 0x54, 0x43, 0x48 }; - - #endregion - - #region MD5 Block - - /// - /// Human-readable signature - /// - public static readonly string Md5SignatureString = $"MD5_"; - /// /// Signature as an unsigned Int32 value /// public const uint Md5SignatureValue = 0x5F35444D; - /// - /// Signature as a byte array - /// - public static readonly byte[] Md5SignatureBytes = new byte[] { 0x4D, 0x44, 0x35, 0x5F }; - - #endregion - - #region XFRM Block - - /// - /// Human-readable signature - /// - public static readonly string XFRMSignatureString = $"XFRM"; - /// /// Signature as an unsigned Int32 value /// public const uint XFRMSignatureValue = 0x4D524658; - /// - /// Signature as a byte array - /// - public static readonly byte[] XFRMSignatureBytes = new byte[] { 0x58, 0x46, 0x52, 0x4D }; - - #endregion - - #region BSDIFF Patch Type - - /// - /// Human-readable signature - /// - public static readonly string BSDIFF40SignatureString = $"BSDIFF40"; - /// /// Signature as an unsigned Int64 value /// public const ulong BSDIFF40SignatureValue = 0x3034464649445342; - /// - /// Signature as a byte array - /// - public static readonly byte[] BSDIFF40SignatureBytes = new byte[] { 0x42, 0x53, 0x44, 0x49, 0x46, 0x46, 0x34, 0x30 }; - - #endregion - - #endregion - #endregion #endregion @@ -259,7 +107,7 @@ namespace BurnOutSharp.Builder // Check for User Data uint possibleSignature = data.ReadUInt32(); data.Seek(-4, SeekOrigin.Current); - if (possibleSignature == UserDataSignatureValue) + if (possibleSignature == 0x1B51504D) { // Save the current position for offset correction long basePtr = data.Position; @@ -283,7 +131,7 @@ namespace BurnOutSharp.Builder // Check for the Header possibleSignature = data.ReadUInt32(); data.Seek(-4, SeekOrigin.Current); - if (possibleSignature == ArchiveHeaderSignatureValue) + if (possibleSignature == 0x1A51504D) { // Try to parse the archive header var archiveHeader = ParseArchiveHeader(data); @@ -293,13 +141,19 @@ namespace BurnOutSharp.Builder // 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 == 0) + if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1) { // If we have a hash table long hashTableOffset = archive.ArchiveHeader.HashTablePosition; @@ -328,7 +182,7 @@ namespace BurnOutSharp.Builder } // Version 2 and 3 - else if (archive.ArchiveHeader.FormatVersion == 1 || archive.ArchiveHeader.FormatVersion == 2) + 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; @@ -357,7 +211,7 @@ namespace BurnOutSharp.Builder } // Version 4 - else if (archive.ArchiveHeader.FormatVersion == 3) + else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4) { // If we have a hash table long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition; @@ -390,7 +244,7 @@ namespace BurnOutSharp.Builder #region Block Table // Version 1 - if (archive.ArchiveHeader.FormatVersion == 0) + if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1) { // If we have a block table long blockTableOffset = archive.ArchiveHeader.BlockTablePosition; @@ -419,7 +273,7 @@ namespace BurnOutSharp.Builder } // Version 2 and 3 - else if (archive.ArchiveHeader.FormatVersion == 1 || archive.ArchiveHeader.FormatVersion == 2) + 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; @@ -448,7 +302,7 @@ namespace BurnOutSharp.Builder } // Version 4 - else if (archive.ArchiveHeader.FormatVersion == 3) + else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4) { // If we have a block table long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition; @@ -481,9 +335,7 @@ namespace BurnOutSharp.Builder #region Hi-Block Table // Version 2, 3, and 4 - if (archive.ArchiveHeader.FormatVersion == 1 - || archive.ArchiveHeader.FormatVersion == 2 - || archive.ArchiveHeader.FormatVersion == 3) + if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format2) { // If we have a hi-block table long hiBlockTableOffset = (long)archive.ArchiveHeader.HiBlockTablePosition; @@ -510,7 +362,7 @@ namespace BurnOutSharp.Builder #region BET Table // Version 3 and 4 - if (archive.ArchiveHeader.FormatVersion == 2 || archive.ArchiveHeader.FormatVersion == 3) + if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3) { // If we have a BET table long betTableOffset = (long)archive.ArchiveHeader.BetTablePosition; @@ -533,7 +385,7 @@ namespace BurnOutSharp.Builder #region HET Table // Version 3 and 4 - if (archive.ArchiveHeader.FormatVersion == 2 || archive.ArchiveHeader.FormatVersion == 3) + if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3) { // If we have a HET table long hetTableOffset = (long)archive.ArchiveHeader.HetTablePosition; @@ -567,12 +419,12 @@ namespace BurnOutSharp.Builder // V1 - Common archiveHeader.Signature = data.ReadUInt32(); - if (archiveHeader.Signature != ArchiveHeaderSignatureValue) + if (archiveHeader.Signature != 0x1A51504D) return null; archiveHeader.HeaderSize = data.ReadUInt32(); archiveHeader.ArchiveSize = data.ReadUInt32(); - archiveHeader.FormatVersion = data.ReadUInt16(); + archiveHeader.FormatVersion = (FormatVersion)data.ReadUInt16(); archiveHeader.BlockSize = data.ReadUInt16(); archiveHeader.HashTablePosition = data.ReadUInt32(); archiveHeader.BlockTablePosition = data.ReadUInt32(); @@ -580,7 +432,7 @@ namespace BurnOutSharp.Builder archiveHeader.BlockTableSize = data.ReadUInt32(); // V2 - if (archiveHeader.FormatVersion >= 2 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion2Size) + if (archiveHeader.FormatVersion >= FormatVersion.Format2) { archiveHeader.HiBlockTablePosition = data.ReadUInt64(); archiveHeader.HashTablePositionHi = data.ReadUInt16(); @@ -588,7 +440,7 @@ namespace BurnOutSharp.Builder } // V3 - if (archiveHeader.FormatVersion >= 3 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion3Size) + if (archiveHeader.FormatVersion >= FormatVersion.Format3) { archiveHeader.ArchiveSizeLong = data.ReadUInt64(); archiveHeader.BetTablePosition = data.ReadUInt64(); @@ -596,7 +448,7 @@ namespace BurnOutSharp.Builder } // V4 - if (archiveHeader.FormatVersion >= 4 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion4Size) + if (archiveHeader.FormatVersion >= FormatVersion.Format4) { archiveHeader.HashTableSizeLong = data.ReadUInt64(); archiveHeader.BlockTableSizeLong = data.ReadUInt64(); @@ -626,7 +478,7 @@ namespace BurnOutSharp.Builder UserData userData = new UserData(); userData.Signature = data.ReadUInt32(); - if (userData.Signature != UserDataSignatureValue) + if (userData.Signature != 0x1B51504D) return null; userData.UserDataSize = data.ReadUInt32(); @@ -647,7 +499,7 @@ namespace BurnOutSharp.Builder // Common Headers hetTable.Signature = data.ReadUInt32(); - if (hetTable.Signature != HetTableSignatureValue) + if (hetTable.Signature != 0x1A544548) return null; hetTable.Version = data.ReadUInt32(); @@ -680,7 +532,7 @@ namespace BurnOutSharp.Builder // Common Headers betTable.Signature = data.ReadUInt32(); - if (betTable.Signature != BetTableSignatureValue) + if (betTable.Signature != 0x1A544542) return null; betTable.Version = data.ReadUInt32(); @@ -777,5 +629,63 @@ namespace BurnOutSharp.Builder } #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 } } diff --git a/BurnOutSharp.Models/MoPaQ/ArchiveHeader.cs b/BurnOutSharp.Models/MoPaQ/ArchiveHeader.cs index 4a1fe308..d97e722d 100644 --- a/BurnOutSharp.Models/MoPaQ/ArchiveHeader.cs +++ b/BurnOutSharp.Models/MoPaQ/ArchiveHeader.cs @@ -37,7 +37,7 @@ namespace BurnOutSharp.Models.MoPaQ /// 2 = Format 3 (WoW - Cataclysm beta or newer) /// 3 = Format 4 (WoW - Cataclysm beta or newer) /// - public ushort FormatVersion; + public FormatVersion FormatVersion; /// /// Power of two exponent specifying the number of 512-byte disk sectors in each logical sector diff --git a/BurnOutSharp.Models/MoPaQ/Enums.cs b/BurnOutSharp.Models/MoPaQ/Enums.cs index b157119e..753fd1b7 100644 --- a/BurnOutSharp.Models/MoPaQ/Enums.cs +++ b/BurnOutSharp.Models/MoPaQ/Enums.cs @@ -2,6 +2,58 @@ namespace BurnOutSharp.Models.MoPaQ { + /// + /// Compression types for multiple compressions + /// + [Flags] + public enum CompressionType : uint + { + /// + /// Huffmann compression (used on WAVE files only) + /// + MPQ_COMPRESSION_HUFFMANN = 0x01, + + /// + /// ZLIB compression + /// + MPQ_COMPRESSION_ZLIB = 0x02, + + /// + /// PKWARE DCL compression + /// + MPQ_COMPRESSION_PKWARE = 0x08, + + /// + /// BZIP2 compression (added in Warcraft III) + /// + MPQ_COMPRESSION_BZIP2 = 0x10, + + /// + /// Sparse compression (added in Starcraft 2) + /// + MPQ_COMPRESSION_SPARSE = 0x20, + + /// + /// IMA ADPCM compression (mono) + /// + MPQ_COMPRESSION_ADPCM_MONO = 0x40, + + /// + /// IMA ADPCM compression (stereo) + /// + MPQ_COMPRESSION_ADPCM_STEREO = 0x80, + + /// + /// LZMA compression. Added in Starcraft 2. This value is NOT a combination of flags. + /// + MPQ_COMPRESSION_LZMA = 0x12, + + /// + /// Same compression + /// + MPQ_COMPRESSION_NEXT_SAME = 0xFFFFFFFF, + } + [Flags] public enum FileFlags : uint { @@ -57,6 +109,29 @@ namespace BurnOutSharp.Models.MoPaQ MPQ_FILE_EXISTS = 0x80000000, } + public enum FormatVersion : ushort + { + /// + /// Format 1 (up to The Burning Crusade) + /// + Format1 = 0, + + /// + /// Format 2 (The Burning Crusade and newer) + /// + Format2 = 1, + + /// + /// Format 3 (WoW - Cataclysm beta or newer) + /// + Format3 = 2, + + /// + /// Format 4 (WoW - Cataclysm beta or newer) + /// + Format4 = 3, + } + public enum Locale : short { Neutral = 0,