diff --git a/src/SharpCompress/Common/Arj/ArjFilePart.cs b/src/SharpCompress/Common/Arj/ArjFilePart.cs
index 10fe24c6..a60e9281 100644
--- a/src/SharpCompress/Common/Arj/ArjFilePart.cs
+++ b/src/SharpCompress/Common/Arj/ArjFilePart.cs
@@ -39,16 +39,10 @@ namespace SharpCompress.Common.Arj
);
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 LHDecoderStream(
+ _stream,
+ (int)Header.OriginalSize
);
-
- compressedStream = new MemoryStream(decompressedData);
break;
default:
throw new NotSupportedException(
diff --git a/src/SharpCompress/Common/Arj/ArjVolume.cs b/src/SharpCompress/Common/Arj/ArjVolume.cs
index bf3ba109..0c99beb4 100644
--- a/src/SharpCompress/Common/Arj/ArjVolume.cs
+++ b/src/SharpCompress/Common/Arj/ArjVolume.cs
@@ -4,6 +4,8 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using SharpCompress.Common.Rar;
+using SharpCompress.Common.Rar.Headers;
using SharpCompress.Readers;
namespace SharpCompress.Common.Arj
@@ -12,5 +14,28 @@ namespace SharpCompress.Common.Arj
{
public ArjVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
: base(stream, readerOptions, index) { }
+
+ public override bool IsFirstVolume
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ ///
+ /// ArjArchive is part of a multi-part archive.
+ ///
+ public override bool IsMultiVolume
+ {
+ get
+ {
+ return false;
+ }
+ }
+ internal IEnumerable GetVolumeFileParts()
+ {
+ return new List();
+ }
}
}
diff --git a/src/SharpCompress/Compressors/Arj/BitReader.cs b/src/SharpCompress/Compressors/Arj/BitReader.cs
new file mode 100644
index 00000000..c747b0ca
--- /dev/null
+++ b/src/SharpCompress/Compressors/Arj/BitReader.cs
@@ -0,0 +1,60 @@
+using System;
+using System.IO;
+
+namespace SharpCompress.Compressors.Arj
+{
+ [CLSCompliant(true)]
+ public sealed class BitReader
+ {
+ private readonly Stream _stream;
+ private int _bitBuffer;
+ private int _bitsRemaining;
+ private bool _disposed;
+
+ public BitReader(Stream input)
+ {
+ _stream = input ?? throw new ArgumentNullException(nameof(input));
+ if (!input.CanRead)
+ throw new ArgumentException("Stream must be readable.", nameof(input));
+ }
+
+ public int ReadBits(int count)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(BitReader));
+
+ if (count <= 0 || count > 32)
+ throw new ArgumentOutOfRangeException(
+ nameof(count),
+ "Bit count must be between 1 and 32."
+ );
+
+ int result = 0;
+ for (int i = 0; i < count; i++)
+ {
+ if (_bitsRemaining == 0)
+ {
+ int nextByte = _stream.ReadByte();
+ if (nextByte == -1)
+ throw new EndOfStreamException();
+
+ _bitBuffer = nextByte;
+ _bitsRemaining = 8;
+ }
+
+ // hoogste bit eerst
+ result = (result << 1) | ((_bitBuffer >> 7) & 1);
+ _bitBuffer <<= 1;
+ _bitsRemaining--;
+ }
+
+ return result;
+ }
+
+ public void AlignToByte()
+ {
+ _bitsRemaining = 0;
+ _bitBuffer = 0;
+ }
+ }
+}
diff --git a/src/SharpCompress/Compressors/Arj/DecodeFastest.cs b/src/SharpCompress/Compressors/Arj/DecodeFastest.cs
deleted file mode 100644
index f9f4ef44..00000000
--- a/src/SharpCompress/Compressors/Arj/DecodeFastest.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-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(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();
- }
- }
-}
diff --git a/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs b/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs
new file mode 100644
index 00000000..e17c0744
--- /dev/null
+++ b/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using SharpCompress.IO;
+
+namespace SharpCompress.Compressors.Arj
+{
+ [CLSCompliant(true)]
+ public sealed class LHDecoderStream : Stream, IStreamStack
+ {
+#if DEBUG_STREAMS
+ long IStreamStack.InstanceId { get; set; }
+#endif
+ int IStreamStack.DefaultBufferSize { get; set; }
+
+ Stream IStreamStack.BaseStream() => _stream;
+
+ int IStreamStack.BufferSize
+ {
+ get => 0;
+ set { }
+ }
+ int IStreamStack.BufferPosition
+ {
+ get => 0;
+ set { }
+ }
+
+ void IStreamStack.SetPosition(long position) { }
+
+ private readonly BitReader _bitReader;
+ private readonly Stream _stream;
+
+ // Buffer containing *all* bytes decoded so far.
+ private readonly List _buffer = new();
+
+ private long _readPosition;
+ private readonly int _originalSize;
+ private bool _finishedDecoding;
+ private bool _disposed;
+
+ private const int THRESHOLD = 3;
+
+ public LHDecoderStream(Stream compressedStream, int originalSize)
+ {
+ _stream = compressedStream ?? throw new ArgumentNullException(nameof(compressedStream));
+ if (!compressedStream.CanRead)
+ throw new ArgumentException(
+ "compressedStream must be readable.",
+ nameof(compressedStream)
+ );
+
+ _bitReader = new BitReader(compressedStream);
+ _originalSize = originalSize;
+ _readPosition = 0;
+ _finishedDecoding = (originalSize == 0);
+ }
+
+ public Stream BaseStream => _stream;
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+
+ public override long Length => _originalSize;
+
+ public override long Position
+ {
+ get => _readPosition;
+ set => throw new NotSupportedException();
+ }
+
+ ///
+ /// Decodes a single element (literal or back-reference) and appends it to _buffer.
+ /// Returns true if data was added, or false if all input has already been decoded.
+ ///
+ private bool DecodeNext()
+ {
+ if (_buffer.Count >= _originalSize)
+ {
+ _finishedDecoding = true;
+ return false;
+ }
+
+ int len = DecodeVal(0, 7);
+ if (len == 0)
+ {
+ byte nextChar = (byte)_bitReader.ReadBits(8);
+ _buffer.Add(nextChar);
+ }
+ else
+ {
+ int repCount = len + THRESHOLD - 1;
+ int backPtr = DecodeVal(9, 13);
+
+ if (backPtr >= _buffer.Count)
+ throw new InvalidDataException("Invalid back_ptr in LH stream");
+
+ int srcIndex = _buffer.Count - 1 - backPtr;
+ for (int j = 0; j < repCount && _buffer.Count < _originalSize; j++)
+ {
+ byte b = _buffer[srcIndex];
+ _buffer.Add(b);
+ srcIndex++;
+ // srcIndex may grow; it's allowed (source region can overlap destination)
+ }
+ }
+
+ if (_buffer.Count >= _originalSize)
+ {
+ _finishedDecoding = true;
+ }
+
+ return true;
+ }
+
+ private int DecodeVal(int from, int to)
+ {
+ int add = 0;
+ int bit = from;
+
+ while (bit < to && _bitReader.ReadBits(1) == 1)
+ {
+ add |= 1 << bit;
+ bit++;
+ }
+
+ int res = bit > 0 ? _bitReader.ReadBits(bit) : 0;
+ return res + add;
+ }
+
+ ///
+ /// Reads decompressed bytes into buffer[offset..offset+count].
+ /// The method decodes additional data on demand when needed.
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(LHDecoderStream));
+ if (buffer == null)
+ throw new ArgumentNullException(nameof(buffer));
+ if (offset < 0 || count < 0 || offset + count > buffer.Length)
+ throw new ArgumentOutOfRangeException("offset/count");
+
+ if (_readPosition >= _originalSize)
+ return 0; // EOF
+
+ int totalRead = 0;
+
+ while (totalRead < count && _readPosition < _originalSize)
+ {
+ if (_readPosition >= _buffer.Count)
+ {
+ bool had = DecodeNext();
+ if (!had)
+ {
+ break;
+ }
+ }
+
+ int available = _buffer.Count - (int)_readPosition;
+ if (available <= 0)
+ {
+ if (!_finishedDecoding)
+ {
+ continue;
+ }
+ break;
+ }
+
+ int toCopy = Math.Min(available, count - totalRead);
+ _buffer.CopyTo((int)_readPosition, buffer, offset + totalRead, toCopy);
+
+ _readPosition += toCopy;
+ totalRead += toCopy;
+ }
+
+ return totalRead;
+ }
+
+ public override void Flush() => throw new NotSupportedException();
+
+ 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();
+ }
+}
diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs
index d5441e27..6dca8ae8 100644
--- a/src/SharpCompress/Readers/Arj/ArjReader.cs
+++ b/src/SharpCompress/Readers/Arj/ArjReader.cs
@@ -1,23 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj;
using SharpCompress.Common.Arj.Headers;
-using SharpCompress.Common.Zip;
-using SharpCompress.Common.Zip.Headers;
+using SharpCompress.Readers.Rar;
namespace SharpCompress.Readers.Arj
{
- public class ArjReader : AbstractReader
+ public abstract class ArjReader : AbstractReader
{
- private ArjReader(Stream stream, ReaderOptions options)
- : base(options, ArchiveType.Arj) => Volume = new ArjVolume(stream, options, 0);
+ internal ArjReader(ReaderOptions options)
+ : base(options, ArchiveType.Arj)
+ { }
- public override ArjVolume Volume { get; }
+ ///
+ /// Derived class must create or manage the Volume itself.
+ /// AbstractReader.Volume is get-only, so it cannot be set here.
+ ///
+
+ public override ArjVolume? Volume => _volume;
+
+ private ArjVolume? _volume;
///
/// Opens an ArjReader for Non-seeking usage with a single volume
@@ -28,15 +32,39 @@ namespace SharpCompress.Readers.Arj
public static ArjReader Open(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
- return new ArjReader(stream, options ?? new ReaderOptions());
+ return new SingleVolumeArjReader(stream, options ?? new ReaderOptions());
}
+ ///
+ /// Opens an ArjReader for Non-seeking usage with multiple volumes
+ ///
+ ///
+ ///
+ ///
+ public static ArjReader Open(IEnumerable streams, ReaderOptions? options = null)
+ {
+ streams.NotNull(nameof(streams));
+ return new MultiVolumeArjReader(streams, options ?? new ReaderOptions());
+ }
+
+ protected abstract void ValidateArchive(ArjVolume archive);
protected override IEnumerable GetEntries(Stream stream)
{
- ArchiveEncoding encoding = new ArchiveEncoding();
+ var encoding = new ArchiveEncoding();
var mainHeaderReader = new ArjMainHeader(encoding);
var localHeaderReader = new ArjLocalHeader(encoding);
+
var mainHeader = mainHeaderReader.Read(stream);
+ if (mainHeader?.IsVolume == true)
+ {
+ throw new MultiVolumeExtractionException("Multi volumes are currently not supported");
+ }
+
+ if (_volume == null)
+ {
+ _volume = new ArjVolume(stream, Options, 0);
+ ValidateArchive(_volume);
+ }
while (true)
{
@@ -47,5 +75,8 @@ namespace SharpCompress.Readers.Arj
yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream));
}
}
+
+ protected virtual IEnumerable CreateFilePartEnumerableForCurrentEntry() =>
+ Entry.Parts;
}
}
diff --git a/src/SharpCompress/Readers/Arj/MultiVolumeArjReader.cs b/src/SharpCompress/Readers/Arj/MultiVolumeArjReader.cs
new file mode 100644
index 00000000..77687da5
--- /dev/null
+++ b/src/SharpCompress/Readers/Arj/MultiVolumeArjReader.cs
@@ -0,0 +1,115 @@
+#nullable disable
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using SharpCompress.Common;
+using SharpCompress.Common.Arj;
+using SharpCompress.Readers.Rar;
+
+namespace SharpCompress.Readers.Arj;
+internal class MultiVolumeArjReader : ArjReader
+{
+ private readonly IEnumerator streams;
+ private Stream tempStream;
+
+ internal MultiVolumeArjReader(IEnumerable streams, ReaderOptions options)
+ : base(options) => this.streams = streams.GetEnumerator();
+
+ protected override void ValidateArchive(ArjVolume archive) { }
+
+ protected override Stream RequestInitialStream()
+ {
+ if (streams.MoveNext())
+ {
+ return streams.Current;
+ }
+ throw new MultiVolumeExtractionException(
+ "No stream provided when requested by MultiVolumeArjReader"
+ );
+ }
+ internal override bool NextEntryForCurrentStream()
+ {
+ if (!base.NextEntryForCurrentStream())
+ {
+ // if we're got another stream to try to process then do so
+ return streams.MoveNext() && LoadStreamForReading(streams.Current);
+ }
+ return true;
+ }
+
+ protected override IEnumerable CreateFilePartEnumerableForCurrentEntry()
+ {
+ var enumerator = new MultiVolumeStreamEnumerator(this, streams, tempStream);
+ tempStream = null;
+ return enumerator;
+ }
+
+ private class MultiVolumeStreamEnumerator : IEnumerable, IEnumerator
+ {
+ private readonly MultiVolumeArjReader reader;
+ private readonly IEnumerator nextReadableStreams;
+ private Stream tempStream;
+ private bool isFirst = true;
+
+ internal MultiVolumeStreamEnumerator(
+ MultiVolumeArjReader r,
+ IEnumerator nextReadableStreams,
+ Stream tempStream
+ )
+ {
+ reader = r;
+ this.nextReadableStreams = nextReadableStreams;
+ this.tempStream = tempStream;
+ }
+
+ public IEnumerator GetEnumerator() => this;
+
+ IEnumerator IEnumerable.GetEnumerator() => this;
+
+ public FilePart Current { get; private set; }
+
+ public void Dispose() { }
+
+ object IEnumerator.Current => Current;
+
+ public bool MoveNext()
+ {
+ if (isFirst)
+ {
+ Current = reader.Entry.Parts.First();
+ isFirst = false; //first stream already to go
+ return true;
+ }
+
+ if (!reader.Entry.IsSplitAfter)
+ {
+ return false;
+ }
+ if (tempStream != null)
+ {
+ reader.LoadStreamForReading(tempStream);
+ tempStream = null;
+ }
+ else if (!nextReadableStreams.MoveNext())
+ {
+ throw new MultiVolumeExtractionException(
+ "No stream provided when requested by MultiVolumeArjReader"
+ );
+ }
+ else
+ {
+ reader.LoadStreamForReading(nextReadableStreams.Current);
+ }
+
+ Current = reader.Entry.Parts.First();
+ return true;
+ }
+
+ public void Reset() { }
+ }
+}
diff --git a/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs
new file mode 100644
index 00000000..aa293438
--- /dev/null
+++ b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs
@@ -0,0 +1,30 @@
+using System;
+using System.IO;
+using SharpCompress.Common;
+using SharpCompress.Common.Arj;
+
+namespace SharpCompress.Readers.Arj
+{
+ internal class SingleVolumeArjReader : ArjReader
+ {
+ private readonly Stream _stream;
+
+ internal SingleVolumeArjReader(Stream stream, ReaderOptions options)
+ : base(options)
+ {
+ stream.NotNull(nameof(stream));
+ _stream = stream;
+ }
+
+ protected override Stream RequestInitialStream() => _stream;
+
+ protected override void ValidateArchive(ArjVolume archive)
+ {
+ if (archive.IsMultiVolume)
+ {
+ throw new MultiVolumeExtractionException(
+ "Streamed archive is a Multi-volume archive. Use a different ArjReader method to extract.");
+ }
+ }
+ }
+}
diff --git a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs
index ee9cecca..01556b13 100644
--- a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs
+++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs
@@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
+using SharpCompress.Readers.Arj;
using Xunit;
namespace SharpCompress.Test.Arj
@@ -23,5 +24,43 @@ namespace SharpCompress.Test.Arj
[Fact]
public void Arj_Method4_Read() => Read("Arj.method4.arj");
+
+ [Fact]
+ public void Arj_Multi_Reader()
+ {
+ var exception = Assert.Throws(() =>
+ DoArj_Multi_Reader(
+ [
+ "Arj.store.split.arj",
+ "Arj.store.split.a01",
+ "Arj.store.split.a02",
+ "Arj.store.split.a03",
+ "Arj.store.split.a04",
+ "Arj.store.split.a05",
+ ]
+
+ ));
+ }
+
+ private void DoArj_Multi_Reader(string[] archives)
+ {
+ using (
+ var reader = ArjReader.Open(
+ archives
+ .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
+ .Select(p => File.OpenRead(p))
+ )
+ )
+ {
+ while (reader.MoveToNextEntry())
+ {
+ reader.WriteEntryToDirectory(
+ SCRATCH_FILES_PATH,
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
+ );
+ }
+ }
+ VerifyFiles();
+ }
}
}
diff --git a/tests/TestArchives/Archives/Arj.store.split.a01 b/tests/TestArchives/Archives/Arj.store.split.a01
new file mode 100644
index 00000000..a27d4f00
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.a01 differ
diff --git a/tests/TestArchives/Archives/Arj.store.split.a02 b/tests/TestArchives/Archives/Arj.store.split.a02
new file mode 100644
index 00000000..0d40578c
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.a02 differ
diff --git a/tests/TestArchives/Archives/Arj.store.split.a03 b/tests/TestArchives/Archives/Arj.store.split.a03
new file mode 100644
index 00000000..15062609
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.a03 differ
diff --git a/tests/TestArchives/Archives/Arj.store.split.a04 b/tests/TestArchives/Archives/Arj.store.split.a04
new file mode 100644
index 00000000..c54f83e0
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.a04 differ
diff --git a/tests/TestArchives/Archives/Arj.store.split.a05 b/tests/TestArchives/Archives/Arj.store.split.a05
new file mode 100644
index 00000000..a75c4ed3
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.a05 differ
diff --git a/tests/TestArchives/Archives/Arj.store.split.arj b/tests/TestArchives/Archives/Arj.store.split.arj
new file mode 100644
index 00000000..eb17059f
Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.split.arj differ