Compare commits

...

29 Commits

Author SHA1 Message Date
Adam Hathcock
2e301becb4 fmt 2025-11-27 15:50:25 +00:00
Adam Hathcock
2e7d4eb74b first pass of making Rar multi-threaded 2025-11-27 15:49:29 +00:00
Adam Hathcock
5b2030bb98 add SupportsMultiThreading and IsMultiVolume to archive 2025-11-27 15:07:13 +00:00
Adam Hathcock
c1169539ea add multi-threading test with fix 2025-11-27 12:22:49 +00:00
Adam Hathcock
8d2463f575 More test fixes and fmt 2025-11-27 12:14:35 +00:00
Adam Hathcock
af7e270b2d added SupportsMultiThreading flag for File based Zips 2025-11-27 12:11:10 +00:00
Adam Hathcock
1984da6997 Merge remote-tracking branch 'origin/master' into adam/multi-threaded 2025-11-27 10:55:50 +00:00
Adam Hathcock
9433e06b93 Merge pull request #1023 from adamhathcock/copilot/fix-zip64-validation-issue
Fix version mismatch between Local File Header and Central Directory File Header in Zip archives
2025-11-25 15:30:48 +00:00
copilot-swe-agent[bot]
a92aaa51d5 Remove ZipCompressionMethod.None from version 63 check
None (stored) compression only requires version 10/20, not version 63.
Version 63 is specifically for advanced compression methods like LZMA,
PPMd, BZip2, and ZStandard.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-25 14:54:13 +00:00
Adam Hathcock
d41908adeb fixes for clarity 2025-11-25 14:25:58 +00:00
Adam Hathcock
81ca15b567 Update src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 14:13:10 +00:00
Adam Hathcock
b81d0fd730 Merge pull request #1009 from adamhathcock/dependabot/nuget/AwesomeAssertions-9.3.0
Bump AwesomeAssertions from 9.2.1 to 9.3.0
2025-11-25 11:55:41 +00:00
Adam Hathcock
3a1bb187e8 Merge pull request #1031 from adamhathcock/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-25 11:55:21 +00:00
Adam Hathcock
3fee14a070 Merge pull request #1035 from adamhathcock/adam/update-csharpier
Update csharpier and reformat
2025-11-25 11:54:56 +00:00
Adam Hathcock
5bf789ac65 Update csharpier and reformat 2025-11-25 11:50:21 +00:00
dependabot[bot]
be06049db3 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 09:20:23 +00:00
Adam Hathcock
a0435f6a60 Merge pull request #1030 from TwanVanDongen/master
Added buffer boundary tests.
2025-11-23 15:53:36 +00:00
Twan van Dongen
2321e2c90b Added buffer boundaty tests. Changed largefile to Alice29.txt as it's sufficient for the tests. 2025-11-22 12:32:25 +01:00
Adam Hathcock
97e98d8629 Merge pull request #1028 from TwanVanDongen/master
Buffer boundary tests
2025-11-21 08:35:54 +00:00
Twan van Dongen
d96e7362d2 Buffer boundary test for ARC's Squeezed method 2025-11-20 21:56:07 +01:00
Twan van Dongen
7dd46fe5ed More buffer boundary tests 2025-11-20 19:43:07 +01:00
Adam Hathcock
04c044cb2b Update tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 15:49:55 +00:00
Adam Hathcock
cc10a12fbc Update tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 15:49:27 +00:00
Adam Hathcock
8b0a1c699f Update tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 15:49:17 +00:00
copilot-swe-agent[bot]
0fe48c647e Enhance fix to handle LZMA/PPMd/BZip2/ZStandard compression methods
Also fixes pre-existing version mismatch for advanced compression methods that require version 63. Added tests for LZMA and PPMd to verify version consistency.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-19 11:29:43 +00:00
copilot-swe-agent[bot]
434ce05416 Fix Zip64 version mismatch between LFH and CDFH
When UseZip64=true but files are small, ensure Central Directory File Header uses version 45 to match Local File Header. This fixes validation failures in System.IO.Packaging and other strict readers.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-19 11:14:06 +00:00
copilot-swe-agent[bot]
51237a34eb Initial plan 2025-11-19 11:02:28 +00:00
dependabot[bot]
4c61628078 Bump AwesomeAssertions from 9.2.1 to 9.3.0
---
updated-dependencies:
- dependency-name: AwesomeAssertions
  dependency-version: 9.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 11:57:30 +00:00
Adam Hathcock
4536fddec2 intermediate commit: add zip/filepart that only deals with fileinfo 2025-10-29 13:02:27 +00:00
50 changed files with 4421 additions and 255 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.1.2",
"version": "1.2.1",
"commands": [
"csharpier"
],

View File

@@ -14,7 +14,7 @@ jobs:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x

View File

@@ -1,7 +1,7 @@
<Project>
<ItemGroup>
<PackageVersion Include="Bullseye" Version="6.0.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.2.1" />
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.14" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />

View File

@@ -172,4 +172,9 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
return Entries.All(x => x.IsComplete);
}
}
public virtual bool IsMultiVolume =>
_sourceStream?.Files.Count > 1 || _sourceStream?.Streams.Count > 1;
public virtual bool SupportsMultiThreading => false;
}

View File

@@ -45,4 +45,14 @@ public interface IArchive : IDisposable
/// The total size of the files as uncompressed in the archive.
/// </summary>
long TotalUncompressSize { get; }
/// <summary>
/// Is the archive part of a multi-volume set.
/// </summary>
bool IsMultiVolume { get; }
/// <summary>
/// Does the archive support multi-threaded extraction.
/// </summary>
bool SupportsMultiThreading { get; }
}

View File

@@ -88,7 +88,7 @@ public static class IArchiveEntryExtensions
entry,
destinationDirectory,
options,
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
entry.WriteToFileAsync,
cancellationToken
);
@@ -124,10 +124,11 @@ public static class IArchiveEntryExtensions
entry,
destinationFileName,
options,
async (x, fm) =>
async (x, fm, ct) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
}
await entry.WriteToAsync(fs, ct).ConfigureAwait(false);
},
cancellationToken
);
}

View File

@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives.Rar;
/// <summary>
/// A rar part based on a FileInfo object
/// </summary>
internal class FileInfoRarArchiveVolume : RarVolume
{
internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index)
: base(StreamingMode.Seekable, fileInfo.OpenRead(), FixOptions(options), index)
{
FileInfo = fileInfo;
FileParts = GetVolumeFileParts().ToArray().ToReadOnly();
}
private static ReaderOptions FixOptions(ReaderOptions options)
{
//make sure we're closing streams with fileinfo
options.LeaveStreamOpen = false;
return options;
}
internal ReadOnlyCollection<RarFilePart> FileParts { get; }
internal FileInfo FileInfo { get; }
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
new FileInfoRarFilePart(this, ReaderOptions.Password, markHeader, fileHeader, FileInfo);
internal override IEnumerable<RarFilePart> ReadFileParts() => FileParts;
}

View File

@@ -1,21 +0,0 @@
using System.IO;
using SharpCompress.Common.Rar.Headers;
namespace SharpCompress.Archives.Rar;
internal sealed class FileInfoRarFilePart : SeekableFilePart
{
internal FileInfoRarFilePart(
FileInfoRarArchiveVolume volume,
string? password,
MarkHeader mh,
FileHeader fh,
FileInfo fi
)
: base(mh, fh, volume.Index, volume.Stream, password) => FileInfo = fi;
internal FileInfo FileInfo { get; }
internal override string FilePartName =>
"Rar File: " + FileInfo.FullName + " File Entry: " + FileHeader.FileName;
}

View File

@@ -47,9 +47,9 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
{
sourceStream.LoadAllParts(); //request all streams
var streams = sourceStream.Streams.ToArray();
var i = 0;
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions)) //test part 2 - true = multipart not split
{
var i = 0;
sourceStream.IsVolumes = true;
streams[1].Position = 0;
sourceStream.Position = 0;
@@ -57,12 +57,18 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
return sourceStream.Streams.Select(a => new StreamRarArchiveVolume(
a,
ReaderOptions,
i++
i++,
IsMultiVolume
));
}
//split mode or single file
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
return new StreamRarArchiveVolume(
sourceStream,
ReaderOptions,
0,
IsMultiVolume
).AsEnumerable();
}
protected override IReader CreateReaderForSolidExtraction()
@@ -83,6 +89,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
}
public override bool IsSolid => Volumes.First().IsSolidArchive;
public override bool SupportsMultiThreading => !IsMultiVolume && !IsSolid;
public virtual int MinVersion => Volumes.First().MinVersion;
public virtual int MaxVersion => Volumes.First().MaxVersion;

View File

@@ -134,4 +134,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
);
}
}
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
}

View File

@@ -1,25 +1,29 @@
using System.IO;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
namespace SharpCompress.Archives.Rar;
internal class SeekableFilePart : RarFilePart
internal class SeekableRarFilePart : RarFilePart
{
private readonly Stream _stream;
private readonly string? _password;
private readonly bool _isMultiVolume;
internal SeekableFilePart(
internal SeekableRarFilePart(
MarkHeader mh,
FileHeader fh,
int index,
Stream stream,
string? password
string? password,
bool isMultiVolume
)
: base(mh, fh, index)
{
_stream = stream;
_password = password;
_isMultiVolume = isMultiVolume;
}
internal override Stream GetCompressedStream()
@@ -42,4 +46,7 @@ internal class SeekableFilePart : RarFilePart
}
internal override string FilePartName => "Unknown Stream - File Entry: " + FileHeader.FileName;
public override bool SupportsMultiThreading =>
!_isMultiVolume && _stream is SourceStream ss && ss.IsFileMode && ss.Files.Count == 1;
}

View File

@@ -9,11 +9,28 @@ namespace SharpCompress.Archives.Rar;
internal class StreamRarArchiveVolume : RarVolume
{
internal StreamRarArchiveVolume(Stream stream, ReaderOptions options, int index)
: base(StreamingMode.Seekable, stream, options, index) { }
private readonly bool _isMultiVolume;
internal StreamRarArchiveVolume(
Stream stream,
ReaderOptions options,
int index,
bool isMultiVolume
)
: base(StreamingMode.Seekable, stream, options, index)
{
_isMultiVolume = isMultiVolume;
}
internal override IEnumerable<RarFilePart> ReadFileParts() => GetVolumeFileParts();
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
new SeekableFilePart(markHeader, fileHeader, Index, Stream, ReaderOptions.Password);
new SeekableRarFilePart(
markHeader,
fileHeader,
Index,
Stream,
ReaderOptions.Password,
_isMultiVolume
);
}

View File

@@ -283,7 +283,12 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
yield return new ZipArchiveEntry(
this,
new SeekableZipFilePart(headerFactory.NotNull(), deh, s)
new SeekableZipFilePart(
headerFactory.NotNull(),
deh,
s,
IsMultiVolume
)
);
}
break;
@@ -385,4 +390,6 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
((IStreamStack)stream).StackSeek(0);
return ZipReader.Open(stream, ReaderOptions, Entries);
}
public override bool SupportsMultiThreading => !IsMultiVolume;
}

View File

@@ -23,5 +23,7 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
public bool IsComplete => true;
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
#endregion
}

View File

@@ -57,7 +57,7 @@ namespace SharpCompress.Common.Arc
return value switch
{
1 or 2 => CompressionType.None,
3 => CompressionType.RLE90,
3 => CompressionType.Packed,
4 => CompressionType.Squeezed,
5 or 6 or 7 or 8 => CompressionType.Crunched,
9 => CompressionType.Squashed,

View File

@@ -44,7 +44,7 @@ namespace SharpCompress.Common.Arc
Header.CompressedSize
);
break;
case CompressionType.RLE90:
case CompressionType.Packed:
compressedStream = new RunLength90Stream(
_stream,
(int)Header.CompressedSize
@@ -54,6 +54,14 @@ namespace SharpCompress.Common.Arc
compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize);
break;
case CompressionType.Crunched:
if (Header.OriginalSize > 128 * 1024)
{
throw new NotSupportedException(
"CompressionMethod: "
+ Header.CompressionMethod
+ " with size > 128KB"
);
}
compressedStream = new ArcLzwStream(
_stream,
(int)Header.CompressedSize,

View File

@@ -41,7 +41,7 @@ namespace SharpCompress.Common.Arj
case CompressionMethod.CompressedMost:
case CompressionMethod.Compressed:
case CompressionMethod.CompressedFaster:
if (Header.CompressedSize > 128 * 1024)
if (Header.OriginalSize > 128 * 1024)
{
throw new NotSupportedException(
"CompressionMethod: "

View File

@@ -23,7 +23,7 @@ public enum CompressionType
Reduce4,
Explode,
Squeezed,
RLE90,
Packed,
Crunched,
Squashed,
Crushed,

View File

@@ -87,4 +87,5 @@ public abstract class Entry : IEntry
/// Entry file attribute.
/// </summary>
public virtual int? Attrib => throw new NotImplementedException();
public virtual bool SupportsMultiThreading => false;
}

View File

@@ -128,7 +128,7 @@ internal static class ExtractionMethods
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
Func<string, ExtractionOptions?, Task> writeAsync,
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
CancellationToken cancellationToken = default
)
{
@@ -189,7 +189,7 @@ internal static class ExtractionMethods
"Entry is trying to write a file outside of the destination directory."
);
}
await writeAsync(destinationFileName, options).ConfigureAwait(false);
await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false);
}
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
{
@@ -201,7 +201,7 @@ internal static class ExtractionMethods
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
Func<string, FileMode, Task> openAndWriteAsync,
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
CancellationToken cancellationToken = default
)
{
@@ -225,7 +225,8 @@ internal static class ExtractionMethods
fm = FileMode.CreateNew;
}
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
await openAndWriteAsync(destinationFileName, fm, cancellationToken)
.ConfigureAwait(false);
entry.PreserveExtractionOptions(destinationFileName, options);
}
}

View File

@@ -14,4 +14,6 @@ public abstract class FilePart
internal abstract Stream? GetCompressedStream();
internal abstract Stream? GetRawStream();
internal bool Skipped { get; set; }
public virtual bool SupportsMultiThreading => false;
}

View File

@@ -21,4 +21,5 @@ public interface IEntry
DateTime? LastModifiedTime { get; }
long Size { get; }
int? Attrib { get; }
bool SupportsMultiThreading { get; }
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
namespace SharpCompress.Common.Zip;
@@ -7,13 +8,19 @@ internal class SeekableZipFilePart : ZipFilePart
{
private bool _isLocalHeaderLoaded;
private readonly SeekableZipHeaderFactory _headerFactory;
private readonly bool _isMultiVolume;
internal SeekableZipFilePart(
SeekableZipHeaderFactory headerFactory,
DirectoryEntryHeader header,
Stream stream
Stream stream,
bool isMultiVolume
)
: base(header, stream) => _headerFactory = headerFactory;
: base(header, stream)
{
_headerFactory = headerFactory;
_isMultiVolume = isMultiVolume;
}
internal override Stream GetCompressedStream()
{
@@ -30,8 +37,20 @@ internal class SeekableZipFilePart : ZipFilePart
protected override Stream CreateBaseStream()
{
if (!_isMultiVolume && BaseStream is SourceStream ss)
{
if (ss.IsFileMode && ss.Files.Count == 1)
{
var fileStream = ss.CurrentFile.OpenRead();
fileStream.Position = Header.DataStartPosition.NotNull();
return fileStream;
}
}
BaseStream.Position = Header.DataStartPosition.NotNull();
return BaseStream;
}
public override bool SupportsMultiThreading =>
!_isMultiVolume && BaseStream is SourceStream ss && ss.IsFileMode && ss.Files.Count == 1;
}

View File

@@ -62,10 +62,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
base.Dispose(disposing);
if (disposing)
{
#if DEBUG_STREAMS
this.DebugDispose(typeof(MultiVolumeReadOnlyStream));
#endif
if (filePartEnumerator != null)
{
filePartEnumerator.Dispose();

View File

@@ -82,9 +82,6 @@ internal class RarStream : Stream, IStreamStack
{
if (disposing)
{
#if DEBUG_STREAMS
this.DebugDispose(typeof(RarStream));
#endif
ArrayPool<byte>.Shared.Return(this.tmpBuffer);
this.tmpBuffer = null;
}

View File

@@ -15,7 +15,7 @@ public class SourceStream : Stream, IStreamStack
#endif
int IStreamStack.DefaultBufferSize { get; set; }
Stream IStreamStack.BaseStream() => _streams[_stream];
Stream IStreamStack.BaseStream() => _streams[_streamIndex];
int IStreamStack.BufferSize
{
@@ -35,7 +35,7 @@ public class SourceStream : Stream, IStreamStack
private readonly List<Stream> _streams;
private readonly Func<int, FileInfo?>? _getFilePart;
private readonly Func<int, Stream?>? _getStreamPart;
private int _stream;
private int _streamIndex;
public SourceStream(FileInfo file, Func<int, FileInfo?> getPart, ReaderOptions options)
: this(null, null, file, getPart, options) { }
@@ -59,7 +59,7 @@ public class SourceStream : Stream, IStreamStack
if (!IsFileMode)
{
_streams.Add(stream!);
_streams.Add(stream.NotNull("stream is null"));
_getStreamPart = getStreamPart;
_getFilePart = _ => null;
if (stream is FileStream fileStream)
@@ -69,12 +69,12 @@ public class SourceStream : Stream, IStreamStack
}
else
{
_files.Add(file!);
_files.Add(file.NotNull("file is null"));
_streams.Add(_files[0].OpenRead());
_getFilePart = getFilePart;
_getStreamPart = _ => null;
}
_stream = 0;
_streamIndex = 0;
_prevSize = 0;
#if DEBUG_STREAMS
@@ -93,10 +93,12 @@ public class SourceStream : Stream, IStreamStack
public ReaderOptions ReaderOptions { get; }
public bool IsFileMode { get; }
public IEnumerable<FileInfo> Files => _files;
public IEnumerable<Stream> Streams => _streams;
public IReadOnlyList<FileInfo> Files => _files;
public IReadOnlyList<Stream> Streams => _streams;
private Stream Current => _streams[_stream];
private Stream Current => _streams[_streamIndex];
public FileInfo CurrentFile => _files[_streamIndex];
public bool LoadStream(int index) //ensure all parts to id are loaded
{
@@ -107,7 +109,7 @@ public class SourceStream : Stream, IStreamStack
var f = _getFilePart.NotNull("GetFilePart is null")(_streams.Count);
if (f == null)
{
_stream = _streams.Count - 1;
_streamIndex = _streams.Count - 1;
return false;
}
//throw new Exception($"File part {idx} not available.");
@@ -119,7 +121,7 @@ public class SourceStream : Stream, IStreamStack
var s = _getStreamPart.NotNull("GetStreamPart is null")(_streams.Count);
if (s == null)
{
_stream = _streams.Count - 1;
_streamIndex = _streams.Count - 1;
return false;
}
//throw new Exception($"Stream part {idx} not available.");
@@ -137,10 +139,10 @@ public class SourceStream : Stream, IStreamStack
{
if (LoadStream(idx))
{
_stream = idx;
_streamIndex = idx;
}
return _stream == idx;
return _streamIndex == idx;
}
public override bool CanRead => true;
@@ -184,7 +186,7 @@ public class SourceStream : Stream, IStreamStack
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
if (!SetStream(_streamIndex + 1))
{
break;
}
@@ -223,7 +225,7 @@ public class SourceStream : Stream, IStreamStack
while (_prevSize + Current.Length < pos)
{
_prevSize += Current.Length;
SetStream(_stream + 1);
SetStream(_streamIndex + 1);
}
}
@@ -273,7 +275,7 @@ public class SourceStream : Stream, IStreamStack
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
if (!SetStream(_streamIndex + 1))
{
break;
}
@@ -322,7 +324,7 @@ public class SourceStream : Stream, IStreamStack
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
if (!SetStream(_streamIndex + 1))
{
break;
}

View File

@@ -82,7 +82,7 @@ public static class IReaderExtensions
reader.Entry,
destinationDirectory,
options,
(fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken),
reader.WriteEntryToFileAsync,
cancellationToken
)
.ConfigureAwait(false);
@@ -101,10 +101,10 @@ public static class IReaderExtensions
reader.Entry,
destinationFileName,
options,
async (x, fm) =>
async (x, fm, ct) =>
{
using var fs = File.Open(destinationFileName, fm);
await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false);
await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
},
cancellationToken
)

View File

@@ -48,7 +48,29 @@ internal class ZipCentralDirectoryEntry
var decompressedvalue = zip64 ? uint.MaxValue : (uint)Decompressed;
var headeroffsetvalue = zip64 ? uint.MaxValue : (uint)HeaderOffset;
var extralength = zip64 ? (2 + 2 + 8 + 8 + 8 + 4) : 0;
var version = (byte)(zip64 ? 45 : 20); // Version 20 required for deflate/encryption
// Determine version needed to extract:
// - Version 63 for LZMA, PPMd, BZip2, ZStandard (advanced compression methods)
// - Version 45 for Zip64 extensions (when Zip64HeaderOffset != 0 or actual sizes require it)
// - Version 20 for standard Deflate/None compression
byte version;
if (
compression == ZipCompressionMethod.LZMA
|| compression == ZipCompressionMethod.PPMd
|| compression == ZipCompressionMethod.BZip2
|| compression == ZipCompressionMethod.ZStandard
)
{
version = 63;
}
else if (zip64 || Zip64HeaderOffset != 0)
{
version = 45;
}
else
{
version = 20;
}
var flags = Equals(archiveEncoding.GetEncoding(), Encoding.UTF8)
? HeaderFlags.Efs

View File

@@ -27,5 +27,22 @@ namespace SharpCompress.Test.Arc
[Fact]
public void Arc_Crunched_Read() => Read("Arc.crunched.arc");
[Theory]
[InlineData("Arc.crunched.largefile.arc", CompressionType.Crunched)]
public void Arc_LargeFile_ShouldThrow(string fileName, CompressionType compressionType)
{
var exception = Assert.Throws<NotSupportedException>(() =>
ReadForBufferBoundaryCheck(fileName, compressionType)
);
}
[Theory]
[InlineData("Arc.uncompressed.largefile.arc", CompressionType.None)]
[InlineData("Arc.squeezed.largefile.arc", CompressionType.Squeezed)]
public void Arc_LargeFileTest_Read(string fileName, CompressionType compressionType)
{
ReadForBufferBoundaryCheck(fileName, compressionType);
}
}
}

View File

@@ -134,6 +134,7 @@ public class ArchiveTests : ReaderTests
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
Assert.False(entry.SupportsMultiThreading);
entry.WriteToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
@@ -266,6 +267,31 @@ public class ArchiveTests : ReaderTests
VerifyFiles();
}
protected async Task ArchiveFileRead_Multithreaded(
IArchiveFactory archiveFactory,
string testArchive,
ReaderOptions? readerOptions = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
var tasks = new List<Task>();
using (var archive = archiveFactory.Open(new FileInfo(testArchive), readerOptions))
{
Assert.True(archive.SupportsMultiThreading);
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
Assert.True(entry.SupportsMultiThreading);
var t = entry.WriteToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
tasks.Add(t);
}
}
await Task.WhenAll(tasks);
VerifyFiles();
}
protected void ArchiveFileRead(
IArchiveFactory archiveFactory,
string testArchive,
@@ -289,6 +315,11 @@ public class ArchiveTests : ReaderTests
protected void ArchiveFileRead(string testArchive, ReaderOptions? readerOptions = null) =>
ArchiveFileRead(ArchiveFactory.AutoFactory, testArchive, readerOptions);
protected Task ArchiveFileRead_Multithreaded(
string testArchive,
ReaderOptions? readerOptions = null
) => ArchiveFileRead_Multithreaded(ArchiveFactory.AutoFactory, testArchive, readerOptions);
protected void ArchiveFileSkip(
string testArchive,
string fileOrder,

View File

@@ -45,16 +45,14 @@ 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",
]
)
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",
])
);
}
@@ -65,7 +63,7 @@ namespace SharpCompress.Test.Arj
public void Arj_LargeFile_ShouldThrow(string fileName, CompressionType compressionType)
{
var exception = Assert.Throws<NotSupportedException>(() =>
Arj_LargeFileTest_Read(fileName, compressionType)
ReadForBufferBoundaryCheck(fileName, compressionType)
);
}
@@ -74,24 +72,7 @@ namespace SharpCompress.Test.Arj
[InlineData("Arj.method4.largefile.arj", CompressionType.ArjLZ77)]
public void Arj_LargeFileTest_Read(string fileName, CompressionType compressionType)
{
using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, fileName)))
using (
var reader = ReaderFactory.Open(stream, new ReaderOptions { LookForHeader = true })
)
{
while (reader.MoveToNextEntry())
{
Assert.Equal(compressionType, reader.Entry.CompressionType);
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
CompareFilesByPath(
Path.Combine(SCRATCH_FILES_PATH, "news.txt"),
Path.Combine(MISC_TEST_FILES_PATH, "news.txt")
);
ReadForBufferBoundaryCheck(fileName, compressionType);
}
private void DoArj_Multi_Reader(string[] archives)

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
@@ -292,9 +293,15 @@ public class RarArchiveTests : ArchiveTests
[Fact]
public void Rar_ArchiveFileRead() => ArchiveFileRead("Rar.rar");
[Fact]
public Task Rar_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar.rar");
[Fact]
public void Rar5_ArchiveFileRead() => ArchiveFileRead("Rar5.rar");
[Fact]
public Task Rar5_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar5.rar");
[Fact]
public void Rar_ArchiveFileRead_HasDirectories() =>
DoRar_ArchiveFileRead_HasDirectories("Rar.rar");
@@ -359,6 +366,9 @@ public class RarArchiveTests : ArchiveTests
[Fact]
public void Rar2_ArchiveFileRead() => ArchiveFileRead("Rar2.rar");
[Fact]
public Task Rar2_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar2.rar");
[Fact]
public void Rar15_ArchiveFileRead()
{

View File

@@ -15,29 +15,25 @@ public class RarReaderAsyncTests : ReaderTests
{
[Fact]
public async Task Rar_Multi_Reader_Async() =>
await DoRar_Multi_Reader_Async(
[
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]
);
await DoRar_Multi_Reader_Async([
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]);
[Fact]
public async Task Rar5_Multi_Reader_Async() =>
await DoRar_Multi_Reader_Async(
[
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]
);
await DoRar_Multi_Reader_Async([
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]);
private async Task DoRar_Multi_Reader_Async(string[] archives)
{
@@ -95,29 +91,25 @@ public class RarReaderAsyncTests : ReaderTests
[Fact]
public async Task Rar_Multi_Reader_Delete_Files_Async() =>
await DoRar_Multi_Reader_Delete_Files_Async(
[
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]
);
await DoRar_Multi_Reader_Delete_Files_Async([
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]);
[Fact]
public async Task Rar5_Multi_Reader_Delete_Files_Async() =>
await DoRar_Multi_Reader_Delete_Files_Async(
[
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]
);
await DoRar_Multi_Reader_Delete_Files_Async([
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]);
private async Task DoRar_Multi_Reader_Delete_Files_Async(string[] archives)
{

View File

@@ -14,29 +14,25 @@ public class RarReaderTests : ReaderTests
{
[Fact]
public void Rar_Multi_Reader() =>
DoRar_Multi_Reader(
[
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]
);
DoRar_Multi_Reader([
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]);
[Fact]
public void Rar5_Multi_Reader() =>
DoRar_Multi_Reader(
[
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]
);
DoRar_Multi_Reader([
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]);
private void DoRar_Multi_Reader(string[] archives)
{
@@ -61,16 +57,14 @@ public class RarReaderTests : ReaderTests
[Fact]
public void Rar_Multi_Reader_Encrypted() =>
DoRar_Multi_Reader_Encrypted(
[
"Rar.EncryptedParts.part01.rar",
"Rar.EncryptedParts.part02.rar",
"Rar.EncryptedParts.part03.rar",
"Rar.EncryptedParts.part04.rar",
"Rar.EncryptedParts.part05.rar",
"Rar.EncryptedParts.part06.rar",
]
);
DoRar_Multi_Reader_Encrypted([
"Rar.EncryptedParts.part01.rar",
"Rar.EncryptedParts.part02.rar",
"Rar.EncryptedParts.part03.rar",
"Rar.EncryptedParts.part04.rar",
"Rar.EncryptedParts.part05.rar",
"Rar.EncryptedParts.part06.rar",
]);
private void DoRar_Multi_Reader_Encrypted(string[] archives) =>
Assert.Throws<InvalidFormatException>(() =>
@@ -97,29 +91,25 @@ public class RarReaderTests : ReaderTests
[Fact]
public void Rar_Multi_Reader_Delete_Files() =>
DoRar_Multi_Reader_Delete_Files(
[
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]
);
DoRar_Multi_Reader_Delete_Files([
"Rar.multi.part01.rar",
"Rar.multi.part02.rar",
"Rar.multi.part03.rar",
"Rar.multi.part04.rar",
"Rar.multi.part05.rar",
"Rar.multi.part06.rar",
]);
[Fact]
public void Rar5_Multi_Reader_Delete_Files() =>
DoRar_Multi_Reader_Delete_Files(
[
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]
);
DoRar_Multi_Reader_Delete_Files([
"Rar5.multi.part01.rar",
"Rar5.multi.part02.rar",
"Rar5.multi.part03.rar",
"Rar5.multi.part04.rar",
"Rar5.multi.part05.rar",
"Rar5.multi.part06.rar",
]);
private void DoRar_Multi_Reader_Delete_Files(string[] archives)
{
@@ -407,16 +397,14 @@ public class RarReaderTests : ReaderTests
Path.Combine("exe", "test.exe"),
}
);
using var reader = RarReader.Open(
[
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part01.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part02.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part03.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part04.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part05.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part06.rar"),
]
);
using var reader = RarReader.Open([
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part01.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part02.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part03.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part04.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part05.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part06.rar"),
]);
while (reader.MoveToNextEntry())
{
Assert.Equal(expectedOrder.Pop(), reader.Entry.Key);

View File

@@ -176,6 +176,27 @@ public abstract class ReaderTests : TestBase
}
}
protected void ReadForBufferBoundaryCheck(string fileName, CompressionType compressionType)
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, fileName));
using var reader = ReaderFactory.Open(stream, new ReaderOptions { LookForHeader = true });
while (reader.MoveToNextEntry())
{
Assert.Equal(compressionType, reader.Entry.CompressionType);
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
CompareFilesByPath(
Path.Combine(SCRATCH_FILES_PATH, "alice29.txt"),
Path.Combine(MISC_TEST_FILES_PATH, "alice29.txt")
);
}
protected void Iterate(
string testArchive,
string fileOrder,

View File

@@ -74,9 +74,21 @@ public class XzIndexAsyncTests : XzTestsBase
public async Task SkipsPaddingAsync()
{
// Index with 3-byte padding.
using Stream badStream = new MemoryStream(
[0x00, 0x01, 0x10, 0x80, 0x01, 0x00, 0x00, 0x00, 0xB1, 0x01, 0xD9, 0xC9, 0xFF]
);
using Stream badStream = new MemoryStream([
0x00,
0x01,
0x10,
0x80,
0x01,
0x00,
0x00,
0x00,
0xB1,
0x01,
0xD9,
0xC9,
0xFF,
]);
var br = new BinaryReader(badStream);
var index = new XZIndex(br, false);
await index.ProcessAsync().ConfigureAwait(false);

View File

@@ -71,9 +71,21 @@ public class XzIndexTests : XzTestsBase
public void SkipsPadding()
{
// Index with 3-byte padding.
using Stream badStream = new MemoryStream(
[0x00, 0x01, 0x10, 0x80, 0x01, 0x00, 0x00, 0x00, 0xB1, 0x01, 0xD9, 0xC9, 0xFF]
);
using Stream badStream = new MemoryStream([
0x00,
0x01,
0x10,
0x80,
0x01,
0x00,
0x00,
0x00,
0xB1,
0x01,
0xD9,
0xC9,
0xFF,
]);
var br = new BinaryReader(badStream);
var index = new XZIndex(br, false);
index.Process();

View File

@@ -0,0 +1,441 @@
using System;
using System.Buffers.Binary;
using System.IO;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
/// <summary>
/// Tests for verifying version consistency between Local File Header (LFH)
/// and Central Directory File Header (CDFH) when using Zip64.
/// </summary>
public class Zip64VersionConsistencyTests : WriterTests
{
public Zip64VersionConsistencyTests()
: base(ArchiveType.Zip) { }
[Fact]
public void Zip64_Small_File_With_UseZip64_Should_Have_Matching_Versions()
{
// Create a zip with UseZip64=true but with a small file
var filename = Path.Combine(SCRATCH2_FILES_PATH, "zip64_version_test.zip");
if (File.Exists(filename))
{
File.Delete(filename);
}
// Create archive with UseZip64=true
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
{
LeaveStreamOpen = false,
UseZip64 = true,
};
ZipArchive zipArchive = ZipArchive.Create();
zipArchive.AddEntry("empty", new MemoryStream());
zipArchive.SaveTo(filename, writerOptions);
// Now read the raw bytes to verify version consistency
using var fs = File.OpenRead(filename);
using var br = new BinaryReader(fs);
// Read Local File Header
var lfhSignature = br.ReadUInt32();
Assert.Equal(0x04034b50u, lfhSignature); // Local file header signature
var lfhVersion = br.ReadUInt16();
// Skip to Central Directory
// Find Central Directory by searching from the end
fs.Seek(-22, SeekOrigin.End); // Min EOCD size
var eocdSignature = br.ReadUInt32();
if (eocdSignature != 0x06054b50u)
{
// Might have Zip64 EOCD, search backwards
fs.Seek(-100, SeekOrigin.End);
var buffer = new byte[100];
fs.Read(buffer, 0, 100);
// Find EOCD signature
for (int i = buffer.Length - 4; i >= 0; i--)
{
if (BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(i)) == 0x06054b50u)
{
fs.Seek(-100 + i, SeekOrigin.End);
break;
}
}
}
// Read EOCD
fs.Seek(-22, SeekOrigin.End);
br.ReadUInt32(); // EOCD signature
br.ReadUInt16(); // disk number
br.ReadUInt16(); // disk with central dir
br.ReadUInt16(); // entries on this disk
br.ReadUInt16(); // total entries
br.ReadUInt32(); // central directory size (unused)
var cdOffset = br.ReadUInt32();
// If Zip64, need to read from Zip64 EOCD
if (cdOffset == 0xFFFFFFFF)
{
// Find Zip64 EOCD Locator
fs.Seek(-22 - 20, SeekOrigin.End);
var z64eocdlSig = br.ReadUInt32();
if (z64eocdlSig == 0x07064b50u)
{
br.ReadUInt32(); // disk number
var z64eocdOffset = br.ReadUInt64();
br.ReadUInt32(); // total disks
// Read Zip64 EOCD
fs.Seek((long)z64eocdOffset, SeekOrigin.Begin);
br.ReadUInt32(); // signature
br.ReadUInt64(); // size of EOCD64
br.ReadUInt16(); // version made by
br.ReadUInt16(); // version needed
br.ReadUInt32(); // disk number
br.ReadUInt32(); // disk with CD
br.ReadUInt64(); // entries on disk
br.ReadUInt64(); // total entries
br.ReadUInt64(); // CD size
cdOffset = (uint)br.ReadUInt64(); // CD offset
}
}
// Read Central Directory Header
fs.Seek(cdOffset, SeekOrigin.Begin);
var cdhSignature = br.ReadUInt32();
Assert.Equal(0x02014b50u, cdhSignature); // Central directory header signature
br.ReadUInt16(); // version made by
var cdhVersionNeeded = br.ReadUInt16();
// The versions should match when UseZip64 is true
Assert.Equal(lfhVersion, cdhVersionNeeded);
}
[Fact]
public void Zip64_Small_File_Without_UseZip64_Should_Have_Version_20()
{
// Create a zip without UseZip64
var filename = Path.Combine(SCRATCH2_FILES_PATH, "no_zip64_version_test.zip");
if (File.Exists(filename))
{
File.Delete(filename);
}
// Create archive without UseZip64
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
{
LeaveStreamOpen = false,
UseZip64 = false,
};
ZipArchive zipArchive = ZipArchive.Create();
zipArchive.AddEntry("empty", new MemoryStream());
zipArchive.SaveTo(filename, writerOptions);
// Read the raw bytes
using var fs = File.OpenRead(filename);
using var br = new BinaryReader(fs);
// Read Local File Header version
var lfhSignature = br.ReadUInt32();
Assert.Equal(0x04034b50u, lfhSignature);
var lfhVersion = br.ReadUInt16();
// Read Central Directory Header version
fs.Seek(-22, SeekOrigin.End);
br.ReadUInt32(); // EOCD signature
br.ReadUInt16(); // disk number
br.ReadUInt16(); // disk with central dir
br.ReadUInt16(); // entries on this disk
br.ReadUInt16(); // total entries
br.ReadUInt32(); // CD size
var cdOffset = br.ReadUInt32();
fs.Seek(cdOffset, SeekOrigin.Begin);
var cdhSignature = br.ReadUInt32();
Assert.Equal(0x02014b50u, cdhSignature);
br.ReadUInt16(); // version made by
var cdhVersionNeeded = br.ReadUInt16();
// Both should be version 20 (or less)
Assert.True(lfhVersion <= 20);
Assert.Equal(lfhVersion, cdhVersionNeeded);
}
[Fact]
public void LZMA_Compression_Should_Use_Version_63()
{
// Create a zip with LZMA compression
var filename = Path.Combine(SCRATCH2_FILES_PATH, "lzma_version_test.zip");
if (File.Exists(filename))
{
File.Delete(filename);
}
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.LZMA)
{
LeaveStreamOpen = false,
UseZip64 = false,
};
ZipArchive zipArchive = ZipArchive.Create();
var data = new byte[100];
new Random(42).NextBytes(data);
zipArchive.AddEntry("test.bin", new MemoryStream(data));
zipArchive.SaveTo(filename, writerOptions);
// Read the raw bytes
using var fs = File.OpenRead(filename);
using var br = new BinaryReader(fs);
// Read Local File Header version
var lfhSignature = br.ReadUInt32();
Assert.Equal(0x04034b50u, lfhSignature);
var lfhVersion = br.ReadUInt16();
// Read Central Directory Header version
fs.Seek(-22, SeekOrigin.End);
br.ReadUInt32(); // EOCD signature
br.ReadUInt16(); // disk number
br.ReadUInt16(); // disk with central dir
br.ReadUInt16(); // entries on this disk
br.ReadUInt16(); // total entries
br.ReadUInt32(); // CD size
var cdOffset = br.ReadUInt32();
fs.Seek(cdOffset, SeekOrigin.Begin);
var cdhSignature = br.ReadUInt32();
Assert.Equal(0x02014b50u, cdhSignature);
br.ReadUInt16(); // version made by
var cdhVersionNeeded = br.ReadUInt16();
// Both should be version 63 for LZMA
Assert.Equal(63, lfhVersion);
Assert.Equal(lfhVersion, cdhVersionNeeded);
}
[Fact]
public void PPMd_Compression_Should_Use_Version_63()
{
// Create a zip with PPMd compression
var filename = Path.Combine(SCRATCH2_FILES_PATH, "ppmd_version_test.zip");
if (File.Exists(filename))
{
File.Delete(filename);
}
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.PPMd)
{
LeaveStreamOpen = false,
UseZip64 = false,
};
ZipArchive zipArchive = ZipArchive.Create();
var data = new byte[100];
new Random(42).NextBytes(data);
zipArchive.AddEntry("test.bin", new MemoryStream(data));
zipArchive.SaveTo(filename, writerOptions);
// Read the raw bytes
using var fs = File.OpenRead(filename);
using var br = new BinaryReader(fs);
// Read Local File Header version
var lfhSignature = br.ReadUInt32();
Assert.Equal(0x04034b50u, lfhSignature);
var lfhVersion = br.ReadUInt16();
// Read Central Directory Header version
fs.Seek(-22, SeekOrigin.End);
br.ReadUInt32(); // EOCD signature
br.ReadUInt16(); // disk number
br.ReadUInt16(); // disk with central dir
br.ReadUInt16(); // entries on this disk
br.ReadUInt16(); // total entries
br.ReadUInt32(); // CD size
var cdOffset = br.ReadUInt32();
fs.Seek(cdOffset, SeekOrigin.Begin);
var cdhSignature = br.ReadUInt32();
Assert.Equal(0x02014b50u, cdhSignature);
br.ReadUInt16(); // version made by
var cdhVersionNeeded = br.ReadUInt16();
// Both should be version 63 for PPMd
Assert.Equal(63, lfhVersion);
Assert.Equal(lfhVersion, cdhVersionNeeded);
}
[Fact]
public void Zip64_Multiple_Small_Files_With_UseZip64_Should_Have_Matching_Versions()
{
// Create a zip with UseZip64=true but with multiple small files
var filename = Path.Combine(SCRATCH2_FILES_PATH, "zip64_version_multiple_test.zip");
if (File.Exists(filename))
{
File.Delete(filename);
}
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
{
LeaveStreamOpen = false,
UseZip64 = true,
};
ZipArchive zipArchive = ZipArchive.Create();
for (int i = 0; i < 5; i++)
{
var data = new byte[100];
new Random(i).NextBytes(data);
zipArchive.AddEntry($"file{i}.bin", new MemoryStream(data));
}
zipArchive.SaveTo(filename, writerOptions);
// Verify that all entries have matching versions
using var fs = File.OpenRead(filename);
using var br = new BinaryReader(fs);
// Read all LFH versions
var lfhVersions = new System.Collections.Generic.List<ushort>();
while (true)
{
var sig = br.ReadUInt32();
if (sig == 0x04034b50u) // LFH signature
{
var version = br.ReadUInt16();
lfhVersions.Add(version);
// Skip rest of LFH
br.ReadUInt16(); // flags
br.ReadUInt16(); // compression
br.ReadUInt32(); // mod time
br.ReadUInt32(); // crc
br.ReadUInt32(); // compressed size
br.ReadUInt32(); // uncompressed size
var fnLen = br.ReadUInt16();
var extraLen = br.ReadUInt16();
fs.Seek(fnLen + extraLen, SeekOrigin.Current);
// Skip compressed data by reading compressed size from extra field if zip64
// For simplicity in this test, we'll just find the next signature
var found = false;
while (fs.Position < fs.Length - 4)
{
var b = br.ReadByte();
if (b == 0x50)
{
var nextBytes = br.ReadBytes(3);
if (
(nextBytes[0] == 0x4b && nextBytes[1] == 0x03 && nextBytes[2] == 0x04)
|| // LFH
(nextBytes[0] == 0x4b && nextBytes[1] == 0x01 && nextBytes[2] == 0x02)
) // CDH
{
fs.Seek(-4, SeekOrigin.Current);
found = true;
break;
}
}
}
if (!found)
{
break;
}
}
else if (sig == 0x02014b50u) // CDH signature
{
break; // Reached central directory
}
else
{
break; // Unknown signature
}
}
// Find Central Directory
fs.Seek(-22, SeekOrigin.End);
br.ReadUInt32(); // EOCD signature
br.ReadUInt16(); // disk number
br.ReadUInt16(); // disk with central dir
br.ReadUInt16(); // entries on this disk
var totalEntries = br.ReadUInt16();
br.ReadUInt32(); // CD size
var cdOffset = br.ReadUInt32();
// Check if we need Zip64 EOCD
if (cdOffset == 0xFFFFFFFF)
{
fs.Seek(-22 - 20, SeekOrigin.End);
var z64eocdlSig = br.ReadUInt32();
if (z64eocdlSig == 0x07064b50u)
{
br.ReadUInt32(); // disk number
var z64eocdOffset = br.ReadUInt64();
fs.Seek((long)z64eocdOffset, SeekOrigin.Begin);
br.ReadUInt32(); // signature
br.ReadUInt64(); // size
br.ReadUInt16(); // version made by
br.ReadUInt16(); // version needed
br.ReadUInt32(); // disk number
br.ReadUInt32(); // disk with CD
br.ReadUInt64(); // entries on disk
totalEntries = (ushort)br.ReadUInt64(); // total entries
br.ReadUInt64(); // CD size
cdOffset = (uint)br.ReadUInt64(); // CD offset
}
}
// Read CDH versions
fs.Seek(cdOffset, SeekOrigin.Begin);
var cdhVersions = new System.Collections.Generic.List<ushort>();
for (int i = 0; i < totalEntries; i++)
{
var sig = br.ReadUInt32();
Assert.Equal(0x02014b50u, sig);
br.ReadUInt16(); // version made by
var version = br.ReadUInt16();
cdhVersions.Add(version);
// Skip rest of CDH
br.ReadUInt16(); // flags
br.ReadUInt16(); // compression
br.ReadUInt32(); // mod time
br.ReadUInt32(); // crc
br.ReadUInt32(); // compressed size
br.ReadUInt32(); // uncompressed size
var fnLen = br.ReadUInt16();
var extraLen = br.ReadUInt16();
var commentLen = br.ReadUInt16();
br.ReadUInt16(); // disk number start
br.ReadUInt16(); // internal attributes
br.ReadUInt32(); // external attributes
br.ReadUInt32(); // LFH offset
fs.Seek(fnLen + extraLen + commentLen, SeekOrigin.Current);
}
// Verify all versions match
Assert.Equal(lfhVersions.Count, cdhVersions.Count);
for (int i = 0; i < lfhVersions.Count; i++)
{
Assert.Equal(lfhVersions[i], cdhVersions[i]);
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
@@ -88,6 +89,10 @@ public class ZipArchiveTests : ArchiveTests
[Fact]
public void Zip_Deflate_ArchiveFileRead() => ArchiveFileRead("Zip.deflate.zip");
[Fact]
public Task Zip_Deflate_ArchiveFileRead_Multithreaded() =>
ArchiveFileRead_Multithreaded("Zip.deflate.zip");
[Fact]
public void Zip_Deflate_ArchiveExtractToDirectory() =>
ArchiveExtractToDirectory("Zip.deflate.zip");

View File

@@ -4,9 +4,9 @@
".NETFramework,Version=v4.8": {
"AwesomeAssertions": {
"type": "Direct",
"requested": "[9.2.1, )",
"resolved": "9.2.1",
"contentHash": "lbwhyQNXxxEGx4oCbFqNfFy2DsywsvNhN6qoOjY4wwhMgI2L9+YrxjyF/M0io99yrvWV1Cjj12LP2QGcC43Uhw==",
"requested": "[9.3.0, )",
"resolved": "9.3.0",
"contentHash": "8lGLYap2ec2gNLgjf2xKZaKLpQ7j36oJvrYzBVVpNAumqnxRdevqqhEF66qxE92f8y2+zsbQ061DeHG61ZhzaQ==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
@@ -199,9 +199,9 @@
"net8.0": {
"AwesomeAssertions": {
"type": "Direct",
"requested": "[9.2.1, )",
"resolved": "9.2.1",
"contentHash": "lbwhyQNXxxEGx4oCbFqNfFy2DsywsvNhN6qoOjY4wwhMgI2L9+YrxjyF/M0io99yrvWV1Cjj12LP2QGcC43Uhw=="
"requested": "[9.3.0, )",
"resolved": "9.3.0",
"contentHash": "8lGLYap2ec2gNLgjf2xKZaKLpQ7j36oJvrYzBVVpNAumqnxRdevqqhEF66qxE92f8y2+zsbQ061DeHG61ZhzaQ=="
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff