mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-02 17:27:23 -04:00
375 lines
13 KiB
C#
375 lines
13 KiB
C#
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using static OpenDiablo2.Common.Models.MPQ;
|
|
|
|
|
|
namespace OpenDiablo2.Common.Models
|
|
{
|
|
public sealed class MPQStream : Stream
|
|
{
|
|
private readonly MPQ mpq;
|
|
private readonly BlockRecord blockRecord;
|
|
private uint[] blockPositions;
|
|
private long position;
|
|
private byte[] _currentData;
|
|
private int _currentBlockIndex = -1;
|
|
private int blockSize;
|
|
|
|
internal MPQStream(MPQ mpq, BlockRecord blockRecord)
|
|
{
|
|
this.mpq = mpq;
|
|
this.blockRecord = blockRecord;
|
|
this.blockSize = 0x200 << mpq.Header.BlockSize;
|
|
|
|
if (blockRecord.IsCompressed && !blockRecord.SingleUnit)
|
|
LoadBlockOffsets();
|
|
|
|
}
|
|
|
|
private void LoadBlockOffsets()
|
|
{
|
|
|
|
int blockposcount = (int)((blockRecord.FileSize + blockSize - 1) / blockSize) + 1;
|
|
blockPositions = new uint[blockposcount];
|
|
|
|
lock (mpq.fileStream)
|
|
{
|
|
mpq.fileStream.Seek(blockRecord.BlockOffset, SeekOrigin.Begin);
|
|
var br = new BinaryReader(mpq.fileStream);
|
|
for (int i = 0; i < blockposcount; i++)
|
|
blockPositions[i] = br.ReadUInt32();
|
|
|
|
}
|
|
|
|
uint blockpossize = (uint)blockposcount * 4;
|
|
|
|
if (blockRecord.IsEncrypted)
|
|
{
|
|
MPQ.DecryptBlock(blockPositions, blockRecord.EncryptionSeed - 1);
|
|
|
|
if (blockPositions[0] != blockpossize)
|
|
throw new ApplicationException("Decryption failed");
|
|
if (blockPositions[1] > blockSize + blockpossize)
|
|
throw new ApplicationException("Decryption failed");
|
|
}
|
|
|
|
}
|
|
|
|
public override bool CanRead => true;
|
|
|
|
public override bool CanSeek => true;
|
|
|
|
public override bool CanWrite => false;
|
|
|
|
public override long Length => blockRecord.FileSize;
|
|
|
|
public override long Position
|
|
{
|
|
get => position;
|
|
set => Seek(value, SeekOrigin.Begin);
|
|
}
|
|
|
|
public override void Flush() { }
|
|
|
|
public override int Read(byte[] buffer, int offset, int count)
|
|
{
|
|
if (blockRecord.SingleUnit)
|
|
return ReadInternalSingleUnit(buffer, offset, count);
|
|
|
|
int toread = count;
|
|
int readtotal = 0;
|
|
|
|
while (toread > 0)
|
|
{
|
|
int read = ReadInternal(buffer, offset, toread);
|
|
if (read == 0) break;
|
|
readtotal += read;
|
|
offset += read;
|
|
toread -= read;
|
|
}
|
|
return readtotal;
|
|
}
|
|
|
|
private int ReadInternalSingleUnit(byte[] buffer, int offset, int count)
|
|
{
|
|
if (position >= Length)
|
|
return 0;
|
|
|
|
if (_currentData == null)
|
|
LoadSingleUnit();
|
|
|
|
int bytestocopy = Math.Min((int)(_currentData.Length - position), count);
|
|
|
|
Array.Copy(_currentData, position, buffer, offset, bytestocopy);
|
|
|
|
position += bytestocopy;
|
|
return bytestocopy;
|
|
}
|
|
|
|
private void LoadSingleUnit()
|
|
{
|
|
// Read the entire file into memory
|
|
byte[] filedata = new byte[blockSize];
|
|
lock (mpq.fileStream)
|
|
{
|
|
mpq.fileStream.Seek(mpq.Header.HeaderSize + blockRecord.BlockOffset, SeekOrigin.Begin);
|
|
int read = mpq.fileStream.Read(filedata, 0, filedata.Length);
|
|
if (read != filedata.Length)
|
|
throw new ApplicationException("Insufficient data or invalid data length");
|
|
}
|
|
|
|
if (blockSize == blockRecord.FileSize)
|
|
_currentData = filedata;
|
|
else
|
|
_currentData = DecompressMulti(filedata, (int)blockRecord.FileSize);
|
|
}
|
|
|
|
private int ReadInternal(byte[] buffer, int offset, int count)
|
|
{
|
|
// OW: avoid reading past the contents of the file
|
|
if (position >= Length)
|
|
return 0;
|
|
|
|
BufferData();
|
|
|
|
int localposition = (int)(position % blockSize);
|
|
int bytestocopy = Math.Min(_currentData.Length - localposition, count);
|
|
if (bytestocopy <= 0) return 0;
|
|
|
|
Array.Copy(_currentData, localposition, buffer, offset, bytestocopy);
|
|
|
|
position += bytestocopy;
|
|
return bytestocopy;
|
|
}
|
|
|
|
public override int ReadByte()
|
|
{
|
|
if (position >= Length) return -1;
|
|
|
|
if (blockRecord.SingleUnit)
|
|
return ReadByteSingleUnit();
|
|
|
|
BufferData();
|
|
|
|
int localposition = (int)(position % blockSize);
|
|
position++;
|
|
return _currentData[localposition];
|
|
}
|
|
|
|
private int ReadByteSingleUnit()
|
|
{
|
|
if (_currentData == null)
|
|
LoadSingleUnit();
|
|
|
|
return _currentData[position++];
|
|
}
|
|
|
|
private void BufferData()
|
|
{
|
|
int requiredblock = (int)(position / blockSize);
|
|
if (requiredblock != _currentBlockIndex)
|
|
{
|
|
int expectedlength = (int)Math.Min(Length - (requiredblock * blockSize), blockSize);
|
|
_currentData = LoadBlock(requiredblock, expectedlength);
|
|
_currentBlockIndex = requiredblock;
|
|
}
|
|
}
|
|
|
|
private byte[] LoadBlock(int blockIndex, int expectedLength)
|
|
{
|
|
uint offset;
|
|
int toread;
|
|
uint encryptionseed;
|
|
|
|
if (blockRecord.IsCompressed)
|
|
{
|
|
offset = blockPositions[blockIndex];
|
|
toread = (int)(blockPositions[blockIndex + 1] - offset);
|
|
}
|
|
else
|
|
{
|
|
offset = (uint)(blockIndex * blockSize);
|
|
toread = expectedLength;
|
|
}
|
|
offset += blockRecord.BlockOffset;
|
|
|
|
byte[] data = new byte[toread];
|
|
lock (mpq.fileStream)
|
|
{
|
|
mpq.fileStream.Seek(offset, SeekOrigin.Begin);
|
|
int read = mpq.fileStream.Read(data, 0, toread);
|
|
if (read != toread)
|
|
throw new ApplicationException("Insufficient data or invalid data length");
|
|
}
|
|
|
|
if (blockRecord.IsEncrypted && blockRecord.FileSize > 3)
|
|
{
|
|
if (blockRecord.EncryptionSeed == 0)
|
|
throw new ApplicationException("Unable to determine encryption key");
|
|
|
|
encryptionseed = (uint)(blockIndex + blockRecord.EncryptionSeed);
|
|
MPQ.DecryptBlock(data, encryptionseed);
|
|
}
|
|
|
|
if (blockRecord.IsCompressed && (toread != expectedLength))
|
|
{
|
|
//if ((blockRecord.Flags & MpqFileFlags.CompressedMulti) != 0)
|
|
if (!blockRecord.SingleUnit)
|
|
data = DecompressMulti(data, expectedLength);
|
|
else
|
|
data = PKDecompress(new MemoryStream(data), expectedLength);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
private static byte[] DecompressMulti(byte[] input, int outputLength)
|
|
{
|
|
Stream sinput = new MemoryStream(input);
|
|
|
|
byte comptype = (byte)sinput.ReadByte();
|
|
|
|
switch (comptype)
|
|
{
|
|
case 1: // Huffman
|
|
return MpqHuffman.Decompress(sinput).ToArray();
|
|
case 2: // ZLib/Deflate
|
|
return ZlibDecompress(sinput, outputLength);
|
|
case 8: // PKLib/Impode
|
|
return PKDecompress(sinput, outputLength);
|
|
case 0x10: // BZip2
|
|
return BZip2Decompress(sinput, outputLength);
|
|
case 0x80: // IMA ADPCM Stereo
|
|
return MpqWavCompression.Decompress(sinput, 2);
|
|
case 0x40: // IMA ADPCM Mono
|
|
return MpqWavCompression.Decompress(sinput, 1);
|
|
|
|
case 0x12:
|
|
throw new ApplicationException("LZMA compression is not yet supported");
|
|
// Combos
|
|
case 0x22:
|
|
// TODO: sparse then zlib
|
|
throw new ApplicationException("Sparse compression + Deflate compression is not yet supported");
|
|
case 0x30:
|
|
// TODO: sparse then bzip2
|
|
throw new ApplicationException("Sparse compression + BZip2 compression is not yet supported");
|
|
case 0x41:
|
|
sinput = MpqHuffman.Decompress(sinput);
|
|
return MpqWavCompression.Decompress(sinput, 1);
|
|
case 0x48:
|
|
{
|
|
byte[] result = PKDecompress(sinput, outputLength);
|
|
return MpqWavCompression.Decompress(new MemoryStream(result), 1);
|
|
}
|
|
case 0x81:
|
|
sinput = MpqHuffman.Decompress(sinput);
|
|
return MpqWavCompression.Decompress(sinput, 2);
|
|
case 0x88:
|
|
{
|
|
byte[] result = PKDecompress(sinput, outputLength);
|
|
return MpqWavCompression.Decompress(new MemoryStream(result), 2);
|
|
}
|
|
default:
|
|
throw new ApplicationException("Compression is not yet supported: 0x" + comptype.ToString("X"));
|
|
}
|
|
}
|
|
|
|
private static byte[] BZip2Decompress(Stream data, int expectedLength)
|
|
{
|
|
using (var output = new MemoryStream(expectedLength))
|
|
{
|
|
new Ionic.BZip2.BZip2InputStream(data)
|
|
.CopyTo(output);
|
|
return output.ToArray();
|
|
}
|
|
}
|
|
|
|
private static byte[] PKDecompress(Stream data, int expectedLength)
|
|
{
|
|
PKLibDecompress pk = new PKLibDecompress(data);
|
|
return pk.Explode(expectedLength);
|
|
}
|
|
|
|
private static byte[] ZlibDecompress(Stream data, int expectedLength)
|
|
{
|
|
// This assumes that Zlib won't be used in combination with another compression type
|
|
byte[] Output = new byte[expectedLength];
|
|
Stream s = new InflaterInputStream(data);
|
|
int Offset = 0;
|
|
while (expectedLength > 0)
|
|
{
|
|
int size = s.Read(Output, Offset, expectedLength);
|
|
if (size == 0) break;
|
|
Offset += size;
|
|
expectedLength -= size;
|
|
}
|
|
return Output;
|
|
}
|
|
|
|
public override long Seek(long offset, SeekOrigin origin)
|
|
{
|
|
long target;
|
|
|
|
switch (origin)
|
|
{
|
|
case SeekOrigin.Begin:
|
|
target = offset;
|
|
break;
|
|
case SeekOrigin.Current:
|
|
target = Position + offset;
|
|
break;
|
|
case SeekOrigin.End:
|
|
target = Length + offset;
|
|
break;
|
|
default:
|
|
throw new ArgumentException("Origin", "Invalid SeekOrigin");
|
|
}
|
|
|
|
if (target < 0)
|
|
throw new ArgumentOutOfRangeException("Attmpted to Seek before the beginning of the stream");
|
|
if (target >= Length)
|
|
throw new ArgumentOutOfRangeException("Attmpted to Seek beyond the end of the stream");
|
|
|
|
position = target;
|
|
|
|
return position;
|
|
}
|
|
|
|
internal static uint DetectFileSeed(uint value0, uint value1, uint decrypted)
|
|
{
|
|
uint temp = (value0 ^ decrypted) - 0xeeeeeeee;
|
|
|
|
for (int i = 0; i < 0x100; i++)
|
|
{
|
|
uint seed1 = temp - MPQ.cryptTable[0x400 + i];
|
|
uint seed2 = 0xeeeeeeee + MPQ.cryptTable[0x400 + (seed1 & 0xff)];
|
|
uint result = value0 ^ (seed1 + seed2);
|
|
|
|
if (result != decrypted)
|
|
continue;
|
|
|
|
uint saveseed1 = seed1;
|
|
|
|
// Test this result against the 2nd value
|
|
seed1 = ((~seed1 << 21) + 0x11111111) | (seed1 >> 11);
|
|
seed2 = result + seed2 + (seed2 << 5) + 3;
|
|
|
|
seed2 += MPQ.cryptTable[0x400 + (seed1 & 0xff)];
|
|
result = value1 ^ (seed1 + seed2);
|
|
|
|
if ((result & 0xfffc0000) == 0)
|
|
return saveseed1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public override void SetLength(long value) => throw new NotImplementedException();
|
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
|
}
|
|
}
|