Files
sharpcompress/src/SharpCompress/Common/ChecksumValidationStream.cs
2026-06-18 17:06:16 +01:00

179 lines
5.0 KiB
C#

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Crypto;
namespace SharpCompress.Common;
internal sealed class ChecksumValidationStream : Stream
{
private readonly Stream _stream;
private readonly ChecksumDescriptor _checksum;
private readonly string _entryName;
private readonly uint[] _crc32Table;
private uint _seed = Crc32Stream.DEFAULT_SEED;
private ushort _crc16;
private bool _validated;
internal ChecksumValidationStream(Stream stream, ChecksumDescriptor checksum, string? entryName)
{
_stream = stream;
_checksum = checksum;
_entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
_crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
}
public override bool CanRead => _stream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _stream.Length;
public override long Position
{
get => _stream.Position;
set => throw new NotSupportedException();
}
public override void Flush() => _stream.Flush();
public override Task FlushAsync(CancellationToken cancellationToken) =>
_stream.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count)
{
var read = _stream.Read(buffer, offset, count);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
return read;
}
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer)
{
var read = _stream.Read(buffer);
UpdateAndValidateAtEof(buffer[..read], read);
return read;
}
#endif
public override int ReadByte() => throw new NotSupportedException();
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
var read = await _stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
return read;
}
#if !LEGACY_DOTNET
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.Span[..read], read);
return read;
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
private void UpdateAndValidateAtEof(ReadOnlySpan<byte> buffer, int read)
{
if (read > 0)
{
UpdateChecksum(buffer);
return;
}
Validate();
}
private void UpdateChecksum(ReadOnlySpan<byte> buffer)
{
switch (_checksum.Kind)
{
case ChecksumKind.Crc32:
case ChecksumKind.Crc32NoFinalXor:
_seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, buffer);
break;
case ChecksumKind.Crc16Arc:
_crc16 = CalculateCrc16Arc(_crc16, buffer);
break;
}
}
private void Validate()
{
if (_validated)
{
return;
}
_validated = true;
switch (_checksum.Kind)
{
case ChecksumKind.Crc32:
ValidateCrc32(finalXor: true);
break;
case ChecksumKind.Crc32NoFinalXor:
ValidateCrc32(finalXor: false);
break;
case ChecksumKind.Crc16Arc:
ValidateCrc16Arc();
break;
}
}
private void ValidateCrc32(bool finalXor)
{
var actual = finalXor ? ~_seed : _seed;
var expected = unchecked((uint)_checksum.ExpectedValue);
if (actual != expected)
{
throw new InvalidFormatException(
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expected:X8}, actual 0x{actual:X8}."
);
}
}
private void ValidateCrc16Arc()
{
var expected = unchecked((ushort)_checksum.ExpectedValue);
if (_crc16 != expected)
{
throw new InvalidFormatException(
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expected:X4}, actual 0x{_crc16:X4}."
);
}
}
private static ushort CalculateCrc16Arc(ushort crc, ReadOnlySpan<byte> buffer)
{
foreach (var value in buffer)
{
crc ^= value;
for (var i = 0; i < 8; i++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
return crc;
}
}