/// <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); }
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); }
/// <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); } } } }
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); } }
/// <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; }
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; }
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); }
/// <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; }
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; } } }
/// <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); } } } }
/// <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); }
protected override void GetTableEntries(MpqArchive mpqArchive, uint index, uint relativeFileOffset, uint compressedSize, uint fileSize, out MpqEntry mpqEntry, out MpqHash mpqHash) { throw new NotSupportedException(); }
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); }