MultiVolume not supported, tests provided by split archive.

This commit is contained in:
Twan van Dongen
2026-01-03 19:09:43 +01:00
parent bfcdeb3784
commit 3ebf97dd49
8 changed files with 227 additions and 37 deletions

View File

@@ -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
/// </remarks>
public class AceReader : AbstractReader<AceEntry, AceVolume>
public abstract class AceReader : AbstractReader<AceEntry, AceVolume>
{
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; }
/// <summary>
/// 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());
}
/// <summary>
/// Opens an AceReader for Non-seeking usage with multiple volumes
/// </summary>
/// <param name="streams"></param>
/// <param name="options"></param>
/// <returns></returns>
public static AceReader Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
}
protected abstract void ValidateArchive(AceVolume archive);
protected override IEnumerable<AceEntry> 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<FilePart> CreateFilePartEnumerableForCurrentEntry() =>
Entry.Parts;
}
}

View File

@@ -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<Stream> streams;
private Stream tempStream;
internal MultiVolumeAceReader(IEnumerable<Stream> 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<FilePart> CreateFilePartEnumerableForCurrentEntry()
{
var enumerator = new MultiVolumeStreamEnumerator(this, streams, tempStream);
tempStream = null;
return enumerator;
}
private class MultiVolumeStreamEnumerator : IEnumerable<FilePart>, IEnumerator<FilePart>
{
private readonly MultiVolumeAceReader reader;
private readonly IEnumerator<Stream> nextReadableStreams;
private Stream tempStream;
private bool isFirst = true;
internal MultiVolumeStreamEnumerator(
MultiVolumeAceReader r,
IEnumerator<Stream> nextReadableStreams,
Stream tempStream
)
{
reader = r;
this.nextReadableStreams = nextReadableStreams;
this.tempStream = tempStream;
}
public IEnumerator<FilePart> 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() { }
}
}

View File

@@ -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."
);
}
}
}
}

View File

@@ -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<MultiVolumeExtractionException>(() =>
DoMultiReader(
["Ace.store.split.ace", "Ace.store.split.c01"],
streams => AceReader.Open(streams)
)
);
}
}
}

View File

@@ -45,14 +45,17 @@ namespace SharpCompress.Test.Arj
public void Arj_Multi_Reader()
{
var exception = Assert.Throws<MultiVolumeExtractionException>(() =>
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();
}
}
}

View File

@@ -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<IEnumerable<Stream>, 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();
}
}

Binary file not shown.

Binary file not shown.