Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/SharpCompress/Compressors/Rar/RarStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public override int Read(byte[] buffer, int offset, int count)
fetch = false;
}
_position += outTotal;
if (count > 0 && outTotal == 0 && _position != Length)
if (count > 0 && outTotal == 0 && _position < Length)
{
// sanity check, eg if we try to decompress a redir entry
throw new InvalidOperationException(
Expand Down Expand Up @@ -179,7 +179,7 @@ System.Threading.CancellationToken cancellationToken
fetch = false;
}
_position += outTotal;
if (count > 0 && outTotal == 0 && _position != Length)
if (count > 0 && outTotal == 0 && _position < Length)
{
// sanity check, eg if we try to decompress a redir entry
throw new InvalidOperationException(
Expand Down
65 changes: 65 additions & 0 deletions tests/SharpCompress.Test/Mocks/TruncatedStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.IO;

namespace SharpCompress.Test.Mocks;

/// <summary>
/// A stream wrapper that truncates the underlying stream after reading a specified number of bytes.
/// Used for testing error handling when streams end prematurely.
/// </summary>
public class TruncatedStream : Stream
{
private readonly Stream baseStream;
private readonly long truncateAfterBytes;
private long bytesRead;

public TruncatedStream(Stream baseStream, long truncateAfterBytes)
{
this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
this.truncateAfterBytes = truncateAfterBytes;
bytesRead = 0;
}

public override bool CanRead => baseStream.CanRead;
public override bool CanSeek => baseStream.CanSeek;
public override bool CanWrite => false;
public override long Length => baseStream.Length;

public override long Position
{
get => baseStream.Position;
set => baseStream.Position = value;
}

public override int Read(byte[] buffer, int offset, int count)
{
if (bytesRead >= truncateAfterBytes)
{
// Simulate premature end of stream
return 0;
}

var maxBytesToRead = (int)Math.Min(count, truncateAfterBytes - bytesRead);
var actualBytesRead = baseStream.Read(buffer, offset, maxBytesToRead);
bytesRead += actualBytesRead;
return actualBytesRead;
}

public override long Seek(long offset, SeekOrigin origin) => baseStream.Seek(offset, origin);

public override void SetLength(long value) => throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();

public override void Flush() => baseStream.Flush();

protected override void Dispose(bool disposing)
{
if (disposing)
{
baseStream?.Dispose();
}
base.Dispose(disposing);
}
}
75 changes: 75 additions & 0 deletions tests/SharpCompress.Test/Rar/RarArchiveTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA.Utilites;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using Xunit;

namespace SharpCompress.Test.Rar;
Expand Down Expand Up @@ -642,4 +644,77 @@ public void Rar_TestEncryptedDetection()
);
Assert.True(passwordProtectedFilesArchive.IsEncrypted);
}

/// <summary>
/// Test for issue: InvalidOperationException when extracting RAR files.
/// This test verifies the fix for the validation logic that was changed from
/// (_position != Length) to (_position &lt; Length).
/// The old logic would throw an exception when position exceeded expected length,
/// but the new logic only throws when decompression ends prematurely (position &lt; expected).
/// </summary>
[Fact]
public void Rar_StreamValidation_OnlyThrowsOnPrematureEnd()
{
// Test normal extraction - should NOT throw InvalidOperationException
// even if actual decompressed size differs from header
var testFiles = new[] { "Rar.rar", "Rar5.rar", "Rar4.rar", "Rar2.rar" };

foreach (var testFile in testFiles)
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
using var archive = RarArchive.Open(stream);

// Extract all entries and read them completely
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
using var ms = new MemoryStream();

// This should complete without throwing InvalidOperationException
// The fix ensures we only throw when position &lt; expected length, not when position >= expected
entryStream.CopyTo(ms);

// Verify we read some data
Assert.True(
ms.Length > 0,
$"Failed to extract data from {entry.Key} in {testFile}"
);
}
}
}

/// <summary>
/// Negative test case: Verifies that InvalidOperationException IS thrown when
/// a RAR stream ends prematurely (position &lt; expected length).
/// This tests the validation condition (_position &lt; Length) works correctly.
/// </summary>
[Fact]
public void Rar_StreamValidation_ThrowsOnTruncatedStream()
{
// This test verifies the exception is thrown when decompression ends prematurely
// by using a truncated stream that stops reading after a small number of bytes
var testFile = "Rar.rar";
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

// Wrap the file stream with a truncated stream that will stop reading early
// This simulates a corrupted or truncated RAR file
using var truncatedStream = new TruncatedStream(fileStream, 1000);

// Opening the archive should work, but extracting should throw
// when we try to read beyond the truncated data
var exception = Assert.Throws<InvalidOperationException>(() =>
{
using var archive = RarArchive.Open(truncatedStream);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
using var ms = new MemoryStream();
// This should throw InvalidOperationException when it can't read all expected bytes
entryStream.CopyTo(ms);
}
});

// Verify the exception message matches our expectation
Assert.Contains("unpacked file size does not match header", exception.Message);
}
}