using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Compressors.Xz; using SharpCompress.Crypto; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Zip; using Xunit; namespace SharpCompress.Test; public class ArchiveTests : ReaderTests { protected void ArchiveGetParts(IEnumerable testArchives) { var arcs = testArchives.Select(a => Path.Combine(TEST_ARCHIVES_PATH, a)).ToArray(); var found = ArchiveFactory.GetFileParts(arcs[0]).ToArray(); Assert.Equal(arcs.Length, found.Length); for (var i = 0; i < arcs.Length; i++) { Assert.Equal(arcs[i], found[i]); } } protected void ArchiveStreamReadExtractAll(string testArchive, CompressionType compression) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); ArchiveStreamReadExtractAll(new[] { testArchive }, compression); } protected void ArchiveStreamReadExtractAll( IEnumerable testArchives, CompressionType compression ) { foreach (var path in testArchives) { using (var stream = SharpCompressStream.CreateNonDisposing(File.OpenRead(path))) { try { using var archive = ArchiveFactory.OpenArchive(stream); Assert.True(archive.IsSolid); using (var reader = archive.ExtractAllEntries()) { UseReader(reader, compression); } VerifyFiles(); if (archive.Entries.First().CompressionType == CompressionType.Rar) { stream.ThrowOnDispose = false; return; } WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); stream.ThrowOnDispose = false; } catch (Exception) { // Otherwise this will hide the original exception. stream.ThrowOnDispose = false; throw; } } VerifyFiles(); } } protected void ArchiveStreamRead(string testArchive, ReaderOptions? readerOptions = null) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); ArchiveStreamRead( ArchiveFactory.FindFactory(testArchive), Path.GetExtension(testArchive), readerOptions, testArchive ); } protected void ArchiveStreamRead( IArchiveFactory archiveFactory, string testArchive, ReaderOptions? readerOptions = null ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); ArchiveStreamRead( archiveFactory, Path.GetExtension(testArchive), readerOptions, testArchive ); } protected void ArchiveStreamRead( string extension, ReaderOptions? readerOptions = null, params string[] testArchives ) { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchives[0]); ArchiveStreamRead( ArchiveFactory.FindFactory(testArchive), extension, readerOptions, testArchives ); } protected void ArchiveStreamRead( IArchiveFactory archiveFactory, string extension, ReaderOptions? readerOptions = null, params string[] testArchives ) => ArchiveStreamRead( archiveFactory, readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)), extension ); protected void ArchiveStreamRead( IArchiveFactory archiveFactory, ReaderOptions? readerOptions, IEnumerable testArchives, string extension ) { ExtensionTest(extension, archiveFactory); foreach (var path in testArchives) { using (var stream = SharpCompressStream.CreateNonDisposing(File.OpenRead(path))) using (var archive = archiveFactory.OpenArchive(stream, readerOptions)) { try { WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); } catch (IndexOutOfRangeException) { //SevenZipArchive_BZip2_Split test needs this stream.ThrowOnDispose = false; throw; } stream.ThrowOnDispose = false; } VerifyFiles(); } } protected void ArchiveStreamMultiRead( ReaderOptions? readerOptions = null, params string[] testArchives ) => ArchiveStreamMultiRead( readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)) ); protected void ArchiveStreamMultiRead( ReaderOptions? readerOptions, IEnumerable testArchives ) { using ( var archive = ArchiveFactory.OpenArchive( testArchives.Select(a => new FileInfo(a)), readerOptions ) ) { WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); } VerifyFiles(); } protected void ArchiveOpenStreamRead( ReaderOptions? readerOptions = null, params string[] testArchives ) => ArchiveOpenStreamRead( readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)) ); protected void ArchiveOpenStreamRead( ReaderOptions? readerOptions, IEnumerable testArchives ) { using ( var archive = ArchiveFactory.OpenArchive( testArchives.Select(f => new FileInfo(f)), readerOptions ) ) { WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); } VerifyFiles(); } protected void ArchiveOpenEntryVolumeIndexTest( int[][] results, ReaderOptions? readerOptions = null, params string[] testArchives ) => ArchiveOpenEntryVolumeIndexTest( results, readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)) ); private void ArchiveOpenEntryVolumeIndexTest( int[][] results, ReaderOptions? readerOptions, IEnumerable testArchives ) { var src = testArchives.ToArray(); using var archive = ArchiveFactory.OpenArchive( src.Select(f => new FileInfo(f)), readerOptions ); var idx = 0; foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { Assert.Equal(entry.VolumeIndexFirst, results[idx][0]); Assert.Equal(entry.VolumeIndexLast, results[idx][1]); Assert.Equal( src[entry.VolumeIndexFirst], archive.Volumes.First(a => a.Index == entry.VolumeIndexFirst).FileName ); Assert.Equal( src[entry.VolumeIndexLast], archive.Volumes.First(a => a.Index == entry.VolumeIndexLast).FileName ); idx++; } } protected void ArchiveExtractToDirectory( string testArchive, ReaderOptions? readerOptions = null ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using (var archive = ArchiveFactory.OpenArchive(new FileInfo(testArchive), readerOptions)) { archive.WriteToDirectory(SCRATCH_FILES_PATH); } VerifyFiles(); } protected void ArchiveFileRead( string testArchive, ReaderOptions? readerOptions = null, IArchiveFactory? archiveFactory = null ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); archiveFactory ??= ArchiveFactory.FindFactory(testArchive); ExtensionTest(testArchive, archiveFactory); using (var archive = archiveFactory.OpenArchive(new FileInfo(testArchive), readerOptions)) { WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); } VerifyFiles(); } private void ExtensionTest(string fullPath, IArchiveFactory archiveFactory) { var extension = Path.GetExtension(fullPath).Substring(1); if (!int.TryParse(extension, out _) && "exe" != extension) //exclude parts { extension.Should().BeOneOf(archiveFactory.GetSupportedExtensions()); } } protected void ArchiveFileSkip( string testArchive, string fileOrder, ReaderOptions? readerOptions = null ) { if (!Environment.OSVersion.IsWindows()) { fileOrder = fileOrder.Replace('\\', '/'); } var expected = new Stack(fileOrder.Split(' ')); testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using var archive = ArchiveFactory.OpenArchive(testArchive, readerOptions); foreach (var entry in archive.Entries) { Assert.Equal(expected.Pop(), entry.Key); } } /// /// Demonstrate the ExtractionOptions.PreserveFileTime and ExtractionOptions.PreserveAttributes extract options /// protected void ArchiveFileReadEx(string testArchive) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using (var archive = ArchiveFactory.OpenArchive(testArchive)) { WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH); } VerifyFilesEx(); } protected void ArchiveDeltaDistanceRead(string testArchive) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using var archive = ArchiveFactory.OpenArchive(testArchive); var hasNonExtractableEntries = archive.Entries.Any(entry => !entry.IsDirectory && entry is not IExtractableArchiveEntry ); if (hasNonExtractableEntries) { using var reader = archive.ExtractAllEntries(); while (reader.MoveToNextEntry()) { if (!reader.Entry.IsDirectory) { using var memory = new MemoryStream(); reader.WriteEntryTo(memory); VerifyDeltaDistanceStream(memory); } } return; } foreach (var entry in archive.Entries) { if (!entry.IsDirectory) { var memory = new MemoryStream(); entry.WriteTo(memory); VerifyDeltaDistanceStream(memory); } } } /// /// Calculates CRC32 for the given data using SharpCompress implementation /// protected static uint CalculateCrc32(byte[] data) => Crc32.Compute(data); /// /// Creates a writer with the specified compression type and level /// protected static IWriter CreateWriterWithLevel( Stream stream, CompressionType compressionType, int? compressionLevel = null ) { var writerOptions = compressionLevel.HasValue ? new WriterOptions(compressionType, compressionLevel.Value) : new WriterOptions(compressionType); return WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions); } protected static IAsyncWriter CreateWriterWithLevelAsync( Stream stream, CompressionType compressionType, int? compressionLevel = null ) { var writerOptions = compressionLevel.HasValue ? new WriterOptions(compressionType, compressionLevel.Value) { LeaveStreamOpen = true } : new WriterOptions(compressionType) { LeaveStreamOpen = true }; return WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), ArchiveType.Zip, writerOptions ); } /// /// Verifies archive content against expected files with CRC32 validation /// protected void VerifyArchiveContent( MemoryStream zipStream, Dictionary expectedFiles ) { zipStream.Position = 0; using var archive = ArchiveFactory.OpenArchive(zipStream); Assert.Equal(expectedFiles.Count, archive.Entries.Count()); foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) { using var entryStream = Assert .IsAssignableFrom(entry) .OpenEntryStream(); using var extractedStream = new MemoryStream(); entryStream.CopyTo(extractedStream); var extractedData = extractedStream.ToArray(); Assert.True( expectedFiles.ContainsKey(entry.Key.NotNull()), $"Unexpected entry: {entry.Key}" ); var (expectedData, expectedCrc) = expectedFiles[entry.Key.NotNull()]; var actualCrc = CalculateCrc32(extractedData); Assert.Equal(expectedCrc, actualCrc); Assert.Equal(expectedData.Length, extractedData.Length); // For large files, spot check rather than full comparison for performance if (expectedData.Length > 1024 * 1024) { VerifyDataSpotCheck(expectedData, extractedData); } else { Assert.Equal(expectedData, extractedData); } } } /// /// Performs efficient spot checks on large data arrays /// protected static void VerifyDataSpotCheck(byte[] expected, byte[] actual) { // Check first, middle, and last 1KB Assert.Equal(expected.Take(1024), actual.Take(1024)); var mid = expected.Length / 2; Assert.Equal(expected.Skip(mid).Take(1024), actual.Skip(mid).Take(1024)); Assert.Equal( expected.Skip(Math.Max(0, expected.Length - 1024)), actual.Skip(Math.Max(0, actual.Length - 1024)) ); } /// /// Verifies compression ratio meets expectations /// protected void VerifyCompressionRatio( long originalSize, long compressedSize, double maxRatio, string context ) { var compressionRatio = (double)compressedSize / originalSize; Assert.True( compressionRatio < maxRatio, $"Expected better compression for {context}. Original: {originalSize}, Compressed: {compressedSize}, Ratio: {compressionRatio:P}" ); } /// /// Creates a memory-based archive with specified files and compression /// protected MemoryStream CreateMemoryArchive( Dictionary files, CompressionType compressionType, int? compressionLevel = null ) { var zipStream = new MemoryStream(); using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel)) { foreach (var kvp in files) { writer.Write(kvp.Key, new MemoryStream(kvp.Value)); } } return zipStream; } /// /// Verifies streaming CRC calculation for large data /// protected void VerifyStreamingCrc(Stream entryStream, uint expectedCrc, long expectedLength) { using var crcStream = new Crc32Stream(Stream.Null); const int bufferSize = 64 * 1024; var buffer = new byte[bufferSize]; int totalBytesRead = 0; int bytesRead; while ((bytesRead = entryStream.Read(buffer, 0, bufferSize)) > 0) { crcStream.Write(buffer, 0, bytesRead); totalBytesRead += bytesRead; } var actualCrc = crcStream.Crc; Assert.Equal(expectedCrc, actualCrc); Assert.Equal(expectedLength, totalBytesRead); } /// /// Creates and verifies a basic archive with compression testing /// protected void CreateAndVerifyBasicArchive( Dictionary testFiles, CompressionType compressionType, int? compressionLevel = null, double maxCompressionRatio = 0.8 ) { // Calculate expected CRCs var expectedFiles = testFiles.ToDictionary( kvp => kvp.Key, kvp => (data: kvp.Value, crc: CalculateCrc32(kvp.Value)) ); // Create archive using var zipStream = CreateMemoryArchive(testFiles, compressionType, compressionLevel); // Verify compression occurred if expected if (compressionType != CompressionType.None) { var originalSize = testFiles.Values.Sum(data => (long)data.Length); VerifyCompressionRatio( originalSize, zipStream.Length, maxCompressionRatio, compressionType.ToString() ); } // Verify content VerifyArchiveContent(zipStream, expectedFiles); } /// /// Verifies archive entries have correct compression type /// protected void VerifyCompressionType( MemoryStream zipStream, CompressionType expectedCompressionType ) { zipStream.Position = 0; using var archive = ArchiveFactory.OpenArchive(zipStream); foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) { Assert.Equal(expectedCompressionType, entry.CompressionType); } } /// /// Extracts and verifies a single entry from archive /// protected (byte[] data, uint crc) ExtractAndVerifyEntry( MemoryStream zipStream, string entryName ) { zipStream.Position = 0; using var archive = ArchiveFactory.OpenArchive(zipStream); var entry = archive.Entries.FirstOrDefault(e => e.Key == entryName && !e.IsDirectory); Assert.NotNull(entry); using var entryStream = Assert .IsAssignableFrom(entry) .OpenEntryStream(); using var extractedStream = new MemoryStream(); entryStream.CopyTo(extractedStream); var extractedData = extractedStream.ToArray(); var crc = CalculateCrc32(extractedData); return (extractedData, crc); } protected async Task ArchiveStreamReadAsync( string testArchive, ReaderOptions? readerOptions = null ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); await ArchiveStreamReadAsync( ArchiveFactory.FindFactory(testArchive), readerOptions, new[] { testArchive } ); } protected async Task ArchiveStreamReadAsync( IArchiveFactory archiveFactory, ReaderOptions? readerOptions, IEnumerable testArchives ) { foreach (var path in testArchives) { using (var stream = SharpCompressStream.CreateNonDisposing(File.OpenRead(path))) await using ( var archive = await archiveFactory.OpenAsyncArchive( new AsyncOnlyStream(stream), readerOptions ) ) { try { await WriteArchiveEntriesToDirectoryAsync(archive, SCRATCH_FILES_PATH); } catch (IndexOutOfRangeException) { //SevenZipArchive_BZip2_Split test needs this stream.ThrowOnDispose = false; throw; } stream.ThrowOnDispose = false; } VerifyFiles(); } } private static void VerifyDeltaDistanceStream(MemoryStream memory) { memory.Position = 0; for (var y = 0; y < 9; y++) { for (var x = 0; x < 256; x++) { Assert.Equal(x, memory.ReadByte()); } } Assert.Equal(-1, memory.ReadByte()); } private static void WriteArchiveEntriesToDirectory( IArchive archive, string destinationDirectory ) { if ( archive.Entries.Any(entry => !entry.IsDirectory && entry is not IExtractableArchiveEntry ) ) { archive.WriteToDirectory(destinationDirectory); return; } foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { entry.WriteToDirectory(destinationDirectory); } } private static async Task WriteArchiveEntriesToDirectoryAsync( IAsyncArchive archive, string destinationDirectory ) { var hasNonExtractableEntries = false; await foreach (var entry in archive.EntriesAsync) { if (!entry.IsDirectory && entry is not IExtractableArchiveEntry) { hasNonExtractableEntries = true; break; } } if (hasNonExtractableEntries) { await archive.WriteToDirectoryAsync(destinationDirectory).ConfigureAwait(false); return; } await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { await entry.WriteToDirectoryAsync(destinationDirectory).ConfigureAwait(false); } } }