Added ARJ's compressmion method4 (compressed fastest).

Refactored TestBase to support archives with mixed compression algorithms.Merge branch 'adamhathcock:master' into master
This commit is contained in:
Twan
2025-10-31 14:33:14 +01:00
committed by Twan van Dongen
23 changed files with 866 additions and 76 deletions

View File

@@ -20,7 +20,7 @@ public static class ArchiveFactory
public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions();
stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize);
stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
}

View File

@@ -25,7 +25,17 @@ namespace SharpCompress.Common.Arj
public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0;
public override CompressionType CompressionType => CompressionType.None;
public override CompressionType CompressionType
{
get
{
if (_filePart.Header.CompressionMethod == CompressionMethod.Stored)
{
return CompressionType.None;
}
return CompressionType.ArjLZ77;
}
}
public override long Size => _filePart?.Header.OriginalSize ?? 0;
@@ -39,7 +49,7 @@ namespace SharpCompress.Common.Arj
public override bool IsEncrypted => false;
public override bool IsDirectory => _filePart.Header.FileType == (int)FileType.Directory;
public override bool IsDirectory => _filePart.Header.FileType == FileType.Directory;
public override bool IsSplitAfter => false;

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Compressors.Arj;
using SharpCompress.IO;
namespace SharpCompress.Common.Arj
@@ -37,6 +38,18 @@ namespace SharpCompress.Common.Arj
Header.CompressedSize
);
break;
case CompressionMethod.CompressedFastest:
byte[] compressedData = new byte[Header.CompressedSize];
_stream.Position = Header.DataStartPosition;
_stream.Read(compressedData, 0, compressedData.Length);
byte[] decompressedData = LHDecoder.DecodeFastest(
compressedData,
(int)Header.OriginalSize // ARJ can only handle files up to 2GB, so casting to int should not be an issue.
);
compressedStream = new MemoryStream(decompressedData);
break;
default:
throw new NotSupportedException(
"CompressionMethod: " + Header.CompressionMethod

View File

@@ -26,6 +26,8 @@ namespace SharpCompress.Common.Arj.Headers
}
public ArjHeaderType ArjHeaderType { get; }
public byte Flags { get; set; }
public FileType FileType { get; set; }
public abstract ArjHeader? Read(Stream reader);
@@ -34,23 +36,31 @@ namespace SharpCompress.Common.Arj.Headers
// check for magic bytes
Span<byte> magic = stackalloc byte[2];
if (stream.Read(magic) != 2)
{
return Array.Empty<byte>();
}
var magicValue = (ushort)(magic[0] | magic[1] << 8);
if (magicValue != ARJ_MAGIC)
{
throw new InvalidDataException("Not an ARJ file (wrong magic bytes)");
}
// read header_size
byte[] headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8);
if (headerSize < 1)
{
return Array.Empty<byte>();
}
var body = new byte[headerSize];
var read = stream.Read(body, 0, headerSize);
if (read < headerSize)
{
return Array.Empty<byte>();
}
byte[] crc = new byte[4];
read = stream.Read(crc, 0, 4);
@@ -111,5 +121,22 @@ namespace SharpCompress.Common.Arj.Headers
extendedHeader.Add(header);
}
}
// Flag helpers
public bool IsGabled => (Flags & 0x01) != 0;
public bool IsAnsiPage => (Flags & 0x02) != 0;
public bool IsVolume => (Flags & 0x04) != 0;
public bool IsArjProtected => (Flags & 0x08) != 0;
public bool IsPathSym => (Flags & 0x10) != 0;
public bool IsBackup => (Flags & 0x20) != 0;
public bool IsSecured => (Flags & 0x40) != 0;
public bool IsAltName => (Flags & 0x80) != 0;
public static FileType FileTypeFromByte(byte value)
{
return Enum.IsDefined(typeof(FileType), value)
? (FileType)value
: Headers.FileType.Unknown;
}
}
}

View File

@@ -16,9 +16,7 @@ namespace SharpCompress.Common.Arj.Headers
public byte ArchiverVersionNumber { get; set; }
public byte MinVersionToExtract { get; set; }
public HostOS HostOS { get; set; }
public byte ArjFlags { get; set; }
public CompressionMethod CompressionMethod { get; set; }
public int FileType { get; set; }
public DosDateTime DateTimeModified { get; set; } = new DosDateTime(0);
public long CompressedSize { get; set; }
public long OriginalSize { get; set; }
@@ -36,11 +34,6 @@ namespace SharpCompress.Common.Arj.Headers
private const byte StdHdrSize = 30;
private const byte R9HdrSize = 46;
public bool IsGarbled => (ArjFlags & 0x01) != 0;
public bool IsVolume => (ArjFlags & 0x04) != 0;
public bool IsExtFile => (ArjFlags & 0x08) != 0;
public bool IsPathSym => (ArjFlags & 0x10) != 0;
public bool IsBackup => (ArjFlags & 0x20) != 0;
public ArjLocalHeader(ArchiveEncoding archiveEncoding)
: base(ArjHeaderType.LocalHeader)
@@ -92,12 +85,12 @@ namespace SharpCompress.Common.Arj.Headers
}
byte headerSize = headerBytes[offset++];
byte archiverVersionNumber = headerBytes[offset++];
byte minVersionToExtract = headerBytes[offset++];
ArchiverVersionNumber = headerBytes[offset++];
MinVersionToExtract = headerBytes[offset++];
HostOS hostOS = (HostOS)headerBytes[offset++];
byte arjFlags = headerBytes[offset++];
CompressionMethod compressionMethod = CompressionMethodFromByte(headerBytes[offset++]);
FileType fileType = FileTypeFromByte(headerBytes[offset++]);
Flags = headerBytes[offset++];
CompressionMethod = CompressionMethodFromByte(headerBytes[offset++]);
FileType = FileTypeFromByte(headerBytes[offset++]);
offset++; // Skip 1 byte
@@ -164,12 +157,5 @@ namespace SharpCompress.Common.Arj.Headers
_ => CompressionMethod.Unknown,
};
}
public static FileType FileTypeFromByte(byte value)
{
return Enum.IsDefined(typeof(FileType), value)
? (FileType)value
: Headers.FileType.Unknown;
}
}
}

View File

@@ -16,9 +16,7 @@ namespace SharpCompress.Common.Arj.Headers
public int ArchiverVersionNumber { get; private set; }
public int MinVersionToExtract { get; private set; }
public HostOS HostOs { get; private set; }
public int Flags { get; private set; }
public int SecurityVersion { get; private set; }
public int FileType { get; private set; }
public DosDateTime CreationDateTime { get; private set; } = new DosDateTime(0);
public long CompressedSize { get; private set; }
public long ArchiveSize { get; private set; }
@@ -51,13 +49,13 @@ namespace SharpCompress.Common.Arj.Headers
{
var offset = 1;
int ReadByte()
byte ReadByte()
{
if (offset >= headerBytes.Length)
{
throw new EndOfStreamException();
}
return headerBytes[offset++] & 0xFF;
return (byte)(headerBytes[offset++] & 0xFF);
}
int ReadInt16()
@@ -116,7 +114,7 @@ namespace SharpCompress.Common.Arj.Headers
Flags = ReadByte();
SecurityVersion = ReadByte();
FileType = ReadByte();
FileType = FileTypeFromByte(ReadByte());
offset++; // skip reserved
@@ -136,15 +134,5 @@ namespace SharpCompress.Common.Arj.Headers
return this;
}
// Flag helpers
public bool IsGabled => (Flags & 0x01) != 0;
public bool IsAnsiPage => (Flags & 0x02) != 0;
public bool IsVolume => (Flags & 0x04) != 0;
public bool IsArjProtected => (Flags & 0x08) != 0;
public bool IsPathSym => (Flags & 0x10) != 0;
public bool IsBackup => (Flags & 0x20) != 0;
public bool IsSecured => (Flags & 0x40) != 0;
public bool IsAltName => (Flags & 0x80) != 0;
}
}

View File

@@ -29,4 +29,5 @@ public enum CompressionType
Crushed,
Distilled,
ZStandard,
ArjLZ77,
}

View File

@@ -24,10 +24,29 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Compressors.ADC;
/// <summary>
/// Result of an ADC decompression operation
/// </summary>
public class AdcDecompressResult
{
/// <summary>
/// Number of bytes read from input
/// </summary>
public int BytesRead { get; set; }
/// <summary>
/// Decompressed output buffer
/// </summary>
public byte[]? Output { get; set; }
}
/// <summary>
/// Provides static methods for decompressing Apple Data Compression data
/// </summary>
@@ -78,6 +97,173 @@ public static class ADCBase
public static int Decompress(byte[] input, out byte[]? output, int bufferSize = 262144) =>
Decompress(new MemoryStream(input), out output, bufferSize);
/// <summary>
/// Decompresses a byte buffer asynchronously that's compressed with ADC
/// </summary>
/// <param name="input">Compressed buffer</param>
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async Task<AdcDecompressResult> DecompressAsync(
byte[] input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
) => await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken);
/// <summary>
/// Decompresses a stream asynchronously that's compressed with ADC
/// </summary>
/// <param name="input">Stream containing compressed data</param>
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async Task<AdcDecompressResult> DecompressAsync(
Stream input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
)
{
var result = new AdcDecompressResult();
if (input is null || input.Length == 0)
{
result.BytesRead = 0;
result.Output = null;
return result;
}
var start = (int)input.Position;
var position = (int)input.Position;
int chunkSize;
int offset;
int chunkType;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var outPosition = 0;
var full = false;
byte[] temp = ArrayPool<byte>.Shared.Rent(3);
try
{
while (position < input.Length)
{
cancellationToken.ThrowIfCancellationRequested();
var readByte = input.ReadByte();
if (readByte == -1)
{
break;
}
chunkType = GetChunkType((byte)readByte);
switch (chunkType)
{
case PLAIN:
chunkSize = GetChunkSize((byte)readByte);
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
var readCount = await input.ReadAsync(
buffer,
outPosition,
chunkSize,
cancellationToken
);
outPosition += readCount;
position += readCount + 1;
break;
case TWO_BYTE:
chunkSize = GetChunkSize((byte)readByte);
temp[0] = (byte)readByte;
temp[1] = (byte)input.ReadByte();
offset = GetOffset(temp.AsSpan(0, 2));
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
if (offset == 0)
{
var lastByte = buffer[outPosition - 1];
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = lastByte;
outPosition++;
}
position += 2;
}
else
{
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = buffer[outPosition - offset - 1];
outPosition++;
}
position += 2;
}
break;
case THREE_BYTE:
chunkSize = GetChunkSize((byte)readByte);
temp[0] = (byte)readByte;
temp[1] = (byte)input.ReadByte();
temp[2] = (byte)input.ReadByte();
offset = GetOffset(temp.AsSpan(0, 3));
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
if (offset == 0)
{
var lastByte = buffer[outPosition - 1];
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = lastByte;
outPosition++;
}
position += 3;
}
else
{
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = buffer[outPosition - offset - 1];
outPosition++;
}
position += 3;
}
break;
}
if (full)
{
break;
}
}
var output = new byte[outPosition];
Array.Copy(buffer, output, outPosition);
result.BytesRead = position - start;
result.Output = output;
return result;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
ArrayPool<byte>.Shared.Return(temp);
}
}
/// <summary>
/// Decompresses a stream that's compressed with ADC
/// </summary>

View File

@@ -28,6 +28,8 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.ADC;
@@ -187,6 +189,76 @@ public sealed class ADCStream : Stream, IStreamStack
return copied;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
if (count == 0)
{
return 0;
}
if (buffer is null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (offset < buffer.GetLowerBound(0))
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if ((offset + count) > buffer.GetLength(0))
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (_outBuffer is null)
{
var result = await ADCBase.DecompressAsync(
_stream,
cancellationToken: cancellationToken
);
_outBuffer = result.Output;
_outPosition = 0;
}
var inPosition = offset;
var toCopy = count;
var copied = 0;
while (_outPosition + toCopy >= _outBuffer.Length)
{
cancellationToken.ThrowIfCancellationRequested();
var piece = _outBuffer.Length - _outPosition;
Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece);
inPosition += piece;
copied += piece;
_position += piece;
toCopy -= piece;
var result = await ADCBase.DecompressAsync(
_stream,
cancellationToken: cancellationToken
);
_outBuffer = result.Output;
_outPosition = 0;
if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0)
{
return copied;
}
}
Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy);
_outPosition += toCopy;
_position += toCopy;
copied += toCopy;
return copied;
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SharpCompress.Compressors.Arj
{
[CLSCompliant(true)]
public class BitReader
{
private readonly byte[] data;
private int bytePos = 0;
private int bitPos = 0;
public BitReader(byte[] input)
{
data = input;
}
public int ReadBits(int count)
{
int result = 0;
for (int i = 0; i < count; i++)
{
if (bytePos >= data.Length)
{
throw new EndOfStreamException();
}
int bit = (data[bytePos] >> (7 - bitPos)) & 1;
result = (result << 1) | bit;
bitPos++;
if (bitPos == 8)
{
bitPos = 0;
bytePos++;
}
}
return result;
}
}
[CLSCompliant(true)]
public static class LHDecoder
{
private const int THRESHOLD = 3;
private static int DecodeVal(BitReader r, int from, int to)
{
int add = 0;
int bit = from;
while (bit < to && r.ReadBits(1) == 1)
{
add |= 1 << bit;
bit++;
}
int res = bit > 0 ? r.ReadBits(bit) : 0;
return res + add;
}
public static byte[] DecodeFastest(byte[] data, int originalSize)
{
var res = new List<byte>(originalSize);
var r = new BitReader(data);
while (res.Count < originalSize)
{
int len = DecodeVal(r, 0, 7);
if (len == 0)
{
byte nextChar = (byte)r.ReadBits(8);
res.Add(nextChar);
}
else
{
int repCount = len + THRESHOLD - 1;
int backPtr = DecodeVal(r, 9, 13);
if (backPtr >= res.Count)
{
throw new InvalidDataException("invalid back_ptr");
}
int i = res.Count - 1 - backPtr;
for (int j = 0; j < repCount; j++)
{
res.Add(res[i]);
i++;
}
}
}
return res.ToArray();
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.BZip2;
@@ -96,13 +98,37 @@ public sealed class BZip2Stream : Stream, IStreamStack
public override void SetLength(long value) => stream.SetLength(value);
#if !NETFRAMEWORK&& !NETSTANDARD2_0
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override int Read(Span<byte> buffer) => stream.Read(buffer);
public override void Write(ReadOnlySpan<byte> buffer) => stream.Write(buffer);
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
#endif
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
public override void Write(byte[] buffer, int offset, int count) =>
stream.Write(buffer, offset, count);

View File

@@ -2,6 +2,8 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
/*
@@ -1127,6 +1129,28 @@ internal class CBZip2InputStream : Stream, IStreamStack
return k;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
var c = -1;
int k;
for (k = 0; k < count; ++k)
{
cancellationToken.ThrowIfCancellationRequested();
c = ReadByte();
if (c == -1)
{
break;
}
buffer[k + offset] = (byte)c;
}
return Task.FromResult(k);
}
public override long Seek(long offset, SeekOrigin origin) => 0;
public override void SetLength(long value) { }

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
/*
@@ -2022,6 +2024,21 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
}
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
for (var k = 0; k < count; ++k)
{
cancellationToken.ThrowIfCancellationRequested();
WriteByte(buffer[k + offset]);
}
return Task.CompletedTask;
}
public override bool CanRead => false;
public override bool CanSeek => false;

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Readers;
using SharpCompress.Readers.Arc;
using SharpCompress.Readers.Arj;
namespace SharpCompress.Factories
{

View File

@@ -10,7 +10,7 @@ using SharpCompress.Common.Arj.Headers;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
namespace SharpCompress.Readers.Arc
namespace SharpCompress.Readers.Arj
{
public class ArjReader : AbstractReader<ArjEntry, ArjVolume>
{
@@ -33,13 +33,14 @@ namespace SharpCompress.Readers.Arc
protected override IEnumerable<ArjEntry> GetEntries(Stream stream)
{
var headerReader = new ArjMainHeader(new ArchiveEncoding());
var mainHeader = headerReader.Read(stream);
ArchiveEncoding encoding = new ArchiveEncoding();
var mainHeaderReader = new ArjMainHeader(encoding);
var localHeaderReader = new ArjLocalHeader(encoding);
var mainHeader = mainHeaderReader.Read(stream);
while (true)
{
var localReader = new ArjLocalHeader(new ArchiveEncoding());
var localHeader = localReader.Read(stream);
var localHeader = localHeaderReader.Read(stream);
if (localHeader == null)
break;

View File

@@ -160,7 +160,7 @@ public class ZipWriter : AbstractWriter
WriteDirectoryEntry(normalizedName, options);
}
public override async Task WriteDirectoryAsync(
public override Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
@@ -168,7 +168,7 @@ public class ZipWriter : AbstractWriter
{
// Synchronous implementation is sufficient for directory entries
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
return Task.CompletedTask;
}
private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options)

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Compressors.ADC;
using Xunit;
namespace SharpCompress.Test;
public class AdcAsyncTest : TestBase
{
[Fact]
public async Task TestAdcStreamAsyncWholeChunk()
{
using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"));
var decompressed = new byte[decFs.Length];
decFs.Read(decompressed, 0, decompressed.Length);
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
var test = new byte[262144];
await decStream.ReadAsync(test, 0, test.Length);
Assert.Equal(decompressed, test);
}
[Fact]
public async Task TestAdcStreamAsync()
{
using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"));
var decompressed = new byte[decFs.Length];
decFs.Read(decompressed, 0, decompressed.Length);
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
using var decMs = new MemoryStream();
var test = new byte[512];
var count = 0;
do
{
count = await decStream.ReadAsync(test, 0, test.Length);
decMs.Write(test, 0, count);
} while (count > 0);
Assert.Equal(decompressed, decMs.ToArray());
}
[Fact]
public async Task TestAdcStreamAsyncWithCancellation()
{
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
var test = new byte[512];
using var cts = new System.Threading.CancellationTokenSource();
// Read should complete without cancellation
var bytesRead = await decStream.ReadAsync(test, 0, test.Length, cts.Token);
Assert.True(bytesRead > 0);
}
}

View File

@@ -622,4 +622,36 @@ public class ArchiveTests : ReaderTests
VerifyFiles();
}
}
[Fact]
public void ArchiveFactory_Open_WithPreWrappedStream()
{
// Test that ArchiveFactory.Open works correctly with a stream that's already wrapped
// This addresses the issue where ZIP files fail to open on Linux
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip");
// Open with a pre-wrapped stream
using (var fileStream = File.OpenRead(testArchive))
using (var wrappedStream = SharpCompressStream.Create(fileStream, bufferSize: 32768))
using (var archive = ArchiveFactory.Open(wrappedStream))
{
Assert.Equal(ArchiveType.Zip, archive.Type);
Assert.Equal(3, archive.Entries.Count());
}
}
[Fact]
public void ArchiveFactory_Open_WithRawFileStream()
{
// Test that ArchiveFactory.Open works correctly with a raw FileStream
// This is the common use case reported in the issue
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip");
using (var stream = File.OpenRead(testArchive))
using (var archive = ArchiveFactory.Open(stream))
{
Assert.Equal(ArchiveType.Zip, archive.Type);
Assert.Equal(3, archive.Entries.Count());
}
}
}

View File

@@ -6,7 +6,6 @@ using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Arc;
using Xunit;
namespace SharpCompress.Test.Arj
@@ -21,26 +20,7 @@ namespace SharpCompress.Test.Arj
[Fact]
public void Arj_Uncompressed_Read() => Read("Arj.store.arj", CompressionType.None);
private void ProcessArchive(string archiveName)
{
// Process a given archive by its name
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)))
using (IReader reader = ArjReader.Open(stream))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
VerifyFilesByExtension();
}
[Fact]
public void Arj_Method4_Read() => Read("Arj.method4.arj");
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Compressors.BZip2;
using Xunit;
namespace SharpCompress.Test.BZip2;
public class BZip2StreamAsyncTests
{
private byte[] CreateTestData(int size)
{
var data = new byte[size];
// Create compressible data with repetitive pattern
for (int i = 0; i < size; i++)
{
data[i] = (byte)('A' + (i % 26));
}
return data;
}
[Fact]
public async Task BZip2CompressDecompressAsyncTest()
{
var testData = CreateTestData(10000);
byte[] compressed;
// Compress
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(testData, 0, testData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Verify compression occurred
Assert.True(compressed.Length > 0);
Assert.True(compressed.Length < testData.Length);
// Decompress
byte[] decompressed;
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
decompressed = new byte[testData.Length];
var totalRead = 0;
int bytesRead;
while (
(
bytesRead = await bzip2Stream.ReadAsync(
decompressed,
totalRead,
testData.Length - totalRead
)
) > 0
)
{
totalRead += bytesRead;
}
}
}
// Verify decompression
Assert.Equal(testData, decompressed);
}
[Fact]
public async Task BZip2ReadAsyncWithCancellationTest()
{
var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well
byte[] compressed;
// Compress
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(testData, 0, testData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Decompress with cancellation support
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
var buffer = new byte[1024];
using var cts = new System.Threading.CancellationTokenSource();
// Read should complete without cancellation
var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
Assert.True(bytesRead > 0);
}
}
}
[Fact]
public async Task BZip2MultipleAsyncWritesTest()
{
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
var data1 = Encoding.ASCII.GetBytes("Hello ");
var data2 = Encoding.ASCII.GetBytes("World");
var data3 = Encoding.ASCII.GetBytes("!");
await bzip2Stream.WriteAsync(data1, 0, data1.Length);
await bzip2Stream.WriteAsync(data2, 0, data2.Length);
await bzip2Stream.WriteAsync(data3, 0, data3.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
var compressed = memoryStream.ToArray();
Assert.True(compressed.Length > 0);
// Decompress and verify
using (var readStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
readStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
var result = new StringBuilder();
var buffer = new byte[256];
int bytesRead;
while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
result.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead));
}
Assert.Equal("Hello World!", result.ToString());
}
}
}
}
[Fact]
public async Task BZip2LargeDataAsyncTest()
{
var largeData = CreateTestData(100000);
// Compress
byte[] compressed;
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(largeData, 0, largeData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Decompress
byte[] decompressed;
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
decompressed = new byte[largeData.Length];
var totalRead = 0;
int bytesRead;
var buffer = new byte[4096];
while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Array.Copy(buffer, 0, decompressed, totalRead, bytesRead);
totalRead += bytesRead;
}
}
}
// Verify
Assert.Equal(largeData, decompressed);
}
}

View File

@@ -13,29 +13,53 @@ namespace SharpCompress.Test;
public abstract class ReaderTests : TestBase
{
protected void Read(string testArchive, ReaderOptions? options = null)
{
ReadCore(testArchive, options, ReadImpl);
}
protected void Read(
string testArchive,
CompressionType expectedCompression,
ReaderOptions? options = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
ReadCore(testArchive, options, (path, opts) => ReadImpl(path, expectedCompression, opts));
}
options ??= new ReaderOptions() { BufferSize = 0x20000 }; //test larger buffer size (need test rather than eyeballing debug logs :P)
private void ReadCore(
string testArchive,
ReaderOptions? options,
Action<string, ReaderOptions> readImpl
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
options ??= new ReaderOptions { BufferSize = 0x20000 };
options.LeaveStreamOpen = true;
ReadImpl(testArchive, expectedCompression, options);
readImpl(testArchive, options);
options.LeaveStreamOpen = false;
ReadImpl(testArchive, expectedCompression, options);
readImpl(testArchive, options);
VerifyFiles();
}
private void ReadImpl(string testArchive, ReaderOptions options)
{
ReadImplCore(testArchive, options, UseReader);
}
private void ReadImpl(
string testArchive,
CompressionType expectedCompression,
ReaderOptions options
)
{
ReadImplCore(testArchive, options, r => UseReader(r, expectedCompression));
}
private void ReadImplCore(string testArchive, ReaderOptions options, Action<IReader> useReader)
{
using var file = File.OpenRead(testArchive);
using var protectedStream = SharpCompressStream.Create(
@@ -47,19 +71,17 @@ public abstract class ReaderTests : TestBase
using var testStream = new TestStream(protectedStream);
using (var reader = ReaderFactory.Open(testStream, options))
{
UseReader(reader, expectedCompression);
useReader(reader);
protectedStream.ThrowOnDispose = false;
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
}
// Boolean XOR -- If the stream should be left open (true), then the stream should not be diposed (false)
// and if the stream should be closed (false), then the stream should be disposed (true)
var message =
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
}
public void UseReader(IReader reader, CompressionType expectedCompression)
protected void UseReader(IReader reader, CompressionType expectedCompression)
{
while (reader.MoveToNextEntry())
{
@@ -74,6 +96,20 @@ public abstract class ReaderTests : TestBase
}
}
private void UseReader(IReader reader)
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
protected async Task ReadAsync(
string testArchive,
CompressionType expectedCompression,

Binary file not shown.