From 16182417fb7a623aacea6c1bf09dac4b8e004bc5 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 27 Oct 2025 10:46:08 +0000 Subject: [PATCH] add tar specific tests --- .../Archives/IArchiveEntryExtensions.cs | 17 ++ src/SharpCompress/Archives/Tar/TarArchive.cs | 8 +- src/SharpCompress/Writers/Tar/TarWriter.cs | 36 +++ tests/SharpCompress.Test/ArchiveTests.cs | 53 +++++ .../Tar/TarArchiveAsyncTests.cs | 223 ++++++++++++++++++ .../Tar/TarWriterAsyncTests.cs | 83 +++++++ tests/SharpCompress.Test/WriterTests.cs | 47 ++++ 7 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs create mode 100644 tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 0ca119b2..6fc17e31 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -75,6 +75,23 @@ public static class IArchiveEntryExtensions entry.WriteToFile ); + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public static Task WriteToDirectoryAsync( + this IArchiveEntry entry, + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + ExtractionMethods.WriteEntryToDirectoryAsync( + entry, + destinationDirectory, + options, + (x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken), + cancellationToken + ); + /// /// Extract to specific file /// diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index a7f277bc..05e74dbe 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -257,7 +257,13 @@ public class TarArchive : AbstractWritableArchive { using var entryStream = entry.OpenEntryStream(); await writer - .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + entry.LastModifiedTime, + entry.Size, + cancellationToken + ) .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index e4b20786..38565c57 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar.Headers; using SharpCompress.Compressors; @@ -91,6 +93,40 @@ public class TarWriter : AbstractWriter PadTo512(size.Value); } + public override async Task WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) => await WriteAsync(filename, source, modificationTime, null, cancellationToken); + + public async Task WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + long? size, + CancellationToken cancellationToken = default + ) + { + if (!source.CanSeek && size is null) + { + throw new ArgumentException("Seekable stream is required if no size is given."); + } + + var realSize = size ?? source.Length; + + var header = new TarHeader(WriterOptions.ArchiveEncoding); + + header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH; + header.Name = NormalizeFilename(filename); + header.Size = realSize; + header.Write(OutputStream); + var written = await source + .TransferToAsync(OutputStream, realSize, cancellationToken) + .ConfigureAwait(false); + PadTo512(written); + } + private void PadTo512(long size) { var zeros = unchecked((int)(((size + 511L) & ~511L) - size)); diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 9c483c60..2849a71f 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Compressors.Xz; @@ -569,4 +570,56 @@ public class ArchiveTests : ReaderTests return (extractedData, crc); } + + protected async Task ArchiveStreamReadAsync( + string testArchive, + ReaderOptions? readerOptions = null + ) + { + testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); + await ArchiveStreamReadAsync( + ArchiveFactory.AutoFactory, + readerOptions, + new[] { testArchive } + ); + } + + protected async Task ArchiveStreamReadAsync( + IArchiveFactory archiveFactory, + ReaderOptions? readerOptions, + IEnumerable testArchives + ) + { + foreach (var path in testArchives) + { + using ( + var stream = SharpCompressStream.Create( + File.OpenRead(path), + leaveOpen: true, + throwOnDispose: true + ) + ) + using (var archive = archiveFactory.Open(stream, readerOptions)) + { + try + { + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + await entry.WriteToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + catch (IndexOutOfRangeException) + { + //SevenZipArchive_BZip2_Split test needs this + stream.ThrowOnDispose = false; + throw; + } + stream.ThrowOnDispose = false; + } + VerifyFiles(); + } + } } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs new file mode 100644 index 00000000..280d37b2 --- /dev/null +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -0,0 +1,223 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Readers.Tar; +using SharpCompress.Writers; +using SharpCompress.Writers.Tar; +using Xunit; + +namespace SharpCompress.Test.Tar; + +public class TarArchiveAsyncTests : ArchiveTests +{ + public TarArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async Task TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar"); + + [Fact] + public async Task Tar_FileName_Exactly_100_Characters_Async() + { + var archive = "Tar_FileName_Exactly_100_Characters.tar"; + + // create the 100 char filename + var filename = + "filename_with_exactly_100_characters_______________________________________________________________X"; + + // Step 1: create a tar file containing a file with the test name + using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) + using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None)) + using (Stream inputStream = new MemoryStream()) + { + var sw = new StreamWriter(inputStream); + await sw.WriteAsync("dummy filecontent"); + await sw.FlushAsync(); + + inputStream.Position = 0; + await writer.WriteAsync(filename, inputStream, null); + } + + // Step 2: check if the written tar file can be read correctly + var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive); + using (var archive2 = TarArchive.Open(unmodified)) + { + Assert.Equal(1, archive2.Entries.Count); + Assert.Contains(filename, archive2.Entries.Select(entry => entry.Key)); + + foreach (var entry in archive2.Entries) + { + Assert.Equal( + "dummy filecontent", + await new StreamReader(entry.OpenEntryStream()).ReadLineAsync() + ); + } + } + } + + [Fact] + public async Task Tar_VeryLongFilepathReadback_Async() + { + var archive = "Tar_VeryLongFilepathReadback.tar"; + + // create a very long filename + var longFilename = ""; + for (var i = 0; i < 600; i = longFilename.Length) + { + longFilename += i.ToString("D10") + "-"; + } + + longFilename += ".txt"; + + // Step 1: create a tar file containing a file with a long name + using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) + using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None)) + using (Stream inputStream = new MemoryStream()) + { + var sw = new StreamWriter(inputStream); + await sw.WriteAsync("dummy filecontent"); + await sw.FlushAsync(); + + inputStream.Position = 0; + await writer.WriteAsync(longFilename, inputStream, null); + } + + // Step 2: check if the written tar file can be read correctly + var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive); + using (var archive2 = TarArchive.Open(unmodified)) + { + Assert.Equal(1, archive2.Entries.Count); + Assert.Contains(longFilename, archive2.Entries.Select(entry => entry.Key)); + + foreach (var entry in archive2.Entries) + { + Assert.Equal( + "dummy filecontent", + await new StreamReader(entry.OpenEntryStream()).ReadLineAsync() + ); + } + } + } + + [Fact] + public async Task Tar_Create_New_Async() + { + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar"); + + using (var archive = TarArchive.Create()) + { + archive.AddAllFromDirectory(ORIGINAL_FILES_PATH); + var twopt = new TarWriterOptions(CompressionType.None, true); + twopt.ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(866) }; + await archive.SaveToAsync(scratchPath, twopt); + } + CompareArchivesByPath(unmodified, scratchPath); + } + + [Fact] + public async Task Tar_Random_Write_Add_Async() + { + var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar"); + var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar"); + + using (var archive = TarArchive.Open(unmodified)) + { + archive.AddEntry("jpg\\test.jpg", jpg); + await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None)); + } + CompareArchivesByPath(modified, scratchPath); + } + + [Fact] + public async Task Tar_Random_Write_Remove_Async() + { + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar"); + var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar"); + + using (var archive = TarArchive.Open(unmodified)) + { + var entry = archive.Entries.Single(x => + x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase) + ); + archive.RemoveEntry(entry); + await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None)); + } + CompareArchivesByPath(modified, scratchPath); + } + + [Theory] + [InlineData(10)] + [InlineData(128)] + public async Task Tar_Japanese_Name_Async(int length) + { + using var mstm = new MemoryStream(); + var enc = new ArchiveEncoding { Default = Encoding.UTF8 }; + var twopt = new TarWriterOptions(CompressionType.None, true); + twopt.ArchiveEncoding = enc; + var fname = new string((char)0x3042, length); + using (var tw = new TarWriter(mstm, twopt)) + using (var input = new MemoryStream(new byte[32])) + { + await tw.WriteAsync(fname, input, null); + } + using (var inputMemory = new MemoryStream(mstm.ToArray())) + { + var tropt = new ReaderOptions { ArchiveEncoding = enc }; + using (var tr = TarReader.Open(inputMemory, tropt)) + { + while (tr.MoveToNextEntry()) + { + Assert.Equal(fname, tr.Entry.Key); + } + } + } + } + + [Fact] + public async Task Tar_Read_One_At_A_Time_Async() + { + var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; + var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) + { + ArchiveEncoding = archiveEncoding, + }; + var testBytes = Encoding.UTF8.GetBytes("This is a test."); + + using var memoryStream = new MemoryStream(); + using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions)) + using (var testFileStream = new MemoryStream(testBytes)) + { + await tarWriter.WriteAsync("test1.txt", testFileStream, null); + testFileStream.Position = 0; + await tarWriter.WriteAsync("test2.txt", testFileStream, null); + } + + memoryStream.Position = 0; + + var numberOfEntries = 0; + + using (var archiveFactory = TarArchive.Open(memoryStream)) + { + foreach (var entry in archiveFactory.Entries) + { + ++numberOfEntries; + + using var tarEntryStream = entry.OpenEntryStream(); + using var testFileStream = new MemoryStream(); + await tarEntryStream.CopyToAsync(testFileStream); + Assert.Equal(testBytes.Length, testFileStream.Length); + } + } + + Assert.Equal(2, numberOfEntries); + } +} diff --git a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs new file mode 100644 index 00000000..f94a3937 --- /dev/null +++ b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Writers.Tar; +using Xunit; + +namespace SharpCompress.Test.Tar; + +public class TarWriterAsyncTests : WriterTests +{ + static TarWriterAsyncTests() + { +#if !NETFRAMEWORK + //fix issue where these tests could not be ran in isolation + System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); +#endif + } + + public TarWriterAsyncTests() + : base(ArchiveType.Tar) => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async Task Tar_Writer_Async() => + await WriteAsync( + CompressionType.None, + "Tar.noEmptyDirs.tar", + "Tar.noEmptyDirs.tar", + Encoding.GetEncoding(866) + ); + + [Fact] + public async Task Tar_BZip2_Writer_Async() => + await WriteAsync( + CompressionType.BZip2, + "Tar.noEmptyDirs.tar.bz2", + "Tar.noEmptyDirs.tar.bz2", + Encoding.GetEncoding(866) + ); + + [Fact] + public async Task Tar_LZip_Writer_Async() => + await WriteAsync( + CompressionType.LZip, + "Tar.noEmptyDirs.tar.lz", + "Tar.noEmptyDirs.tar.lz", + Encoding.GetEncoding(866) + ); + + [Fact] + public async Task Tar_Rar_Write_Async() => + await Assert.ThrowsAsync(async () => + await WriteAsync( + CompressionType.Rar, + "Zip.ppmd.noEmptyDirs.zip", + "Zip.ppmd.noEmptyDirs.zip" + ) + ); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Tar_Finalize_Archive_Async(bool finalizeArchive) + { + using var stream = new MemoryStream(); + using Stream content = File.OpenRead(Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg")); + using ( + var writer = new TarWriter( + stream, + new TarWriterOptions(CompressionType.None, finalizeArchive) + ) + ) + { + await writer.WriteAsync("doesn't matter", content, null); + } + + var paddedContentWithHeader = (content.Length / 512 * 512) + 512 + 512; + var expectedStreamLength = finalizeArchive + ? paddedContentWithHeader + (512 * 2) + : paddedContentWithHeader; + Assert.Equal(expectedStreamLength, stream.Length); + } +} diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 7a367e6e..5212fab5 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -1,5 +1,7 @@ using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Readers; @@ -51,4 +53,49 @@ public class WriterTests : TestBase } VerifyFiles(); } + + protected async Task WriteAsync( + CompressionType compressionType, + string archive, + string archiveToVerifyAgainst, + Encoding? encoding = null, + CancellationToken cancellationToken = default + ) + { + using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) + { + var writerOptions = new WriterOptions(compressionType) { LeaveStreamOpen = true }; + + writerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; + + using var writer = WriterFactory.Open(stream, _type, writerOptions); + await writer.WriteAllAsync( + ORIGINAL_FILES_PATH, + "*", + SearchOption.AllDirectories, + cancellationToken + ); + } + CompareArchivesByPath( + Path.Combine(SCRATCH2_FILES_PATH, archive), + Path.Combine(TEST_ARCHIVES_PATH, archiveToVerifyAgainst) + ); + + using (Stream stream = File.OpenRead(Path.Combine(SCRATCH2_FILES_PATH, archive))) + { + var readerOptions = new ReaderOptions(); + + readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; + + using var reader = ReaderFactory.Open( + SharpCompressStream.Create(stream, leaveOpen: true), + readerOptions + ); + reader.WriteAllToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true } + ); + } + VerifyFiles(); + } }