323 lines
10 KiB
C#
323 lines
10 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.InteropServices;
|
|
|
|
using SevenZip.Compression.LZMA;
|
|
|
|
using StringBuilder = System.Text.StringBuilder;
|
|
|
|
namespace InnoPatcher;
|
|
|
|
sealed class InnoSetup {
|
|
private const int BlockSize = 4096 + 4;
|
|
private const int CompressionPropertiesLength = 5;
|
|
|
|
private readonly IInnoHeader _header;
|
|
|
|
private InnoSetup(IInnoHeader header) {
|
|
this._header = header;
|
|
}
|
|
|
|
internal static bool TryParse(Stream file, int innoVersion, [NotNullWhen(true)] out InnoSetup? result) {
|
|
result = null;
|
|
|
|
byte[] headerB = new byte[Marshal.SizeOf<BlocksHeader>()];
|
|
int read, offset = 0;
|
|
while ((read = file.Read(headerB, offset, headerB.Length - offset)) != 0)
|
|
offset += read;
|
|
if (offset < headerB.Length)
|
|
return false;
|
|
|
|
// Read header
|
|
BlocksHeader blocksHeader = MemoryMarshal.Cast<byte, BlocksHeader>(headerB)[0];
|
|
ReadOnlySpan<byte> headerB2 = headerB;
|
|
if (!InnoLoader.VerifyCRC(headerB2.Slice(4), blocksHeader.CRC))
|
|
return false;
|
|
|
|
if (blocksHeader.Compressed != 0 && innoVersion < 4105)
|
|
throw new NotImplementedException("Zlib decompression not implemented.");
|
|
|
|
Stream data = new FixedBlockReadStream(file, blocksHeader.StoredSize);
|
|
if (blocksHeader.Compressed != 0)
|
|
data = new CompressedReadStream(data);
|
|
|
|
// Parse
|
|
InnoSerializer ser = new InnoSerializer();
|
|
IInnoHeader header = CreateHeader(innoVersion);
|
|
ser.Deserialize(data, header);
|
|
|
|
result = new InnoSetup(header);
|
|
return true;
|
|
}
|
|
|
|
public string? AppName => _header.AppName;
|
|
public string? AppVerName => _header.AppVerName;
|
|
public string? AppId => _header.AppId;
|
|
public string? AppCopyright => _header.AppCopyright;
|
|
public string? AppPublisher => _header.AppPublisher;
|
|
public string? AppPublisherURL => _header.AppPublisherURL;
|
|
public string? AppSupportPhone => _header.AppSupportPhone;
|
|
public string? AppSupportURL => _header.AppSupportURL;
|
|
public string? AppUpdatesURL => _header.AppUpdatesURL;
|
|
public string? AppVersion => _header.AppVersion;
|
|
|
|
private static IInnoHeader CreateHeader(int version) {
|
|
if (version >= 6000 && version <= 7000)
|
|
return new InnoHeaderV6();
|
|
else
|
|
throw new NotSupportedException($"Header format for version {version} is not supported.");
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
|
struct BlocksHeader {
|
|
internal uint CRC;
|
|
internal uint StoredSize; // Total bytes written, including the CRCs
|
|
internal byte Compressed; // True if data is compressed, False if not
|
|
}
|
|
|
|
sealed class InnoSerializer {
|
|
private byte[] _buffer = new byte[512];
|
|
|
|
internal void Deserialize(Stream src, IInnoSerializable dst) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0, j = dst.StringCount; i < j; i++) {
|
|
int byteLen = ReadInt32(src);
|
|
sb.Clear();
|
|
sb.EnsureCapacity(byteLen / 2);
|
|
ReadString(src, sb, byteLen);
|
|
dst.SetString(i, sb.ToString());
|
|
}
|
|
|
|
for (int i = 0, j = dst.ByteArrayCount; i < j; i++) {
|
|
int len = ReadInt32(src);
|
|
byte[] value = new byte[len];
|
|
ReadByteArray(src, value, len);
|
|
dst.SetByteArray(i, value);
|
|
}
|
|
|
|
int size = dst.PrimitiveSize;
|
|
if (_buffer.Length < size) {
|
|
int doubleSize = _buffer.Length * 2;
|
|
_buffer = new byte[doubleSize >= size ? doubleSize : size];
|
|
}
|
|
ReadByteArray(src, _buffer, size);
|
|
ReadOnlySpan<byte> buffer = _buffer;
|
|
dst.SetPrimitive(buffer.Slice(0, size));
|
|
}
|
|
|
|
private int ReadInt32(Stream a) {
|
|
int read, offset = 0;
|
|
byte[] buffer = _buffer;
|
|
while (offset < 4 && (read = a.Read(buffer, offset, 4 - offset)) != 0)
|
|
offset += read;
|
|
if (offset < 4)
|
|
throw new EndOfStreamException();
|
|
ReadOnlySpan<byte> buffer2 = buffer;
|
|
return MemoryMarshal.Cast<byte, int>(buffer2.Slice(0, 4))[0];
|
|
}
|
|
|
|
private void ReadString(Stream src, StringBuilder dst, int byteLen) {
|
|
int read;
|
|
byte[] buffer = _buffer;
|
|
ReadOnlySpan<char> bufferC = MemoryMarshal.Cast<byte, char>(buffer);
|
|
bool splitChar = false;
|
|
while (byteLen != 0 && (read = src.Read(buffer, splitChar ? 1 : 0, Math.Min(byteLen, buffer.Length))) != 0) {
|
|
if (splitChar)
|
|
read++;
|
|
dst.Append(bufferC.Slice(0, read / 2));
|
|
|
|
splitChar = (read & 1) != 0;
|
|
if (splitChar) {
|
|
buffer[0] = buffer[read - 1];
|
|
read--;
|
|
}
|
|
|
|
byteLen -= read;
|
|
}
|
|
if (byteLen != 0)
|
|
throw new EndOfStreamException();
|
|
}
|
|
|
|
private void ReadByteArray(Stream src, byte[] dst, int len) {
|
|
int read, offset = 0;
|
|
while (offset < len && (read = src.Read(dst, offset, len - offset)) != 0)
|
|
offset += read;
|
|
if (offset != len)
|
|
throw new EndOfStreamException();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads underlying stream composed of fixed-sized CRC protected blocks.
|
|
/// </summary>
|
|
sealed class FixedBlockReadStream : Stream {
|
|
private readonly Stream _inner;
|
|
private readonly byte[] _buffer;
|
|
private int _bufferOffset;
|
|
private int _bufferLeft;
|
|
private uint _innerLeft;
|
|
|
|
internal FixedBlockReadStream(Stream inner, uint innerLength) {
|
|
this._inner = inner;
|
|
this._innerLeft = innerLength;
|
|
this._buffer = new byte[BlockSize];
|
|
}
|
|
|
|
public override bool CanRead => true;
|
|
public override bool CanSeek => false;
|
|
public override bool CanWrite => false;
|
|
public override long Length => throw new NotSupportedException();
|
|
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
|
|
|
public override int Read(byte[] buffer, int offset, int count) {
|
|
if (buffer == null)
|
|
throw new ArgumentNullException();
|
|
if (offset < 0 || count < 0 || buffer.Length - offset < count)
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
int result = 0;
|
|
while (count > 0) {
|
|
if (_bufferLeft == 0) {
|
|
if (_innerLeft == 0)
|
|
return result;
|
|
ReadBlock();
|
|
}
|
|
|
|
int toCopy = count;
|
|
if (toCopy > _bufferLeft)
|
|
toCopy = _bufferLeft;
|
|
Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy);
|
|
_bufferOffset += toCopy;
|
|
_bufferLeft -= toCopy;
|
|
offset += toCopy;
|
|
count -= toCopy;
|
|
result += toCopy;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public override int ReadByte() {
|
|
if (_bufferLeft == 0) {
|
|
if (_innerLeft == 0)
|
|
return -1;
|
|
ReadBlock();
|
|
}
|
|
|
|
_bufferLeft--;
|
|
return _buffer[_bufferOffset++];
|
|
}
|
|
|
|
private void ReadBlock() {
|
|
if (_bufferLeft != 0)
|
|
throw new InvalidOperationException();
|
|
|
|
int blockSize = _innerLeft > BlockSize ? BlockSize : (int)_innerLeft;
|
|
int read, offset = 0;
|
|
while ((read = _inner.Read(_buffer, offset, blockSize - offset)) != 0)
|
|
offset += read;
|
|
if (offset < blockSize)
|
|
throw new IOException();
|
|
|
|
ReadOnlySpan<byte> blockData = _buffer;
|
|
uint blockCRC = MemoryMarshal.Cast<byte, uint>(blockData.Slice(0, 4))[0];
|
|
blockData = blockData.Slice(4);
|
|
if (!InnoLoader.VerifyCRC(blockData, blockCRC))
|
|
throw new IOException();
|
|
|
|
_bufferOffset = 4; // skip CRC
|
|
_bufferLeft = blockSize - 4; // minus CRC
|
|
}
|
|
|
|
public override void Write(byte[] buffer, int offset, int count) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override void SetLength(long value) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override long Seek(long offset, SeekOrigin origin) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override void Flush() {
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads underlying LZMA compressed stream and decompresses it.
|
|
/// </summary>
|
|
sealed class CompressedReadStream : Stream {
|
|
private readonly Stream _inner;
|
|
private readonly Decoder _decoder;
|
|
private byte[]? _singleByteBuffer;
|
|
private MemoryStream? _singleByteStream;
|
|
private long _outOffset;
|
|
|
|
internal CompressedReadStream(Stream inner) {
|
|
this._inner = inner;
|
|
this._decoder = new Decoder();
|
|
|
|
byte[] props = new byte[CompressionPropertiesLength];
|
|
int read, offset = 0;
|
|
while ((read = inner.Read(props, offset, props.Length - offset)) != 0)
|
|
offset += read;
|
|
if (offset != props.Length)
|
|
throw new IOException();
|
|
_decoder.SetDecoderProperties(props);
|
|
|
|
_decoder.Code(inner, Stream.Null, -1, 0, null, noInit: false, isLast: false);
|
|
}
|
|
|
|
public override bool CanRead => true;
|
|
public override bool CanSeek => false;
|
|
public override bool CanWrite => false;
|
|
public override long Length => throw new NotSupportedException();
|
|
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
|
|
|
public override int Read(byte[] buffer, int offset, int count) {
|
|
if (buffer == null)
|
|
throw new ArgumentNullException();
|
|
if (offset < 0 || count < 0 || buffer.Length - offset < count)
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
MemoryStream result = new MemoryStream(buffer, offset, count, writable: true);
|
|
_decoder.Code(_inner, result, -1, _outOffset + count, null, noInit: true, isLast: false);
|
|
int read = (int)result.Position;
|
|
_outOffset += read;
|
|
return read;
|
|
}
|
|
|
|
public override int ReadByte() {
|
|
if (_singleByteBuffer == null || _singleByteStream == null) {
|
|
_singleByteBuffer = new byte[1];
|
|
_singleByteStream = new MemoryStream(_singleByteBuffer, 0, 1, writable: true);
|
|
}
|
|
_singleByteStream.SetLength(0);
|
|
|
|
_decoder.Code(_inner, _singleByteStream, -1, _outOffset + 1, null, noInit: true, isLast: false);
|
|
int read = (int)_singleByteStream.Position;
|
|
_outOffset += read;
|
|
|
|
return read == 0 ? -1 : _singleByteBuffer[0];
|
|
}
|
|
|
|
public override void Write(byte[] buffer, int offset, int count) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override void SetLength(long value) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override long Seek(long offset, SeekOrigin origin) {
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override void Flush() {
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
}
|