Files
Matt Nadareski 8f49e190d8 Fix everything
2026-03-24 19:17:25 -04:00

491 lines
19 KiB
C#

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<Archive>
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Parse a Stream into an ArchiveHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ArchiveHeader on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a BetTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <param name="header">Archive header to get information from</param>
/// <param name="decrypter">Decrypter for processing table data</param>
/// <returns>Filled BetTable on success, null on error</returns>
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);
}
/// <summary>
/// Parse a Stream into a BlockTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <param name="header">Archive header to get information from</param>
/// <param name="decrypter">Decrypter for processing table data</param>
/// <returns>Filled BlockTable on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a HashTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <param name="header">Archive header to get information from</param>
/// <param name="decrypter">Decrypter for processing table data</param>
/// <returns>Filled HashTable on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a HetTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <param name="header">Archive header to get information from</param>
/// <param name="decrypter">Decrypter for processing table data</param>
/// <returns>Filled HetTable on success, null on error</returns>
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);
}
/// <summary>
/// Parse a Stream into a HiBlockTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <param name="header">Archive header to get information from</param>
/// <returns>Filled HiBlockTable on success, null on error</returns>
/// 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;
}
/// <summary>
/// Parse a Stream into a UserData
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled UserData on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a BetTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled BetTable on success, null on error</returns>
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;
}
/// <summary>
/// Parse decrypted data into an BlockEntry
/// </summary>
/// <param name="data">Decrypted data to parse</param>
/// <returns>Filled BlockEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse decrypted data into an HashEntry
/// </summary>
/// <param name="data">Decrypted data to parse</param>
/// <returns>Filled HashEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a decrypted data into a HetTable
/// </summary>
/// <param name="data">Decrypted data to parse</param>
/// <returns>Filled HetTable on success, null on error</returns>
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;
}
}
}