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();
+ }
}