private byte[] ExtractUncompressedSectoredFile ( [NotNull] BlockTableEntry fileBlockEntry, uint fileKey ) { // This file uses sectoring, but is not compressed. It may be encrypted. var finalSectorSize = fileBlockEntry.GetFileSize() % GetMaxSectorSize(); // All the even sectors you can fit into the file size var sectorCount = (fileBlockEntry.GetFileSize() - finalSectorSize) / GetMaxSectorSize(); var rawSectors = new List <byte[]>(); for (var i = 0; i < sectorCount; ++i) { // Read a normal sector (usually 4096 bytes) rawSectors.Add(_archiveReader.ReadBytes((int)GetMaxSectorSize())); } // And finally, if there's an uneven sector at the end, read that one too if (finalSectorSize > 0) { rawSectors.Add(_archiveReader.ReadBytes((int)finalSectorSize)); } uint sectorIndex = 0; var finalSectors = new List <byte[]>(); foreach (var rawSector in rawSectors) { var pendingSector = rawSector; if (fileBlockEntry.IsEncrypted()) { // Decrypt the block pendingSector = MPQCrypt.DecryptData(rawSector, fileKey + sectorIndex); } finalSectors.Add(pendingSector); ++sectorIndex; } return(StitchSectors(finalSectors)); }
/// <summary> /// Extracts a file which is divided into a set of compressed sectors. /// </summary> /// <param name="fileBlockEntry">The block entry of the file.</param> /// <param name="fileKey">The encryption key of the file.</param> /// <param name="adjustedBlockOffset">The offset to where the file sectors begin.</param> /// <returns>The complete file data.</returns> /// <exception cref="InvalidFileSectorTableException">Thrown if the sector table is found to be inconsistent in any way.</exception> private byte[] ExtractCompressedSectoredFile(BlockTableEntry fileBlockEntry, uint fileKey, long adjustedBlockOffset) { // This file uses sectoring, and is compressed. It may be encrypted. //Retrieve the offsets for each sector - these are relative to the beginning of the data. List <uint> sectorOffsets = ReadFileSectorOffsetTable(fileBlockEntry, fileKey); // Read all of the raw file sectors. List <byte[]> compressedSectors = new List <byte[]>(); for (int i = 0; i < sectorOffsets.Count - 1; ++i) { long sectorStartPosition = adjustedBlockOffset + sectorOffsets[i]; this.ArchiveReader.BaseStream.Position = sectorStartPosition; uint sectorLength = sectorOffsets[i + 1] - sectorOffsets[i]; compressedSectors.Add(this.ArchiveReader.ReadBytes((int)sectorLength)); } // Begin decompressing and decrypting the sectors // TODO: If Checksums are present (check the flags), treat the last sector as a checksum sector // TODO: Check "backup.MPQ/realmlist.wtf" for a weird file with checksums that is not working correctly. // It has a single sector with a single checksum after it, and none of the hashing functions seem to // produce a valid hash. CRC32, Adler32, CRC32B, nothing. // Some flags (listfiles mostly) are flagged as having checksums but don't have a checksum sector. // Perhaps related to attributes file? List <byte[]> decompressedSectors = new List <byte[]>(); /* List<uint> SectorChecksums = new List<uint>(); * if (fileBlockEntry.Flags.HasFlag(BlockFlags.BLF_HasChecksums)) * { * byte[] compressedChecksums = compressedSectors.Last(); * byte[] decompressedChecksums = Compression.DecompressSector(compressedChecksums, fileBlockEntry.Flags); * * // Lift out the last sector and treat it as a checksum sector * using (MemoryStream ms = new MemoryStream(decompressedChecksums)) * { * using (BinaryReader br = new BinaryReader(ms)) * { * // Drop the checksum sector from the file sectors * compressedSectors.RemoveAt(compressedSectors.Count - 1); * * for (int i = 0; i < compressedSectors.Count; ++i) * { * SectorChecksums.Add(br.ReadUInt32()); * } * } * } * }*/ uint sectorIndex = 0; foreach (byte[] compressedSector in compressedSectors) { byte[] pendingSector = compressedSector; if (fileBlockEntry.IsEncrypted()) { // Decrypt the block pendingSector = MPQCrypt.DecryptData(compressedSector, fileKey + sectorIndex); } /*if (fileBlockEntry.Flags.HasFlag(BlockFlags.HasCRCChecksums)) * { * // Verify the sector * bool isSectorIntact = MPQCrypt.VerifySectorChecksum(pendingSector, SectorChecksums[(int)sectorIndex]); * if (!isSectorIntact) * { * using (MemoryStream ms = new MemoryStream(pendingSector)) * { * //DEBUG * * uint sectorChecksum = (uint)Adler32.ComputeChecksum(ms); * * string exceptionMessage = String.Format("The decrypted sector failed its integrity checking. \n" + * "The sector had a checksum of \"{0}\", and the expected one was \"{1}\".", * sectorChecksum, SectorChecksums[(int)sectorIndex]); * * throw new InvalidDataException(exceptionMessage); * } * } * }*/ // Decompress the sector if neccesary if (pendingSector.Length < GetMaxSectorSize()) { int currentFileSize = CountBytesInSectors(decompressedSectors); bool canSectorCompleteFile = currentFileSize + pendingSector.Length == fileBlockEntry.GetFileSize(); if (!canSectorCompleteFile && currentFileSize != fileBlockEntry.GetFileSize()) { pendingSector = Compression.DecompressSector(pendingSector, fileBlockEntry.Flags); } } decompressedSectors.Add(pendingSector); ++sectorIndex; } return(StitchSectors(decompressedSectors)); }
// TODO: Filter files based on language and platform /// <summary> /// Extract the file at <paramref name="filePath"/> from the archive. /// </summary> /// <returns>The file as a byte array, or null if the file could not be found.</returns> /// <param name="filePath">Path to the file in the archive.</param> public byte[] ExtractFile(string filePath) { if (this.IsDisposed) { throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); } // Reset all positions to be safe this.ArchiveReader.BaseStream.Position = 0; HashTableEntry fileHashEntry = this.ArchiveHashTable.FindEntry(filePath); if (fileHashEntry == null) { return(null); } BlockTableEntry fileBlockEntry = this.ArchiveBlockTable.GetEntry((int)fileHashEntry.GetBlockEntryIndex()); // Drop out if the file is not actually a file if (!fileBlockEntry.HasData()) { return(null); } // Seek to the beginning of the file's sectors long adjustedBlockOffset; if (this.Header.GetFormat() >= MPQFormat.ExtendedV1 && RequiresExtendedFormat()) { ushort upperOffsetBits = this.ExtendedBlockTable[(int)fileHashEntry.GetBlockEntryIndex()]; adjustedBlockOffset = (long)fileBlockEntry.GetExtendedBlockOffset(upperOffsetBits); } else { adjustedBlockOffset = fileBlockEntry.GetBlockOffset(); } this.ArchiveReader.BaseStream.Position = adjustedBlockOffset; // Calculate the decryption key if neccesary uint fileKey = MPQCrypt.CreateFileEncryptionKey ( filePath, fileBlockEntry.ShouldEncryptionKeyBeAdjusted(), adjustedBlockOffset, fileBlockEntry.GetFileSize() ); // Examine the file storage types and extract as neccesary if (fileBlockEntry.IsSingleUnit()) { return(ExtractSingleUnitFile(fileBlockEntry, fileKey)); } if (fileBlockEntry.IsCompressed()) { return(ExtractCompressedSectoredFile(fileBlockEntry, fileKey, adjustedBlockOffset)); } return(ExtractUncompressedSectoredFile(fileBlockEntry, fileKey)); }
/// <inheritdoc /> /// <exception cref="ObjectDisposedException">Thrown if the archive has been disposed.</exception> /// <exception cref="FileNotFoundException">Thrown if the archive does not contain a file at the given path.</exception> /// <exception cref="FileDeletedException">Thrown if the file is deleted in the archive.</exception> public byte[] ExtractFile(string filePath) { ThrowIfDisposed(); // Reset all positions to be safe ArchiveReader.BaseStream.Position = 0; HashTableEntry fileHashEntry; try { fileHashEntry = ArchiveHashTable.FindEntry(filePath); } catch (FileNotFoundException fex) { throw new FileNotFoundException("No file found at the given path.", filePath, fex); } BlockTableEntry fileBlockEntry = ArchiveBlockTable.GetEntry((int)fileHashEntry.GetBlockEntryIndex()); // Drop out if the file has been deleted if (fileBlockEntry.IsDeleted()) { throw new FileDeletedException("The given file is deleted.", filePath); } // Seek to the beginning of the file's sectors long adjustedBlockOffset; if (Header.GetFormat() >= MPQFormat.ExtendedV1 && RequiresExtendedFormat()) { ushort upperOffsetBits = ExtendedBlockTable[(int)fileHashEntry.GetBlockEntryIndex()]; adjustedBlockOffset = (long)fileBlockEntry.GetExtendedBlockOffset(upperOffsetBits); } else { adjustedBlockOffset = fileBlockEntry.GetBlockOffset(); } ArchiveReader.BaseStream.Position = adjustedBlockOffset; // Calculate the decryption key if neccesary uint fileKey = MPQCrypt.CreateFileEncryptionKey ( filePath, fileBlockEntry.ShouldEncryptionKeyBeAdjusted(), adjustedBlockOffset, fileBlockEntry.GetFileSize() ); // Examine the file storage types and extract as neccesary if (fileBlockEntry.IsSingleUnit()) { return(ExtractSingleUnitFile(fileBlockEntry, fileKey)); } if (fileBlockEntry.IsCompressed()) { return(ExtractCompressedSectoredFile(fileBlockEntry, fileKey, adjustedBlockOffset)); } return(ExtractUncompressedSectoredFile(fileBlockEntry, fileKey)); }