Пример #1
0
        /// <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();
            }
        }
Пример #2
0
        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);
        }
Пример #3
0
        /// <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);
                }
            }
        }
Пример #4
0
        /// <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);
        }
Пример #5
0
        /// <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);
        }
Пример #6
0
        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);
        }
Пример #7
0
        /// <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);
            }
        }
Пример #8
0
        /// <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);
            }
        }
Пример #9
0
        /// <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());
                }
            }
        }
Пример #10
0
        /// <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);
        }