diff --git a/src/SharpCompress/Readers/Ace/AceReader.cs b/src/SharpCompress/Readers/Ace/AceReader.cs index 13c644e8..c4902f95 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.cs @@ -11,6 +11,7 @@ using SharpCompress.Common.Arc; using SharpCompress.Common.Arj; using SharpCompress.Common.Arj.Headers; using SharpCompress.Common.Rar.Headers; +using SharpCompress.Readers.Arj; namespace SharpCompress.Readers.Ace { @@ -26,18 +27,23 @@ namespace SharpCompress.Readers.Ace /// - Recovery record support /// - Additional header flags /// - public class AceReader : AbstractReader + public abstract class AceReader : AbstractReader { - private readonly AceMainHeader _mainHeaderReader; + private readonly ArchiveEncoding _archiveEncoding; - private AceReader(Stream stream, ReaderOptions options) + internal AceReader(ReaderOptions options) : base(options, ArchiveType.Ace) { - Volume = new AceVolume(stream, options, 0); - _mainHeaderReader = new AceMainHeader(Options.ArchiveEncoding); + _archiveEncoding = Options.ArchiveEncoding; } - public override AceVolume Volume { get; } + private AceReader(Stream stream, ReaderOptions options) + : this(options) + { + Volume = new AceVolume(stream, options, 0); + } + + public override AceVolume? Volume { get; } /// /// Opens an AceReader for non-seeking usage with a single volume. @@ -48,12 +54,27 @@ namespace SharpCompress.Readers.Ace public static AceReader Open(Stream stream, ReaderOptions? options = null) { stream.NotNull(nameof(stream)); - return new AceReader(stream, options ?? new ReaderOptions()); + return new SingleVolumeAceReader(stream, options ?? new ReaderOptions()); } + /// + /// Opens an AceReader for Non-seeking usage with multiple volumes + /// + /// + /// + /// + public static AceReader Open(IEnumerable streams, ReaderOptions? options = null) + { + streams.NotNull(nameof(streams)); + return new MultiVolumeAceReader(streams, options ?? new ReaderOptions()); + } + + protected abstract void ValidateArchive(AceVolume archive); + protected override IEnumerable GetEntries(Stream stream) { - var mainHeader = _mainHeaderReader.Read(stream); + var mainHeaderReader = new AceMainHeader(_archiveEncoding); + var mainHeader = mainHeaderReader.Read(stream); if (mainHeader == null) { yield break; @@ -82,5 +103,8 @@ namespace SharpCompress.Readers.Ace yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream)); } } + + protected virtual IEnumerable CreateFilePartEnumerableForCurrentEntry() => + Entry.Parts; } } diff --git a/src/SharpCompress/Readers/Ace/MultiVolumeAceReader.cs b/src/SharpCompress/Readers/Ace/MultiVolumeAceReader.cs new file mode 100644 index 00000000..0b000f12 --- /dev/null +++ b/src/SharpCompress/Readers/Ace/MultiVolumeAceReader.cs @@ -0,0 +1,116 @@ +#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.Ace; + +namespace SharpCompress.Readers.Ace; + +internal class MultiVolumeAceReader : AceReader +{ + private readonly IEnumerator streams; + private Stream tempStream; + + internal MultiVolumeAceReader(IEnumerable streams, ReaderOptions options) + : base(options) => this.streams = streams.GetEnumerator(); + + protected override void ValidateArchive(AceVolume archive) { } + + protected override Stream RequestInitialStream() + { + if (streams.MoveNext()) + { + return streams.Current; + } + throw new MultiVolumeExtractionException( + "No stream provided when requested by MultiVolumeAceReader" + ); + } + + 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 MultiVolumeAceReader reader; + private readonly IEnumerator nextReadableStreams; + private Stream tempStream; + private bool isFirst = true; + + internal MultiVolumeStreamEnumerator( + MultiVolumeAceReader 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 MultiVolumeAceReader" + ); + } + else + { + reader.LoadStreamForReading(nextReadableStreams.Current); + } + + Current = reader.Entry.Parts.First(); + return true; + } + + public void Reset() { } + } +} diff --git a/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs new file mode 100644 index 00000000..61182d66 --- /dev/null +++ b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using SharpCompress.Common; +using SharpCompress.Common.Ace; + +namespace SharpCompress.Readers.Ace +{ + internal class SingleVolumeAceReader : AceReader + { + private readonly Stream _stream; + + internal SingleVolumeAceReader(Stream stream, ReaderOptions options) + : base(options) + { + stream.NotNull(nameof(stream)); + _stream = stream; + } + + protected override Stream RequestInitialStream() => _stream; + + protected override void ValidateArchive(AceVolume archive) + { + if (archive.IsMultiVolume) + { + throw new MultiVolumeExtractionException( + "Streamed archive is a Multi-volume archive. Use a different AceReader method to extract." + ); + } + } + } +} diff --git a/tests/SharpCompress.Test/Ace/AceReaderTests.cs b/tests/SharpCompress.Test/Ace/AceReaderTests.cs index a695b547..e156ba0b 100644 --- a/tests/SharpCompress.Test/Ace/AceReaderTests.cs +++ b/tests/SharpCompress.Test/Ace/AceReaderTests.cs @@ -6,6 +6,8 @@ using System.Text; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; +using SharpCompress.Readers.Ace; +using SharpCompress.Readers.Arj; using Xunit; namespace SharpCompress.Test.Ace @@ -32,11 +34,23 @@ namespace SharpCompress.Test.Ace Read(fileName, compressionType) ); } + [Theory] [InlineData("Ace.store.largefile.ace", CompressionType.None)] public void Ace_LargeFileTest_Read(string fileName, CompressionType compressionType) { ReadForBufferBoundaryCheck(fileName, compressionType); } + + [Fact] + public void Arj_Multi_Reader() + { + var exception = Assert.Throws(() => + DoMultiReader( + ["Ace.store.split.ace", "Ace.store.split.c01"], + streams => AceReader.Open(streams) + ) + ); + } } } diff --git a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs index ec8b422d..bad98ec8 100644 --- a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs +++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs @@ -45,14 +45,17 @@ namespace SharpCompress.Test.Arj 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", - ]) + DoMultiReader( + [ + "Arj.store.split.arj", + "Arj.store.split.a01", + "Arj.store.split.a02", + "Arj.store.split.a03", + "Arj.store.split.a04", + "Arj.store.split.a05", + ], + streams => ArjReader.Open(streams) + ) ); } @@ -74,26 +77,5 @@ namespace SharpCompress.Test.Arj { ReadForBufferBoundaryCheck(fileName, compressionType); } - - 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/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index cc5a75a7..897e6b32 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; @@ -220,4 +221,26 @@ public abstract class ReaderTests : TestBase Assert.Equal(expected.Pop(), reader.Entry.Key); } } + + protected void DoMultiReader( + string[] archives, + Func, IDisposable> readerFactory + ) + { + using var reader = readerFactory( + archives.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)).Select(File.OpenRead) + ); + + dynamic dynReader = reader; + + while (dynReader.MoveToNextEntry()) + { + dynReader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + + VerifyFiles(); + } } diff --git a/tests/TestArchives/Archives/Ace.store.split.ace b/tests/TestArchives/Archives/Ace.store.split.ace new file mode 100644 index 00000000..df3e5255 Binary files /dev/null and b/tests/TestArchives/Archives/Ace.store.split.ace differ diff --git a/tests/TestArchives/Archives/Ace.store.split.c00 b/tests/TestArchives/Archives/Ace.store.split.c00 new file mode 100644 index 00000000..4f1d89a4 Binary files /dev/null and b/tests/TestArchives/Archives/Ace.store.split.c00 differ