diff --git a/Bytes/MoPaQ.Deserializer.cs b/Bytes/MoPaQ.Deserializer.cs new file mode 100644 index 00000000..2adde262 --- /dev/null +++ b/Bytes/MoPaQ.Deserializer.cs @@ -0,0 +1,28 @@ +using System.IO; +using SabreTools.Models.MoPaQ; + +namespace SabreTools.Serialization.Bytes +{ + public partial class MoPaQ : IByteSerializer + { + /// +#if NET48 + public Archive Deserialize(byte[] data, int offset) +#else + public Archive? Deserialize(byte[]? data, int offset) +#endif + { + // 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 new Streams.MoPaQ().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Streams/MoPaQ.Deserializer.cs b/Streams/MoPaQ.Deserializer.cs new file mode 100644 index 00000000..cd7a8cce --- /dev/null +++ b/Streams/MoPaQ.Deserializer.cs @@ -0,0 +1,622 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.MoPaQ; +using static SabreTools.Models.MoPaQ.Constants; + +namespace SabreTools.Serialization.Streams +{ + public partial class MoPaQ : IStreamSerializer + { + /// +#if NET48 + public Archive Deserialize(Stream data) +#else + public Archive? Deserialize(Stream? data) +#endif + { + // 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 + byte[] signature = data.ReadBytes(4); + archiveHeader.Signature = Encoding.ASCII.GetString(signature); + if (archiveHeader.Signature != ArchiveHeaderSignatureString) + 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(); + + byte[] signature = data.ReadBytes(4); + userData.Signature = Encoding.ASCII.GetString(signature); + if (userData.Signature != UserDataSignatureString) + 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 + byte[] signature = data.ReadBytes(4); + hetTable.Signature = Encoding.ASCII.GetString(signature); + if (hetTable.Signature != HetTableSignatureString) + 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 + byte[] signature = data.ReadBytes(4); + betTable.Signature = Encoding.ASCII.GetString(signature); + if (betTable.Signature != BetTableSignatureString) + 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; + } + + #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 + } +} \ No newline at end of file diff --git a/Streams/MoPaQ.Serializer.cs b/Streams/MoPaQ.Serializer.cs new file mode 100644 index 00000000..cad37e37 --- /dev/null +++ b/Streams/MoPaQ.Serializer.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using SabreTools.Models.MoPaQ; + +namespace SabreTools.Serialization.Streams +{ + public partial class MoPaQ : IStreamSerializer + { + /// +#if NET48 + public Stream Serialize(Archive obj) => throw new NotImplementedException(); +#else + public Stream? Serialize(Archive? obj) => throw new NotImplementedException(); +#endif + } +} \ No newline at end of file