1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-02 17:27:23 -04:00
OpenDiablo2/OpenDiablo2.Common/Models/MPQStream.cs
2018-11-22 00:18:42 -05:00

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();
}
}