Esempio n. 1
0
        /// <summary>
        /// Adds an <see cref="MpqHash"/> to the <see cref="HashTable"/>.
        /// </summary>
        /// <param name="hash">The <see cref="MpqHash"/> to be added to the <see cref="HashTable"/>.</param>
        /// <param name="hashIndex">The index at which to add the <see cref="MpqHash"/>.</param>
        /// <param name="hashCollisions">The maximum amount of collisions, if the <see cref="MpqFile"/> came from another <see cref="MpqArchive"/> and has an unknown filename.</param>
        /// <returns>
        /// Returns the amount of <see cref="MpqHash"/> objects that have been added.
        /// This is usually 1, but can be more if the <see cref="MpqFile"/> came from another <see cref="MpqArchive"/>, has an unknown filename,
        /// and the <see cref="HashTable"/> of the <see cref="MpqArchive"/> it came from has a smaller size than this one.
        /// </returns>
        public uint Add(MpqHash hash, uint hashIndex, uint hashCollisions)
        {
            var step = hash.Mask + 1;

            var known = step > _mask;

            Console.WriteLine(
                "Adding file #{0} to hashtable, which is {1} file{2}.",
                hash.BlockIndex,
                known ? "a known" : "an unknown",
                known ? string.Empty : $" found at index {hashIndex} with up to {hashCollisions} collisions");

            // If the hash.Mask is smaller than the hashtable's size, this file came from another archive and has an unknown filename.
            // By passing both the mask and the hashIndex corresponding to that mask, can figure out all hashIndices where this file may belong.
            AddEntry(hash, hashIndex, step);

            // For files with unknown filename, it's also possible that the index at which they were found in the HashTable is not their true index.
            // This is because there may have been StringHash collisions in the HashTable.
            // To deal with this, mark the empty entries in this hashtable, where this file's true hashIndex may be located, as deleted.
            while (hashCollisions > 0)
            {
                if (hashIndex == 0)
                {
                    hashIndex = step;
                }

                /** NOTE: replacing AddEntry with AddDeleted is only possible if passing true to the returnOnUnknown argument of method <see cref="MpqArchive.FindCollidingHashEntries"/> */

                AddDeleted(--hashIndex, step);
                // AddEntry( hash, --hashIndex, step );
                hashCollisions--;
            }

            return(Size / step);
        }
Esempio n. 2
0
        private bool TryGetHashEntry(string filename, out MpqHash hash)
        {
            var index = StormBuffer.HashString(filename, 0);

            index &= _mpqHeader.HashTableSize - 1;
            var name = MpqHash.GetHashedFileName(filename);

            for (var i = index; i < _hashTable.Size; ++i)
            {
                hash = _hashTable[i];
                if (hash.Name == name)
                {
                    return(true);
                }
            }

            for (uint i = 0; i < index; ++i)
            {
                hash = _hashTable[i];
                if (hash.Name == name)
                {
                    return(true);
                }
            }

            hash = default;
            return(false);
        }
Esempio n. 3
0
        /// <summary>
        /// Initializes a new instance of the <see cref="HashTable"/> class.
        /// </summary>
        /// <param name="reader">The <see cref="BinaryReader"/> from which to read the contents of the <see cref="HashTable"/>.</param>
        /// <param name="size">The amount of <see cref="MpqHash"/> objects to be added to the <see cref="HashTable"/>.</param>
        /// <exception cref="ArgumentException">Thrown when the <paramref name="size"/> argument is not a power of two.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="size"/> argument is larger than <see cref="MpqTable.MaxSize"/>.</exception>
        internal HashTable(BinaryReader reader, uint size)
        {
            if (size > MaxSize)
            {
                throw new ArgumentOutOfRangeException(nameof(size));
            }

            if (size != GenerateMask(size) + 1)
            {
                throw new ArgumentException($"Size {size} is not a power of two.", nameof(size));
            }

            _hashes = new MpqHash[size];
            _mask   = size - 1;

            var hashdata = reader.ReadBytes((int)(size * MpqHash.Size));

            Decrypt(hashdata);

            using (var stream = new MemoryStream(hashdata))
            {
                using (var streamReader = new BinaryReader(stream))
                {
                    for (var i = 0; i < size; i++)
                    {
                        _hashes[i] = new MpqHash(streamReader, _mask);
                    }
                }
            }
        }
Esempio n. 4
0
 private void AddEntry(MpqHash hash, uint hashIndex, uint step)
 {
     // If the old archive had a smaller hashtable, it masked less bits to determine the index for the hash entry, and cannot recover the bits that were masked away.
     // As a result, need to add this hash entry in every index where the bits match with the old archive's mask.
     for (var i = hashIndex; i <= _mask; i += step)
     {
         // Console.WriteLine( "Try to add file #{0}'s hash at index {1}", hash.BlockIndex, i );
         TryAdd(hash, i);
     }
 }
Esempio n. 5
0
        /// <summary>
        /// Initializes a new instance of the <see cref="MpqFile"/> class, for which the filename is unknown.
        /// </summary>
        /// <param name="sourceStream"></param>
        /// <param name="mpqHash"></param>
        /// <param name="hashIndex"></param>
        /// <param name="hashCollisions"></param>
        /// <param name="flags"></param>
        /// <param name="blockSize"></param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException">Thrown when the <paramref name="sourceStream"/> argument is null.</exception>
        public MpqFile(Stream sourceStream, MpqHash mpqHash, uint hashIndex, uint hashCollisions, MpqFileFlags flags, ushort blockSize)
            : this(sourceStream, null, flags, blockSize)
        {
            if (mpqHash.Mask == 0)
            {
                throw new ArgumentException("Expected the Mask value of mpqHash argument to be set to a non-zero value.", nameof(mpqHash));
            }

            _hash           = mpqHash;
            _hashIndex      = hashIndex;
            _hashCollisions = hashCollisions;
        }
Esempio n. 6
0
        private void TryAdd(MpqHash hash, uint index)
        {
            while (!_hashes[index].IsEmpty)
            {
                // Deal with collisions
                index = (index + 1) & _mask;

                // or: if (++index)>_mask index=0;
            }

            _hashes[index] = hash;
        }
Esempio n. 7
0
        private int TryGetHashEntry(int entryIndex, out MpqHash hash)
        {
            for (var i = 0; i < _hashTable.Size; i++)
            {
                if (_hashTable[i].BlockIndex == entryIndex)
                {
                    hash = _hashTable[i];
                    return(i);
                }
            }

            hash = MpqHash.NULL;
            return(-1);
        }
Esempio n. 8
0
        /// <summary>
        /// Initializes a new instance of the <see cref="MpqUnknownFile"/> class.
        /// </summary>
        internal MpqUnknownFile(MpqStream mpqStream, MpqFileFlags flags, MpqHash mpqHash, uint hashIndex, uint hashCollisions, uint?encryptionSeed = null)
            : base(mpqHash.Name, mpqStream, flags, mpqHash.Locale, false)
        {
            if (mpqHash.Mask == 0)
            {
                throw new ArgumentException("Expected the Mask value of mpqHash argument to be set to a non-zero value.", nameof(mpqHash));
            }

            if (flags.HasFlag(MpqFileFlags.Encrypted) && encryptionSeed is null)
            {
                throw new ArgumentException($"Cannot encrypt an {nameof(MpqUnknownFile)} without an encryption seed.", nameof(flags));
            }

            _hashMask       = mpqHash.Mask;
            _hashIndex      = hashIndex;
            _hashCollisions = hashCollisions;
            _encryptionSeed = encryptionSeed;
        }
Esempio n. 9
0
        private IEnumerable <MpqHash> GetHashEntries(string filename)
        {
            if (!StormBuffer.TryGetHashString(filename, 0, out var index))
            {
                yield break;
            }

            index &= _mpqHeader.HashTableSize - 1;
            var name = MpqHash.GetHashedFileName(filename);

            var foundAnyHash = false;

            for (var i = index; i < _hashTable.Size; ++i)
            {
                var hash = _hashTable[i];
                if (hash.Name == name)
                {
                    yield return(hash);

                    foundAnyHash = true;
                }
                else if (hash.IsEmpty && foundAnyHash)
                {
                    yield break;
                }
            }

            for (uint i = 0; i < index; ++i)
            {
                var hash = _hashTable[i];
                if (hash.Name == name)
                {
                    yield return(hash);

                    foundAnyHash = true;
                }
                else if (hash.IsEmpty && foundAnyHash)
                {
                    yield break;
                }
            }
        }
Esempio n. 10
0
        /// <summary>
        /// Initializes a new instance of the <see cref="HashTable"/> class.
        /// </summary>
        /// <param name="reader">The <see cref="BinaryReader"/> from which to read the contents of the <see cref="HashTable"/>.</param>
        /// <param name="size">The amount of <see cref="MpqHash"/> objects to be added to the <see cref="HashTable"/>.</param>
        internal HashTable(BinaryReader reader, uint size)
            : base(size)
        {
            _mask   = Size - 1;
            _hashes = new MpqHash[Size];

            var hashdata = reader.ReadBytes((int)(size * MpqHash.Size));

            Decrypt(hashdata);

            using (var stream = new MemoryStream(hashdata))
            {
                using (var streamReader = new BinaryReader(stream))
                {
                    for (var i = 0; i < size; i++)
                    {
                        _hashes[i] = new MpqHash(streamReader, _mask);
                    }
                }
            }
        }
Esempio n. 11
0
        /// <summary>
        /// Repairs corrupted values in an <see cref="MpqArchive"/>.
        /// </summary>
        /// <param name="sourceStream">The stream containing the archive that needs to be repaired.</param>
        /// <param name="leaveOpen">If false, the given <paramref name="sourceStream"/> will be disposed at the end of this method.</param>
        /// <returns>A stream containing the repaired archive.</returns>
        public static MemoryStream Restore(Stream sourceStream, bool leaveOpen = false)
        {
            if (sourceStream is null)
            {
                throw new ArgumentNullException(nameof(sourceStream));
            }

            if (!TryLocateMpqHeader(sourceStream, out var mpqHeader, out var headerOffset))
            {
                throw new MpqParserException($"Unable to locate MPQ header.");
            }

            if (mpqHeader.MpqVersion != 0)
            {
                throw new MpqParserException($"MPQ format version {mpqHeader.MpqVersion} is not supported");
            }

            var memoryStream = new MemoryStream();

            using (var writer = new BinaryWriter(memoryStream, new UTF8Encoding(false, true), true))
            {
                // Skip the MPQ header, since its contents will be calculated afterwards.
                writer.Seek((int)MpqHeader.Size, SeekOrigin.Current);

                var archiveSize       = 0U;
                var hashTableEntries  = mpqHeader.HashTableSize;
                var blockTableEntries = mpqHeader.BlockTableSize > MpqTable.MaxSize
                    ? mpqHeader.IsArchiveAfterHeader()
                        ? mpqHeader.BlockTablePosition < mpqHeader.HeaderOffset
                            ? (mpqHeader.HeaderOffset - mpqHeader.BlockTablePosition) / MpqEntry.Size
                            : (uint)(sourceStream.Length - sourceStream.Position) / MpqEntry.Size
                        : throw new MpqParserException($"Unable to determine true BlockTable size.")
                    : mpqHeader.BlockTableSize;

                var hashTable  = (HashTable?)null;
                var blockTable = (BlockTable?)null;

                using (var reader = new BinaryReader(sourceStream, new UTF8Encoding(), true))
                {
                    // Load hash table
                    sourceStream.Seek(mpqHeader.HashTablePosition, SeekOrigin.Begin);
                    hashTable = new HashTable(reader, hashTableEntries);

                    // Load entry table
                    sourceStream.Seek(mpqHeader.BlockTablePosition, SeekOrigin.Begin);
                    blockTable = new BlockTable(reader, blockTableEntries, (uint)headerOffset);

                    // Load archive files
                    for (var i = 0; i < blockTable.Size; i++)
                    {
                        var entry = blockTable[i];
                        if ((entry.Flags & MpqFileFlags.Garbage) == 0)
                        {
                            var size  = entry.CompressedSize;
                            var flags = entry.Flags;

                            if (entry.IsEncrypted && entry.Flags.HasFlag(MpqFileFlags.BlockOffsetAdjustedKey))
                            {
                                // To prevent encryption seed becoming incorrect, save file uncompressed and unencrypted.
                                var pos = sourceStream.Position;
                                using (var mpqStream = new MpqStream(entry, sourceStream, BlockSizeModifier << mpqHeader.BlockSize))
                                {
                                    mpqStream.CopyTo(memoryStream);
                                }

                                sourceStream.Position = pos + size;

                                size  = entry.FileSize;
                                flags = entry.Flags & ~(MpqFileFlags.Compressed | MpqFileFlags.Encrypted | MpqFileFlags.BlockOffsetAdjustedKey);
                            }
                            else
                            {
                                sourceStream.Position = entry.FilePosition;
                                writer.Write(reader.ReadBytes((int)size));
                            }

                            blockTable[i] = new MpqEntry(null, 0, MpqHeader.Size + archiveSize, size, entry.FileSize, flags);
                            archiveSize  += size;
                        }
                        else
                        {
                            blockTable[i] = new MpqEntry(null, 0, MpqHeader.Size + archiveSize, 0, 0, 0);
                        }
                    }
                }

                // Fix invalid block indices and locales.
                for (var i = 0; i < hashTable.Size; i++)
                {
                    var hash = hashTable[i];
                    if (!hash.IsEmpty && !hash.IsDeleted && hash.BlockIndex > BlockTable.MaxSize)
                    {
                        // TODO: don't force neutral locale if another MpqHash exists with the same Name1 and Name2, and that has the neutral locale
                        hashTable[i] = new MpqHash(hash.Name, MpqLocale.Neutral /*hash.Locale & (MpqLocale)0x00000FFF*/, hash.BlockIndex & (BlockTable.MaxSize - 1), hash.Mask);
                    }
                }

                hashTable.SerializeTo(memoryStream);
                blockTable.SerializeTo(memoryStream);

                writer.Seek(0, SeekOrigin.Begin);
                new MpqHeader(archiveSize, hashTableEntries, blockTableEntries, mpqHeader.BlockSize).WriteTo(writer);
            }

            if (!leaveOpen)
            {
                sourceStream.Dispose();
            }

            memoryStream.Position = 0;
            return(memoryStream);
        }
Esempio n. 12
0
 protected override void GetTableEntries(MpqArchive mpqArchive, uint index, uint relativeFileOffset, uint compressedSize, uint fileSize, out MpqEntry mpqEntry, out MpqHash mpqHash)
 {
     throw new NotSupportedException();
 }
Esempio n. 13
0
 protected override void GetTableEntries(MpqArchive mpqArchive, uint index, uint relativeFileOffset, uint compressedSize, uint fileSize, out MpqEntry mpqEntry, out MpqHash mpqHash)
 {
     mpqEntry = new MpqEntry(null, mpqArchive.HeaderOffset, relativeFileOffset, compressedSize, fileSize, TargetFlags);
     mpqHash  = new MpqHash(Name, Locale, index, Mask);
 }