Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,7 @@
<data name="IO_SeekBeforeBegin" xml:space="preserve">
<value>An attempt was made to move the position before the beginning of the stream.</value>
</data>
<data name="CrcMismatch" xml:space="preserve">
<value>The CRC32 checksum of the extracted data does not match the expected value from the archive.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -831,17 +831,19 @@ private Stream GetDataDecompressor(Stream compressedStreamToRead)
return uncompressedStream;
}

private Stream OpenInReadMode(bool checkOpenable)
private CrcValidatingReadStream OpenInReadMode(bool checkOpenable)
{
if (checkOpenable)
ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: false);
return OpenInReadModeGetDataCompressor(GetOffsetOfCompressedData());
}

private Stream OpenInReadModeGetDataCompressor(long offsetOfCompressedData)
private CrcValidatingReadStream OpenInReadModeGetDataCompressor(long offsetOfCompressedData)
{
Stream compressedStream = new SubReadStream(_archive.ArchiveStream, offsetOfCompressedData, _compressedSize);
return GetDataDecompressor(compressedStream);
Stream decompressedStream = GetDataDecompressor(compressedStream);

return new CrcValidatingReadStream(decompressedStream, _crc32, _uncompressedSize);
}

private WrappedStream OpenInWriteMode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,4 +712,182 @@ public override async ValueTask DisposeAsync()
await base.DisposeAsync().ConfigureAwait(false);
}
}

internal sealed class CrcValidatingReadStream : Stream
{
private readonly Stream _baseStream;
private uint _runningCrc;
private readonly uint _expectedCrc;
private long _totalBytesRead;
private readonly long _expectedLength;
private bool _isDisposed;
private bool _crcValidated;
private bool _crcAbandoned;

public CrcValidatingReadStream(Stream baseStream, uint expectedCrc, long expectedLength)
{
_baseStream = baseStream;
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _runningCrc field should be explicitly initialized to 0 in the constructor for clarity, even though the default value for uint is 0. This makes the intent clear that CRC32 calculation starts with an initial value of 0, consistent with the pattern used in CheckSumAndSizeWriteStream (line 503).

Suggested change
_baseStream = baseStream;
_baseStream = baseStream;
_runningCrc = 0;

Copilot uses AI. Check for mistakes.
_expectedCrc = expectedCrc;
_expectedLength = expectedLength;
}
Comment on lines +727 to +732
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating that expectedLength is non-negative in the constructor. Negative values don't make sense for a length and could cause unexpected behavior in the comparison at line 808 and 822.

Copilot uses AI. Check for mistakes.
Comment on lines +727 to +732
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor should validate that baseStream is not null. Without this check, a null baseStream would result in a NullReferenceException when any stream operation is attempted, rather than an ArgumentNullException at construction time.

Copilot uses AI. Check for mistakes.

public override bool CanRead => !_isDisposed && _baseStream.CanRead;
public override bool CanSeek => !_isDisposed && _baseStream.CanSeek;
public override bool CanWrite => false;

public override long Length => _baseStream.Length;

public override long Position
{
get => _baseStream.Position;
set
{
ThrowIfDisposed();
ThrowIfCantSeek();

_crcAbandoned = true;
_baseStream.Position = value;
}
}

public override int Read(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
ValidateBufferArguments(buffer, offset, count);

int bytesRead = _baseStream.Read(buffer, offset, count);
ProcessBytesRead(buffer.AsSpan(offset, bytesRead));

return bytesRead;
}

public override int Read(Span<byte> buffer)
{
ThrowIfDisposed();

int bytesRead = _baseStream.Read(buffer);
ProcessBytesRead(buffer.Slice(0, bytesRead));

return bytesRead;
}

public override int ReadByte()
{
byte b = default;
return Read(new Span<byte>(ref b)) == 1 ? b : -1;
}

public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ThrowIfDisposed();
ValidateBufferArguments(buffer, offset, count);

int bytesRead = await _baseStream.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
ProcessBytesRead(buffer.AsSpan(offset, bytesRead));

return bytesRead;
}

public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();

int bytesRead = await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
ProcessBytesRead(buffer.Span.Slice(0, bytesRead));

return bytesRead;
}

private void ProcessBytesRead(ReadOnlySpan<byte> data)
{
if (data.Length > 0 && !_crcAbandoned)
{
_runningCrc = Crc32Helper.UpdateCrc32(_runningCrc, data);
_totalBytesRead += data.Length;

if (_totalBytesRead >= _expectedLength)
{
ValidateCrc();
}
Comment on lines +808 to +811
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should check for equality (_totalBytesRead == _expectedLength) instead of greater-than-or-equal. The current implementation allows ValidateCrc() to be called on every read after _expectedLength is reached, even though it will only validate once when exactly equal. Using equality would ensure validation happens exactly once and makes the logic clearer. Additionally, if _totalBytesRead somehow exceeds _expectedLength (which shouldn't happen given the decompressor limits), it would be better to detect and handle this case explicitly.

Suggested change
if (_totalBytesRead >= _expectedLength)
{
ValidateCrc();
}
if (_totalBytesRead == _expectedLength)
{
ValidateCrc();
}
else if (_totalBytesRead > _expectedLength)
{
throw new InvalidDataException(SR.CrcMismatch);
}

Copilot uses AI. Check for mistakes.
}
}

private void ValidateCrc()
{
if (_crcValidated)
return;

_crcValidated = true;

if (_totalBytesRead == _expectedLength && _runningCrc != _expectedCrc)
{
throw new InvalidDataException(SR.CrcMismatch);
}
}

public override void Write(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
throw new NotSupportedException(SR.WritingNotSupported);
}

public override void Flush()
{
ThrowIfDisposed();
throw new NotSupportedException(SR.WritingNotSupported);
}

public override Task FlushAsync(CancellationToken cancellationToken)
{
ThrowIfDisposed();
throw new NotSupportedException(SR.WritingNotSupported);
}

public override long Seek(long offset, SeekOrigin origin)
{
ThrowIfDisposed();
ThrowIfCantSeek();

_crcAbandoned = true;

return _baseStream.Seek(offset, origin);
}

public override void SetLength(long value)
{
ThrowIfDisposed();
throw new NotSupportedException(SR.SetLengthRequiresSeekingAndWriting);
}

private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_isDisposed, this);
}

private void ThrowIfCantSeek()
{
if (!CanSeek)
throw new NotSupportedException(SR.SeekingNotSupported);
}

protected override void Dispose(bool disposing)
{
if (disposing && !_isDisposed)
{
_baseStream.Dispose();
_isDisposed = true;
}
base.Dispose(disposing);
}

public override async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
await _baseStream.DisposeAsync().ConfigureAwait(false);
_isDisposed = true;
}
await base.DisposeAsync().ConfigureAwait(false);
}
}
}
Loading
Loading