/// <summary> /// Initializes a new instance of the <see cref="MpqArchive"/> class. /// </summary> /// <param name="sourceStream">The <see cref="Stream"/> from which to load an <see cref="MpqArchive"/>.</param> /// <param name="loadListfile">If true, automatically execute <see cref="AddListfileFilenames()"/> after the <see cref="MpqArchive"/> is initialized.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="sourceStream"/> is null.</exception> /// <exception cref="MpqParserException">Thrown when the <see cref="MpqHeader"/> could not be found, or when the MPQ format version is not 0.</exception> public MpqArchive(Stream sourceStream, bool loadListfile = false) { _baseStream = sourceStream ?? throw new ArgumentNullException(nameof(sourceStream)); if (!TryLocateMpqHeader(_baseStream, out var mpqHeader, out _headerOffset)) { throw new MpqParserException("Unable to locate MPQ header."); } _mpqHeader = mpqHeader; _blockSize = BlockSizeModifier << _mpqHeader.BlockSize; _archiveFollowsHeader = _mpqHeader.IsArchiveAfterHeader(); if (_mpqHeader.MpqVersion != 0) { throw new MpqParserException($"MPQ format version {_mpqHeader.MpqVersion} is not supported"); } using (var reader = new BinaryReader(_baseStream, new UTF8Encoding(), true)) { // Load hash table _baseStream.Seek(_mpqHeader.HashTablePosition, SeekOrigin.Begin); _hashTable = new HashTable(reader, _mpqHeader.HashTableSize); // Load entry table _baseStream.Seek(_mpqHeader.BlockTablePosition, SeekOrigin.Begin); _blockTable = new BlockTable(reader, _mpqHeader.BlockTableSize, (uint)_headerOffset); } if (loadListfile) { AddListfileFilenames(); } }
private static bool TryLocateMpqHeader( Stream sourceStream, #if NETCOREAPP3_0 [NotNullWhen(true)] #endif out MpqHeader?mpqHeader, out long headerOffset) { sourceStream.Seek(0, SeekOrigin.Begin); using (var reader = new BinaryReader(sourceStream, new UTF8Encoding(), true)) { for (headerOffset = 0; headerOffset < sourceStream.Length - MpqHeader.Size; headerOffset += PreArchiveAlignBytes) { if (reader.ReadUInt32() == MpqHeader.MpqId) { sourceStream.Seek(-4, SeekOrigin.Current); mpqHeader = MpqHeader.FromReader(reader); mpqHeader.HeaderOffset = (uint)headerOffset; return(true); } sourceStream.Seek(PreArchiveAlignBytes - 4, SeekOrigin.Current); } } mpqHeader = null; headerOffset = -1; return(false); }
/// <summary> /// Initializes a new instance of the <see cref="MpqArchive"/> class. /// </summary> /// <param name="sourceStream">The <see cref="Stream"/> from which to load an <see cref="MpqArchive"/>.</param> /// <param name="loadListFile">If <see langword="true"/>, automatically add filenames from the <see cref="MpqArchive"/>'s <see cref="ListFile"/> after the archive is initialized.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="sourceStream"/> is <see langword="null"/>.</exception> /// <exception cref="MpqParserException">Thrown when the <see cref="MpqHeader"/> could not be found, or when the MPQ format version is not 0.</exception> public MpqArchive(Stream sourceStream, bool loadListFile = false) { _isStreamOwner = true; _baseStream = sourceStream ?? throw new ArgumentNullException(nameof(sourceStream)); if (!TryLocateMpqHeader(_baseStream, out var mpqHeader, out _headerOffset)) { throw new MpqParserException("Unable to locate MPQ header."); } _mpqHeader = mpqHeader; _blockSize = BlockSizeModifier << _mpqHeader.BlockSize; _archiveFollowsHeader = _mpqHeader.IsArchiveAfterHeader(); using (var reader = new BinaryReader(_baseStream, new UTF8Encoding(), true)) { // Load hash table _baseStream.Seek(_mpqHeader.HashTablePosition, SeekOrigin.Begin); _hashTable = new HashTable(reader, _mpqHeader.HashTableSize); // Load entry table _baseStream.Seek(_mpqHeader.BlockTablePosition, SeekOrigin.Begin); _blockTable = new BlockTable(reader, _mpqHeader.BlockTableSize, (uint)_headerOffset); } if (loadListFile) { if (TryOpenFile(ListFile.FileName, out var listFileStream)) { using var listFileReader = new StreamReader(listFileStream, leaveOpen: false); AddFileNames(listFileReader.ReadListFile().FileNames); } } }
/// <summary> /// Reads from the given reader to create a new MPQ header. /// </summary> /// <param name="reader">The reader from which to read.</param> /// <returns>The parsed <see cref="MpqHeader"/>.</returns> public static MpqHeader FromReader(BinaryReader reader) { var id = reader?.ReadUInt32() ?? throw new ArgumentNullException(nameof(reader)); if (id != MpqId) { throw new MpqParserException($"Invalid MPQ header signature: {id}"); } var header = new MpqHeader { ID = id, DataOffset = reader.ReadUInt32(), ArchiveSize = reader.ReadUInt32(), MpqVersion = reader.ReadUInt16(), BlockSize = reader.ReadUInt16(), HashTableOffset = reader.ReadUInt32(), BlockTableOffset = reader.ReadUInt32(), HashTableSize = reader.ReadUInt32(), BlockTableSize = reader.ReadUInt32(), }; #if DEBUG if (header.MpqVersion == 0) { // Check validity if (header.DataOffset != Size) { throw new MpqParserException($"Invalid MPQ header field: DataOffset. Was {header.DataOffset}, expected {Size}"); } if (header.ArchiveSize != header.BlockTableOffset + (MpqEntry.Size * header.BlockTableSize)) { throw new MpqParserException($"Invalid MPQ header field: ArchiveSize. Was {header.ArchiveSize}, expected {header.BlockTableOffset + (MpqEntry.Size * header.BlockTableSize)}"); } if (header.HashTableOffset != header.ArchiveSize - (MpqHash.Size * header.HashTableSize) - (MpqEntry.Size * header.BlockTableSize)) { throw new MpqParserException($"Invalid MPQ header field: HashTablePos. Was {header.HashTableOffset}, expected {header.ArchiveSize - (MpqHash.Size * header.HashTableSize) - (MpqEntry.Size * header.BlockTableSize)}"); } if (header.BlockTableOffset != header.HashTableOffset + (MpqHash.Size * header.HashTableSize)) { throw new MpqParserException($"Invalid MPQ header field: BlockTablePos. Was {header.BlockTableOffset}, expected {header.HashTableOffset + (MpqHash.Size * header.HashTableSize)}"); } } #endif if (header.MpqVersion != 0) { throw new NotSupportedException($"MPQ format version {header.MpqVersion} is not supported"); // The extended block table is an array of Int16 - higher bits of the offests in the block table. // header.ExtendedBlockTableOffset = br.ReadInt64(); // header.HashTableOffsetHigh = br.ReadInt16(); // header.BlockTableOffsetHigh = br.ReadInt16(); } return(header); }
/// <summary> /// /// </summary> /// <param name="br"></param> /// <returns></returns> public static MpqHeader FromReader(BinaryReader br) { var id = br?.ReadUInt32() ?? throw new ArgumentNullException(nameof(br)); if (id != MpqId) { return(null); } var header = new MpqHeader { ID = id, DataOffset = br.ReadUInt32(), ArchiveSize = br.ReadUInt32(), MpqVersion = br.ReadUInt16(), BlockSize = br.ReadUInt16(), HashTablePos = br.ReadUInt32(), BlockTablePos = br.ReadUInt32(), HashTableSize = br.ReadUInt32(), BlockTableSize = br.ReadUInt32(), }; #if DEBUG if (header.MpqVersion == 0) { // Check validity // TODO: deal with protected archive DataOffset value. if (header.DataOffset != Size) { throw new MpqParserException(string.Format("Invalid MPQ header field: DataOffset. Was {0}, expected {1}", header.DataOffset, Size)); } if (header.ArchiveSize != header.BlockTablePos + (MpqEntry.Size * header.BlockTableSize)) { throw new MpqParserException(string.Format("Invalid MPQ header field: ArchiveSize. Was {0}, expected {1}", header.ArchiveSize, header.BlockTablePos + (MpqEntry.Size * header.BlockTableSize))); } if (header.HashTablePos != header.ArchiveSize - (MpqHash.Size * header.HashTableSize) - (MpqEntry.Size * header.BlockTableSize)) { throw new MpqParserException(string.Format("Invalid MPQ header field: HashTablePos. Was {0}, expected {1}", header.HashTablePos, header.ArchiveSize - (MpqHash.Size * header.HashTableSize) - (MpqEntry.Size * header.BlockTableSize))); } if (header.BlockTablePos != header.HashTablePos + (MpqHash.Size * header.HashTableSize)) { throw new MpqParserException(string.Format("Invalid MPQ header field: BlockTablePos. Was {0}, expected {1}", header.BlockTablePos, header.HashTablePos + (MpqHash.Size * header.HashTableSize))); } } #endif if (header.MpqVersion == 1) { header.ExtendedBlockTableOffset = br.ReadInt64(); header.HashTableOffsetHigh = br.ReadInt16(); header.BlockTableOffsetHigh = br.ReadInt16(); } return(header); }
private static long LocateMpqHeader(Stream sourceStream, out MpqHeader mpqHeader) { using (var reader = new BinaryReader(sourceStream, new UTF8Encoding(), true)) { for (long i = 0; i < sourceStream.Length - MpqHeader.Size; i += PreArchiveAlignBytes) { sourceStream.Seek(i, SeekOrigin.Begin); mpqHeader = MpqHeader.FromReader(reader); if (mpqHeader?.SetHeaderOffset(i) ?? false) { return(i); } } } mpqHeader = null; return(-1); }
/// <summary> /// Initializes a new instance of the <see cref="MpqArchive"/> class. /// </summary> /// <param name="sourceStream">The <see cref="Stream"/> containing pre-archive data. Can be null.</param> /// <param name="mpqFiles">The <see cref="MpqFile"/>s that should be added to the archive.</param> /// <param name="hashTableSize">The desired size of the <see cref="BlockTable"/>. Larger size decreases the likelihood of hash collisions.</param> /// <param name="blockSize">The size of blocks in compressed files, which is used to enable seeking.</param> /// <param name="writeArchiveFirst">If true, the archive files will be positioned directly after the header. Otherwise, the hashtable and blocktable will come first.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="mpqFiles"/> collection is null.</exception> public MpqArchive(Stream?sourceStream, IEnumerable <MpqFile> inputFiles, ushort?hashTableSize = null, ushort blockSize = DefaultBlockSize, bool writeArchiveFirst = true) { _baseStream = AlignStream(sourceStream); _headerOffset = _baseStream.Position; _blockSize = BlockSizeModifier << blockSize; _archiveFollowsHeader = writeArchiveFirst; var mpqFiles = inputFiles.ToList(); var fileCount = (uint)(mpqFiles ?? throw new ArgumentNullException(nameof(mpqFiles))).Count; _hashTable = new HashTable(Math.Max(hashTableSize ?? fileCount * 8, fileCount)); _blockTable = new BlockTable(); using (var writer = new BinaryWriter(_baseStream, new UTF8Encoding(false, true), true)) { // Skip the MPQ header, since its contents will be calculated afterwards. writer.Seek((int)MpqHeader.Size, SeekOrigin.Current); // Write Archive var fileIndex = 0U; var fileOffset = _archiveFollowsHeader ? MpqHeader.Size : throw new NotImplementedException(); // var gaps = new List<(long Start, long Length)>(); var endOfStream = _baseStream.Position; // Find files that cannot be decrypted, and need to have a specific position in the archive, because that position is used to calculate the encryption seed. var mpqFixedPositionFiles = mpqFiles.Where(mpqFile => mpqFile.IsFilePositionFixed).OrderBy(mpqFile => mpqFile.MpqStream.FilePosition).ToArray(); if (mpqFixedPositionFiles.Length > 0) { if (mpqFixedPositionFiles.First() !.MpqStream.FilePosition < 0) { throw new NotSupportedException($"Cannot place files in front of the header."); } foreach (var mpqFixedPositionFile in mpqFixedPositionFiles) { var position = mpqFixedPositionFile.MpqStream.FilePosition; if (position < endOfStream) { throw new ArgumentException($"Fixed position files overlap with each other and/or the header. Archive cannot be created.", nameof(inputFiles)); } if (position > endOfStream) { var gapSize = position - endOfStream; // gaps.Add((endOfStream, gapSize)); writer.Seek((int)gapSize, SeekOrigin.Current); } mpqFixedPositionFile.AddToArchive(this, fileIndex, out var mpqEntry, out var mpqHash); var hashTableEntries = _hashTable.Add(mpqHash, mpqFixedPositionFile.HashIndex, mpqFixedPositionFile.HashCollisions); for (var i = 0; i < hashTableEntries; i++) { _blockTable.Add(mpqEntry); } mpqFixedPositionFile.Dispose(); fileIndex += hashTableEntries; endOfStream = _baseStream.Position; } } mpqFiles.RemoveAll(mpqFile => mpqFile.IsFilePositionFixed); foreach (var mpqFile in mpqFiles) { // TODO: insert files into the gaps // need to know compressed size of file first, and if file is also encrypted with blockoffsetadjustedkey, encryption needs to happen after gap selection // therefore, can't use current AddToArchive method, which does both compression and encryption at same time // var availableGaps = gaps.Where(gap => gap.Length >= ) var selectedPosition = endOfStream; var selectedGap = false; _baseStream.Position = selectedPosition; mpqFile.AddToArchive(this, fileIndex, out var mpqEntry, out var mpqHash); var hashTableEntries = _hashTable.Add(mpqHash, mpqFile.HashIndex, mpqFile.HashCollisions); for (var i = 0; i < hashTableEntries; i++) { _blockTable.Add(mpqEntry); } mpqFile.Dispose(); fileIndex += hashTableEntries; if (!selectedGap) { endOfStream = _baseStream.Position; } } _baseStream.Position = endOfStream; _hashTable.WriteTo(writer); _blockTable.WriteTo(writer); /*if (!_archiveFollowsHeader) * { * foreach (var mpqFile in mpqFiles) * { * mpqFile.WriteTo(writer, true); * } * }*/ writer.Seek((int)_headerOffset, SeekOrigin.Begin); _mpqHeader = new MpqHeader((uint)(endOfStream - fileOffset), _hashTable.Size, _blockTable.Size, blockSize, _archiveFollowsHeader); _mpqHeader.WriteTo(writer); } }
/// <summary> /// Initializes a new instance of the <see cref="MpqArchive"/> class. /// </summary> /// <param name="sourceStream">The <see cref="Stream"/> containing pre-archive data. Can be null.</param> /// <param name="mpqFiles">The <see cref="MpqFile"/>s that should be added to the archive.</param> /// <param name="hashTableSize">The desired size of the <see cref="BlockTable"/>. Larger size decreases the likelihood of hash collisions.</param> /// <param name="blockSize">The size of blocks in compressed files, which is used to enable seeking.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="mpqFiles"/> collection is null.</exception> public MpqArchive(Stream sourceStream, ICollection <MpqFile> mpqFiles, ushort?hashTableSize = null, ushort blockSize = 8) { // TODO: copy sourceStream contents to a new stream if CanWrite property is false (can do this in alignStream method) _baseStream = AlignStream(sourceStream ?? new MemoryStream()); _headerOffset = _baseStream.Position; _blockSize = BlockSizeModifier << blockSize; var fileCount = (uint)(mpqFiles ?? throw new ArgumentNullException(nameof(mpqFiles))).Count; _hashTable = new HashTable(Math.Max(hashTableSize ?? fileCount * 8, fileCount)); _blockTable = new BlockTable(fileCount); using (var writer = new BinaryWriter(_baseStream, new UTF8Encoding(false, true), true)) { // Skip the MPQ header, since its contents will be calculated afterwards. writer.Seek((int)MpqHeader.Size, SeekOrigin.Current); const bool archiveBeforeTables = true; uint hashTableEntries = 0; // Write Archive var fileIndex = 0U; var fileOffset = archiveBeforeTables ? MpqHeader.Size : throw new NotImplementedException(); var filePos = fileOffset; // TODO: add support for encryption of the archive files foreach (var mpqFile in mpqFiles) { var locale = MpqLocale.Neutral; mpqFile.AddToArchive((uint)_headerOffset, fileIndex, filePos, locale, _hashTable.Mask); if (archiveBeforeTables) { mpqFile.SerializeTo(writer, true); } hashTableEntries += _hashTable.Add(mpqFile.MpqHash, mpqFile.HashIndex, mpqFile.HashCollisions); _blockTable.Add(mpqFile.MpqEntry); filePos += mpqFile.MpqEntry.CompressedSize; fileIndex++; } // Match size of blocktable with amount of occupied entries in hashtable /* * for ( var i = blockTable.Size; i < hashTableEntries; i++ ) * { * var entry = MpqEntry.Dummy; * entry.SetPos( filePos ); * blockTable.Add( entry ); * } * blockTable.UpdateSize(); */ _hashTable.SerializeTo(writer); _blockTable.SerializeTo(writer); if (!archiveBeforeTables) { foreach (var mpqFile in mpqFiles) { mpqFile.SerializeTo(writer, true); } } writer.Seek((int)_headerOffset, SeekOrigin.Begin); _mpqHeader = new MpqHeader(filePos - fileOffset, _hashTable.Size, _blockTable.Size, blockSize, archiveBeforeTables); _mpqHeader.WriteToStream(writer); } }
/// <summary> /// Initializes a new instance of the <see cref="MpqArchive"/> class. /// </summary> /// <param name="sourceStream">The <see cref="Stream"/> containing pre-archive data. Can be <see langword="null"/>.</param> /// <param name="inputFiles">The <see cref="MpqFile"/>s that should be added to the archive.</param> /// <param name="createOptions"></param> /// <param name="leaveOpen">If <see langword="false"/>, the given <paramref name="sourceStream"/> will be disposed when the <see cref="MpqArchive"/> is disposed.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="mpqFiles"/> collection is <see langword="null"/>.</exception> public MpqArchive(Stream?sourceStream, IEnumerable <MpqFile> inputFiles, MpqArchiveCreateOptions createOptions, bool leaveOpen = false) { if (inputFiles is null) { throw new ArgumentNullException(nameof(inputFiles)); } if (createOptions is null) { throw new ArgumentNullException(nameof(createOptions)); } _isStreamOwner = !leaveOpen; _baseStream = AlignStream(sourceStream); _headerOffset = _baseStream.Position; _blockSize = BlockSizeModifier << createOptions.BlockSize; _archiveFollowsHeader = createOptions.WriteArchiveFirst; var signatureName = Signature.FileName.GetStringHash(); var listFileName = ListFile.FileName.GetStringHash(); var attributesName = Attributes.FileName.GetStringHash(); var signatureCreateMode = createOptions.SignatureCreateMode.GetValueOrDefault(MpqFileCreateMode.Prune); var listFileCreateMode = createOptions.ListFileCreateMode.GetValueOrDefault(MpqFileCreateMode.Overwrite); var attributesCreateMode = createOptions.AttributesCreateMode.GetValueOrDefault(MpqFileCreateMode.Overwrite); var haveSignature = false; var haveListFile = false; var haveAttributes = false; var mpqFiles = new HashSet <MpqFile>(MpqFileComparer.Default); foreach (var mpqFile in inputFiles) { if (mpqFile is MpqOrphanedFile) { continue; } if (mpqFile.Name == signatureName) { if (signatureCreateMode.HasFlag(MpqFileCreateMode.RemoveFlag)) { continue; } else { haveSignature = true; } } if (mpqFile.Name == listFileName) { if (listFileCreateMode.HasFlag(MpqFileCreateMode.RemoveFlag)) { continue; } else { haveListFile = true; } } if (mpqFile.Name == attributesName) { if (attributesCreateMode.HasFlag(MpqFileCreateMode.RemoveFlag)) { continue; } else { haveAttributes = true; } } if (!mpqFiles.Add(mpqFile)) { // todo: logging? } } var fileCount = (uint)mpqFiles.Count; var wantGenerateSignature = !haveSignature && signatureCreateMode.HasFlag(MpqFileCreateMode.AddFlag); var signature = wantGenerateSignature ? new Signature() : null; if (wantGenerateSignature) { fileCount++; } var wantGenerateListFile = !haveListFile && listFileCreateMode.HasFlag(MpqFileCreateMode.AddFlag); var listFile = wantGenerateListFile ? new ListFile() : null; if (wantGenerateListFile) { fileCount++; } var wantGenerateAttributes = !haveAttributes && attributesCreateMode.HasFlag(MpqFileCreateMode.AddFlag); var attributes = wantGenerateAttributes ? new Attributes(createOptions) : null; if (wantGenerateAttributes) { fileCount++; } _hashTable = new HashTable(Math.Max(createOptions.HashTableSize ?? fileCount * 8, fileCount)); _blockTable = new BlockTable(); using (var writer = new BinaryWriter(_baseStream, new UTF8Encoding(false, true), true)) { // Skip the MPQ header, since its contents will be calculated afterwards. writer.Seek((int)MpqHeader.Size, SeekOrigin.Current); // Write Archive var fileIndex = 0U; var fileOffset = _archiveFollowsHeader ? MpqHeader.Size : throw new NotImplementedException(); // var gaps = new List<(long Start, long Length)>(); var endOfStream = _baseStream.Position; void InsertMpqFile(MpqFile mpqFile, bool updateEndOfStream, bool allowMultiple = true) { if (listFile is not null && mpqFile is MpqKnownFile knownFile) { listFile.FileNames.Add(knownFile.FileName); } mpqFile.AddToArchive(this, fileIndex, out var mpqEntry, out var mpqHash); var hashTableEntries = _hashTable.Add(mpqHash, mpqFile.HashIndex, mpqFile.HashCollisions); if (!allowMultiple && hashTableEntries > 1) { throw new Exception(); } var crc32 = 0; if (attributes is not null && attributes.Flags.HasFlag(AttributesFlags.Crc32) && allowMultiple) { mpqFile.MpqStream.Position = 0; crc32 = new Ionic.Crc.CRC32().GetCrc32(mpqFile.MpqStream); } for (var i = 0; i < hashTableEntries; i++) { _blockTable.Add(mpqEntry); if (attributes is not null) { if (attributes.Flags.HasFlag(AttributesFlags.Crc32)) { attributes.Crc32s.Add(crc32); } if (attributes.Flags.HasFlag(AttributesFlags.DateTime)) { attributes.DateTimes.Add(DateTime.Now); } if (attributes.Flags.HasFlag(AttributesFlags.Unk0x04)) { attributes.Unk0x04s.Add(new byte[16]); } } } mpqFile.Dispose(); fileIndex += hashTableEntries; if (updateEndOfStream) { endOfStream = _baseStream.Position; } } // Find files that cannot be decrypted, and need to have a specific position in the archive, because that position is used to calculate the encryption seed. var mpqFixedPositionFiles = mpqFiles.Where(mpqFile => mpqFile.IsFilePositionFixed).OrderBy(mpqFile => mpqFile.MpqStream.FilePosition).ToArray(); if (mpqFixedPositionFiles.Length > 0) { if (mpqFixedPositionFiles.First() !.MpqStream.FilePosition < 0) { throw new NotSupportedException($"Cannot place files in front of the header."); } foreach (var mpqFixedPositionFile in mpqFixedPositionFiles) { var position = mpqFixedPositionFile.MpqStream.FilePosition; if (position < endOfStream) { throw new ArgumentException($"Fixed position files overlap with each other and/or the header. Archive cannot be created.", nameof(inputFiles)); } if (position > endOfStream) { var gapSize = position - endOfStream; // gaps.Add((endOfStream, gapSize)); writer.Seek((int)gapSize, SeekOrigin.Current); } InsertMpqFile(mpqFixedPositionFile, true); } } foreach (var mpqFile in mpqFiles.Where(mpqFile => !mpqFile.IsFilePositionFixed)) { // TODO: insert files into the gaps // need to know compressed size of file first, and if file is also encrypted with blockoffsetadjustedkey, encryption needs to happen after gap selection // therefore, can't use current AddToArchive method, which does both compression and encryption at same time // var availableGaps = gaps.Where(gap => gap.Length >= ) var selectedPosition = endOfStream; var selectedGap = false; _baseStream.Position = selectedPosition; InsertMpqFile(mpqFile, !selectedGap); } var signaturePosition = endOfStream + 8; if (signature is not null) { _baseStream.Position = endOfStream; using var signatureStream = new MemoryStream(); using var signatureWriter = new BinaryWriter(signatureStream); signatureWriter.Write(signature); signatureWriter.Flush(); using var signatureMpqFile = MpqFile.New(signatureStream, Signature.FileName); signatureMpqFile.TargetFlags = MpqFileFlags.Exists; InsertMpqFile(signatureMpqFile, true); } if (listFile is not null) { _baseStream.Position = endOfStream; using var listFileStream = new MemoryStream(); using var listFileWriter = new StreamWriter(listFileStream); listFileWriter.WriteListFile(listFile); listFileWriter.Flush(); using var listFileMpqFile = MpqFile.New(listFileStream, ListFile.FileName); listFileMpqFile.TargetFlags = MpqFileFlags.Exists | MpqFileFlags.CompressedMulti | MpqFileFlags.Encrypted | MpqFileFlags.BlockOffsetAdjustedKey; InsertMpqFile(listFileMpqFile, true); } if (attributes is not null) { _baseStream.Position = endOfStream; if (attributes.Flags.HasFlag(AttributesFlags.Crc32)) { attributes.Crc32s.Add(0); } if (attributes.Flags.HasFlag(AttributesFlags.DateTime)) { attributes.DateTimes.Add(DateTime.Now); } if (attributes.Flags.HasFlag(AttributesFlags.Unk0x04)) { attributes.Unk0x04s.Add(new byte[16]); } using var attributesStream = new MemoryStream(); using var attributesWriter = new BinaryWriter(attributesStream); attributesWriter.Write(attributes); attributesWriter.Flush(); using var attributesMpqFile = MpqFile.New(attributesStream, Attributes.FileName); attributesMpqFile.TargetFlags = MpqFileFlags.Exists | MpqFileFlags.CompressedMulti | MpqFileFlags.Encrypted | MpqFileFlags.BlockOffsetAdjustedKey; InsertMpqFile(attributesMpqFile, true, false); } _baseStream.Position = endOfStream; _hashTable.WriteTo(writer); _blockTable.WriteTo(writer); /*if (!_archiveFollowsHeader) * { * foreach (var mpqFile in mpqFiles) * { * mpqFile.WriteTo(writer, true); * } * }*/ writer.Seek((int)_headerOffset, SeekOrigin.Begin); _mpqHeader = new MpqHeader((uint)_headerOffset, (uint)(endOfStream - fileOffset), _hashTable.Size, _blockTable.Size, createOptions.BlockSize, _archiveFollowsHeader); _mpqHeader.WriteTo(writer); if (wantGenerateSignature) { var archiveBytes = new byte[_mpqHeader.ArchiveSize]; _baseStream.Position = _headerOffset; _baseStream.Read(archiveBytes); using var rsa = RSA.Create(); rsa.ImportFromPem(createOptions.SignaturePrivateKey); var signatureBytes = rsa.SignData(archiveBytes, HashAlgorithmName.MD5, RSASignaturePadding.Pkcs1); _baseStream.Position = signaturePosition; _baseStream.Write(signatureBytes.Reverse().ToArray()); } } }
/// <summary> /// Reads from the given reader to create a new MPQ header. /// </summary> /// <param name="reader">The reader from which to read.</param> /// <returns>The parsed <see cref="MpqHeader"/>.</returns> public static MpqHeader FromReader(BinaryReader reader) { var id = reader?.ReadUInt32() ?? throw new ArgumentNullException(nameof(reader)); if (id != MpqId) { throw new MpqParserException($"Invalid MPQ header signature: {id}"); } var header = new MpqHeader { ID = id, DataOffset = reader.ReadUInt32(), ArchiveSize = reader.ReadUInt32(), MpqVersion = reader.ReadUInt16(), BlockSize = reader.ReadUInt16(), HashTableOffset = reader.ReadUInt32(), BlockTableOffset = reader.ReadUInt32(), HashTableSize = reader.ReadUInt32(), BlockTableSize = reader.ReadUInt32(), }; if (header.MpqVersion == 1) { // TODO: support v1 // The extended block table is an array of Int16 - higher bits of the offests in the block table. // header.ExtendedBlockTableOffset = br.ReadInt64(); // header.HashTableOffsetHigh = br.ReadInt16(); // header.BlockTableOffsetHigh = br.ReadInt16(); // TODO: validate v1 const bool IsInvalidVersion1 = true; if (IsInvalidVersion1) { // Assume real version is 0, but it was set to 1 for protection. header.MpqVersion = 0; } } #if DEBUG if (header.MpqVersion == 0) { var expectedHashTableOffset = header.BlockTableOffset - (MpqHash.Size * header.HashTableSize); if (header.HashTableOffset != expectedHashTableOffset) { throw new MpqParserException($"Invalid MPQ header field: {nameof(HashTableOffset)}. Expected: {expectedHashTableOffset}, Actual: {header.HashTableOffset}."); } var expectedBlockTableOffset = header.HashTableOffset + (MpqHash.Size * header.HashTableSize); if (header.BlockTableOffset != expectedBlockTableOffset) { throw new MpqParserException($"Invalid MPQ header field: {nameof(BlockTableOffset)}. Expected: {expectedBlockTableOffset}, Actual: {header.BlockTableOffset}."); } } #endif if (header.MpqVersion != 0) { throw new NotSupportedException($"MPQ format version {header.MpqVersion} is not supported"); } return(header); }