multi-file rars done manually

This commit is contained in:
Adam Hathcock
2026-01-16 13:43:26 +00:00
parent 447d35267f
commit 82d56b9678
8 changed files with 348 additions and 26 deletions

View File

@@ -102,7 +102,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
await MultiVolumeReadOnlyAsyncStream.Create(
Parts.ToAsyncEnumerable().CastAsync<RarFilePart>()
)
);
}
else
@@ -110,7 +112,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
await MultiVolumeReadOnlyAsyncStream.Create(
Parts.ToAsyncEnumerable().CastAsync<RarFilePart>()
)
);
}

View File

@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Rar;
internal sealed class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOnlyStreamBase, IStreamStack
{
#if DEBUG_STREAMS
long IStreamStack.InstanceId { get; set; }
#endif
int IStreamStack.DefaultBufferSize { get; set; }
Stream IStreamStack.BaseStream() => currentStream.NotNull();
int IStreamStack.BufferSize
{
get => 0;
set { }
}
int IStreamStack.BufferPosition
{
get => 0;
set { }
}
void IStreamStack.SetPosition(long position) { }
private long currentPosition;
private long maxPosition;
private IAsyncEnumerator<RarFilePart> filePartEnumerator;
private Stream? currentStream;
private MultiVolumeReadOnlyAsyncStream(IAsyncEnumerable<RarFilePart> parts)
{
filePartEnumerator = parts.GetAsyncEnumerator();
}
internal static async ValueTask<MultiVolumeReadOnlyAsyncStream> Create(
IAsyncEnumerable<RarFilePart> parts
)
{
var stream = new MultiVolumeReadOnlyAsyncStream(parts);
await stream.filePartEnumerator.MoveNextAsync();
stream.InitializeNextFilePart();
return stream;
}
#if NET8_0_OR_GREATER
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
if (filePartEnumerator != null)
{
await filePartEnumerator.DisposeAsync();
}
currentStream = null;
}
#else
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
filePartEnumerator.DisposeAsync().AsTask().GetAwaiter().GetResult();
currentStream = null;
}
#endif
private void InitializeNextFilePart()
{
maxPosition = filePartEnumerator.Current.FileHeader.CompressedSize;
currentPosition = 0;
currentStream = filePartEnumerator.Current.GetCompressedStream();
CurrentCrc = filePartEnumerator.Current.FileHeader.FileCrc;
}
public override int Read(byte[] buffer, int offset, int count) =>
throw new NotSupportedException(
"Synchronous read is not supported in MultiVolumeReadOnlyAsyncStream."
);
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
var totalRead = 0;
var currentOffset = offset;
var currentCount = count;
while (currentCount > 0)
{
var readSize = currentCount;
if (currentCount > maxPosition - currentPosition)
{
readSize = (int)(maxPosition - currentPosition);
}
var read = await currentStream
.NotNull()
.ReadAsync(buffer, currentOffset, readSize, cancellationToken)
.ConfigureAwait(false);
if (read < 0)
{
throw new EndOfStreamException();
}
currentPosition += read;
currentOffset += read;
currentCount -= read;
totalRead += read;
if (
((maxPosition - currentPosition) == 0)
&& filePartEnumerator.Current.FileHeader.IsSplitAfter
)
{
if (filePartEnumerator.Current.FileHeader.R4Salt != null)
{
throw new InvalidFormatException(
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!await filePartEnumerator.MoveNextAsync())
{
throw new InvalidFormatException(
"Multi-part rar file is incomplete. Entry expects a new volume: "
+ fileName
);
}
InitializeNextFilePart();
}
else
{
break;
}
}
return totalRead;
}
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
var totalRead = 0;
var currentOffset = 0;
var currentCount = buffer.Length;
while (currentCount > 0)
{
var readSize = currentCount;
if (currentCount > maxPosition - currentPosition)
{
readSize = (int)(maxPosition - currentPosition);
}
var read = await currentStream
.NotNull()
.ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken)
.ConfigureAwait(false);
if (read < 0)
{
throw new EndOfStreamException();
}
currentPosition += read;
currentOffset += read;
currentCount -= read;
totalRead += read;
if (
((maxPosition - currentPosition) == 0)
&& filePartEnumerator.Current.FileHeader.IsSplitAfter
)
{
if (filePartEnumerator.Current.FileHeader.R4Salt != null)
{
throw new InvalidFormatException(
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!await filePartEnumerator.MoveNextAsync())
{
throw new InvalidFormatException(
"Multi-part rar file is incomplete. Entry expects a new volume: "
+ fileName
);
}
InitializeNextFilePart();
}
else
{
break;
}
}
return totalRead;
}
#endif
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override void Flush() { }
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => 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();
}

View File

@@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
@@ -9,20 +7,21 @@ using SharpCompress.IO;
namespace SharpCompress.Compressors.Rar;
internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
internal sealed class MultiVolumeReadOnlyStream : MultiVolumeReadOnlyStreamBase, IStreamStack
{
#if DEBUG_STREAMS
long IStreamStack.InstanceId { get; set; }
#endif
int IStreamStack.DefaultBufferSize { get; set; }
Stream IStreamStack.BaseStream() => currentStream;
Stream IStreamStack.BaseStream() => currentStream.NotNull();
int IStreamStack.BufferSize
{
get => 0;
set { }
}
int IStreamStack.BufferPosition
{
get => 0;
@@ -35,7 +34,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
private long maxPosition;
private IEnumerator<RarFilePart> filePartEnumerator;
private Stream currentStream;
private Stream? currentStream;
internal MultiVolumeReadOnlyStream(IEnumerable<RarFilePart> parts)
{
@@ -56,11 +55,8 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
this.DebugDispose(typeof(MultiVolumeReadOnlyStream));
#endif
if (filePartEnumerator != null)
{
filePartEnumerator.Dispose();
filePartEnumerator = null;
}
filePartEnumerator.Dispose();
currentStream = null;
}
}
@@ -87,7 +83,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
readSize = (int)(maxPosition - currentPosition);
}
var read = currentStream.Read(buffer, currentOffset, readSize);
var read = currentStream.NotNull().Read(buffer, currentOffset, readSize);
if (read < 0)
{
throw new EndOfStreamException();
@@ -108,6 +104,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!filePartEnumerator.MoveNext())
{
@@ -116,6 +113,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
+ fileName
);
}
InitializeNextFilePart();
}
else
@@ -123,6 +121,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
break;
}
}
return totalRead;
}
@@ -145,6 +144,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
}
var read = await currentStream
.NotNull()
.ReadAsync(buffer, currentOffset, readSize, cancellationToken)
.ConfigureAwait(false);
if (read < 0)
@@ -167,6 +167,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!filePartEnumerator.MoveNext())
{
@@ -175,6 +176,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
+ fileName
);
}
InitializeNextFilePart();
}
else
@@ -182,6 +184,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
break;
}
}
return totalRead;
}
@@ -203,6 +206,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
}
var read = await currentStream
.NotNull()
.ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken)
.ConfigureAwait(false);
if (read < 0)
@@ -250,8 +254,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
public override bool CanWrite => false;
public byte[] CurrentCrc { get; private set; }
public override void Flush() { }
public override long Length => throw new NotSupportedException();

View File

@@ -0,0 +1,8 @@
using System.IO;
namespace SharpCompress.Compressors.Rar;
internal abstract class MultiVolumeReadOnlyStreamBase : Stream
{
public byte[]? CurrentCrc { get; protected set; }
}

View File

@@ -30,7 +30,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
void IStreamStack.SetPosition(long position) { }
private readonly MultiVolumeReadOnlyStream readStream;
private readonly MultiVolumeReadOnlyStreamBase readStream;
private readonly bool disableCRCCheck;
const uint BLAKE2S_NUM_ROUNDS = 10;
@@ -108,7 +108,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
private RarBLAKE2spStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
MultiVolumeReadOnlyStreamBase readStream
)
: base(unpack, fileHeader, readStream)
{
@@ -137,7 +137,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
public static async Task<RarBLAKE2spStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
MultiVolumeReadOnlyAsyncStream readStream,
CancellationToken cancellationToken = default
)
{

View File

@@ -29,14 +29,14 @@ internal class RarCrcStream : RarStream, IStreamStack
void IStreamStack.SetPosition(long position) { }
private readonly MultiVolumeReadOnlyStream readStream;
private readonly MultiVolumeReadOnlyStreamBase readStream;
private uint currentCrc;
private readonly bool disableCRC;
private RarCrcStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
MultiVolumeReadOnlyStreamBase readStream
)
: base(unpack, fileHeader, readStream)
{
@@ -62,7 +62,7 @@ internal class RarCrcStream : RarStream, IStreamStack
public static async Task<RarCrcStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
MultiVolumeReadOnlyStreamBase readStream,
CancellationToken cancellationToken = default
)
{
@@ -92,7 +92,7 @@ internal class RarCrcStream : RarStream, IStreamStack
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0)
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
&& count != 0
)
{
@@ -118,7 +118,7 @@ internal class RarCrcStream : RarStream, IStreamStack
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0)
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
&& count != 0
)
{
@@ -143,7 +143,7 @@ internal class RarCrcStream : RarStream, IStreamStack
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0)
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
&& buffer.Length != 0
)
{

View File

@@ -4,6 +4,8 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
@@ -47,6 +49,13 @@ internal class MultiVolumeRarReader : RarReader
return enumerator;
}
protected override IAsyncEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntryAsync()
{
var enumerator = new MultiVolumeStreamAsyncEnumerator(this, streams, tempStream);
tempStream = null;
return enumerator;
}
private class MultiVolumeStreamEnumerator : IEnumerable<FilePart>, IEnumerator<FilePart>
{
private readonly MultiVolumeRarReader reader;
@@ -110,4 +119,66 @@ internal class MultiVolumeRarReader : RarReader
public void Reset() { }
}
private class MultiVolumeStreamAsyncEnumerator
: IAsyncEnumerable<FilePart>,
IAsyncEnumerator<FilePart>
{
private readonly MultiVolumeRarReader reader;
private readonly IEnumerator<Stream> nextReadableStreams;
private Stream tempStream;
private bool isFirst = true;
internal MultiVolumeStreamAsyncEnumerator(
MultiVolumeRarReader r,
IEnumerator<Stream> nextReadableStreams,
Stream tempStream
)
{
reader = r;
this.nextReadableStreams = nextReadableStreams;
this.tempStream = tempStream;
}
public FilePart Current { get; private set; }
public async ValueTask<bool> MoveNextAsync()
{
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)
{
await reader.LoadStreamForReadingAsync(tempStream);
tempStream = null;
}
else if (!nextReadableStreams.MoveNext())
{
throw new MultiVolumeExtractionException(
"No stream provided when requested by MultiVolumeRarReader"
);
}
else
{
await reader.LoadStreamForReadingAsync(nextReadableStreams.Current);
}
Current = reader.Entry.Parts.First();
return true;
}
public IAsyncEnumerator<FilePart> GetAsyncEnumerator(
CancellationToken cancellationToken = new()
) => this;
public ValueTask DisposeAsync() => new();
}
}

View File

@@ -100,6 +100,9 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
protected virtual IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry() =>
Entry.Parts;
protected virtual IAsyncEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntryAsync() =>
Entry.Parts.ToAsyncEnumerable();
protected override EntryStream GetEntryStream()
{
if (Entry.IsRedir)
@@ -134,8 +137,8 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
throw new InvalidOperationException("no stream for redirect entry");
}
var stream = new MultiVolumeReadOnlyStream(
CreateFilePartEnumerableForCurrentEntry().Cast<RarFilePart>()
var stream = await MultiVolumeReadOnlyAsyncStream.Create(
CreateFilePartEnumerableForCurrentEntryAsync().CastAsync<RarFilePart>()
);
if (Entry.IsRarV3)
{