diff --git a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs index 66210cdcaeca4a..e2f283ba384104 100644 --- a/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs +++ b/src/libraries/System.Formats.Tar/ref/System.Formats.Tar.cs @@ -84,9 +84,13 @@ public enum TarEntryType : byte public static partial class TarFile { public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory) { } + public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory, System.Formats.Tar.TarEntryFormat format) { } public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory) { } + public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, System.Formats.Tar.TarEntryFormat format) { } public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, System.IO.Stream destination, bool includeBaseDirectory, System.Formats.Tar.TarEntryFormat format, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, System.Formats.Tar.TarEntryFormat format, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles) { } public static void ExtractToDirectory(string sourceFileName, string destinationDirectoryName, bool overwriteFiles) { } public static System.Threading.Tasks.Task ExtractToDirectoryAsync(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index f1d6a059c57350..322d437d103e03 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -29,6 +29,23 @@ public static class TarFile /// The directory path was not found. /// An I/O exception occurred. public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, bool includeBaseDirectory) + => CreateFromDirectory(sourceDirectoryName, destination, includeBaseDirectory, TarEntryFormat.Pax); + + /// + /// Creates a tar stream that contains all the filesystem entries from the specified directory. + /// + /// The path of the directory to archive. + /// The destination stream of the archive. + /// to include the base directory name as the first segment in all the names of the archive entries. to exclude the base directory name from the archive entry names. + /// The format of the tar entries. + /// or is . + /// is empty. + /// -or- + /// does not support writing. + /// is either , or not one of the other enum values. + /// The directory path was not found. + /// An I/O exception occurred. + public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format) { ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); ArgumentNullException.ThrowIfNull(destination); @@ -43,10 +60,12 @@ public static void CreateFromDirectory(string sourceDirectoryName, Stream destin throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)); } + ValidateTarEntryFormat(format); + // Rely on Path.GetFullPath for validation of paths sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); - CreateFromDirectoryInternal(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true); + CreateFromDirectoryInternal(sourceDirectoryName, destination, includeBaseDirectory, format, leaveOpen: true); } /// @@ -64,6 +83,25 @@ public static void CreateFromDirectory(string sourceDirectoryName, Stream destin /// The directory path was not found. /// An I/O exception occurred. public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, CancellationToken cancellationToken = default) + => CreateFromDirectoryAsync(sourceDirectoryName, destination, includeBaseDirectory, TarEntryFormat.Pax, cancellationToken); + + /// + /// Asynchronously creates a tar stream that contains all the filesystem entries from the specified directory. + /// + /// The path of the directory to archive. + /// The destination stream of the archive. + /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. + /// The format of the tar entries. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous creation operation. + /// or is . + /// is empty. + /// -or- + /// does not support writing. + /// is either , or not one of the other enum values. + /// The directory path was not found. + /// An I/O exception occurred. + public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { @@ -82,10 +120,12 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream d return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName))); } + ValidateTarEntryFormat(format); + // Rely on Path.GetFullPath for validation of paths sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); - return CreateFromDirectoryInternalAsync(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true, cancellationToken); + return CreateFromDirectoryInternalAsync(sourceDirectoryName, destination, includeBaseDirectory, format, leaveOpen: true, cancellationToken); } /// @@ -99,10 +139,27 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream d /// The directory path was not found. /// An I/O exception occurred. public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory) + => CreateFromDirectory(sourceDirectoryName, destinationFileName, includeBaseDirectory, TarEntryFormat.Pax); + + /// + /// Creates a tar file that contains all the filesystem entries from the specified directory. + /// + /// The path of the directory to archive. + /// The path of the destination archive file. + /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. + /// The format of the tar entries. + /// or is . + /// or is empty. + /// is either , or not one of the other enum values. + /// The directory path was not found. + /// An I/O exception occurred. + public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format) { ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); ArgumentException.ThrowIfNullOrEmpty(destinationFileName); + ValidateTarEntryFormat(format); + // Rely on Path.GetFullPath for validation of paths sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); destinationFileName = Path.GetFullPath(destinationFileName); @@ -115,7 +172,7 @@ public static void CreateFromDirectory(string sourceDirectoryName, string destin // Throws if the destination file exists using FileStream fs = new(destinationFileName, FileMode.CreateNew, FileAccess.Write); - CreateFromDirectoryInternal(sourceDirectoryName, fs, includeBaseDirectory, leaveOpen: false); + CreateFromDirectoryInternal(sourceDirectoryName, fs, includeBaseDirectory, format, leaveOpen: false); } /// @@ -131,6 +188,23 @@ public static void CreateFromDirectory(string sourceDirectoryName, string destin /// The directory path was not found. /// An I/O exception occurred. public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken = default) + => CreateFromDirectoryAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, TarEntryFormat.Pax, cancellationToken); + + /// + /// Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. Can optionally include the base directory as the prefix for the entry names. + /// + /// The path of the directory to archive. + /// The path of the destination archive file. + /// to include the base directory name as the first path segment in all the names of the archive entries. to exclude the base directory name from the entry name paths. + /// The format of the tar entries. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous creation operation. + /// or is . + /// or is empty. + /// is either , or not one of the other enum values. + /// The directory path was not found. + /// An I/O exception occurred. + public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { @@ -139,6 +213,8 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string d ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName); ArgumentException.ThrowIfNullOrEmpty(destinationFileName); + ValidateTarEntryFormat(format); + // Rely on Path.GetFullPath for validation of paths sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); destinationFileName = Path.GetFullPath(destinationFileName); @@ -148,7 +224,7 @@ public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string d return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName))); } - return CreateFromDirectoryInternalAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, cancellationToken); + return CreateFromDirectoryInternalAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, format, cancellationToken); } /// @@ -323,11 +399,11 @@ public static Task ExtractToDirectoryAsync(string sourceFileName, string destina // Creates an archive from the contents of a directory. // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. - private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen) + private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format, bool leaveOpen) { VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination); - using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) + using (TarWriter writer = new TarWriter(destination, format, leaveOpen)) { DirectoryInfo di = new(sourceDirectoryName); @@ -353,7 +429,7 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre } // Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. - private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken) + private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken) { Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName)); Debug.Assert(!string.IsNullOrEmpty(destinationFileName)); @@ -370,18 +446,18 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector FileStream archive = new(destinationFileName, options); await using (archive.ConfigureAwait(false)) { - await CreateFromDirectoryInternalAsync(sourceDirectoryName, archive, includeBaseDirectory, leaveOpen: false, cancellationToken).ConfigureAwait(false); + await CreateFromDirectoryInternalAsync(sourceDirectoryName, archive, includeBaseDirectory, format, leaveOpen: false, cancellationToken).ConfigureAwait(false); } } // Asynchronously creates an archive from the contents of a directory. // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not. - private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, CancellationToken cancellationToken) + private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format, bool leaveOpen, CancellationToken cancellationToken) { VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination); cancellationToken.ThrowIfCancellationRequested(); - TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); + TarWriter writer = new TarWriter(destination, format, leaveOpen); await using (writer.ConfigureAwait(false)) { DirectoryInfo di = new(sourceDirectoryName); @@ -534,5 +610,13 @@ private static void VerifyExtractToDirectoryArguments(Stream source, string dest Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); Debug.Assert(source.CanRead); } + + private static void ValidateTarEntryFormat(TarEntryFormat format) + { + if (format is not (TarEntryFormat.V7 or TarEntryFormat.Ustar or TarEntryFormat.Pax or TarEntryFormat.Gnu)) + { + throw new ArgumentOutOfRangeException(nameof(format)); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs index 81fb9371011716..1797192c2276ba 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs @@ -253,5 +253,45 @@ public void SkipRecursionIntoBaseDirectorySymlink() Assert.Null(reader.GetNextEntry()); } + + [Fact] + public void InvalidFormat_Throws() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + + Assert.Throws(() => TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory: false, TarEntryFormat.Unknown)); + Assert.Throws(() => TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory: false, (TarEntryFormat)99)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void CreateArchiveWithSpecificFormat(TarEntryFormat format) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string fileName = "file.txt"; + string filePath = Path.Join(source.Path, fileName); + File.Create(filePath).Dispose(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory: false, format); + + using FileStream fileStream = File.OpenRead(destinationArchiveFileName); + using TarReader reader = new TarReader(fileStream); + + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + + Assert.Null(reader.GetNextEntry()); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.Stream.Tests.cs index a525e1ce7ea82e..89fb141255100a 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.Stream.Tests.cs @@ -40,5 +40,42 @@ public void NonExistentDirectory_Throws() using MemoryStream archive = new MemoryStream(); Assert.Throws(() => TarFile.CreateFromDirectory(sourceDirectoryName: dirPath, destination: archive, includeBaseDirectory: false)); } + + [Fact] + public void InvalidFormat_Throws() + { + using TempDirectory source = new TempDirectory(); + using MemoryStream archive = new MemoryStream(); + + Assert.Throws(() => TarFile.CreateFromDirectory(source.Path, archive, includeBaseDirectory: false, TarEntryFormat.Unknown)); + Assert.Throws(() => TarFile.CreateFromDirectory(source.Path, archive, includeBaseDirectory: false, (TarEntryFormat)99)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public void CreateArchiveWithSpecificFormat(TarEntryFormat format) + { + using TempDirectory source = new TempDirectory(); + + string fileName = "file.txt"; + string filePath = Path.Join(source.Path, fileName); + File.Create(filePath).Dispose(); + + using MemoryStream archive = new MemoryStream(); + TarFile.CreateFromDirectory(source.Path, archive, includeBaseDirectory: false, format); + + archive.Seek(0, SeekOrigin.Begin); + using TarReader reader = new TarReader(archive); + + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + + Assert.Null(reader.GetNextEntry()); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs index 0aa50a9ed71fb4..df5cae80929799 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs @@ -297,5 +297,52 @@ public async Task SkipRecursionIntoBaseDirectorySymlinkAsync() Assert.Null(await reader.GetNextEntryAsync()); // subDirectory should not be found } + + [Fact] + public async Task InvalidFormat_Throws_Async() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: false, TarEntryFormat.Unknown)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: false, (TarEntryFormat)99)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task CreateArchiveWithSpecificFormat_Async(TarEntryFormat format) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string fileName = "file.txt"; + string filePath = Path.Join(source.Path, fileName); + File.Create(filePath).Dispose(); + + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + await TarFile.CreateFromDirectoryAsync(source.Path, destinationArchiveFileName, includeBaseDirectory: false, format); + + FileStreamOptions readOptions = new() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous, + }; + + await using FileStream fileStream = File.Open(destinationArchiveFileName, readOptions); + await using TarReader reader = new TarReader(fileStream); + + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs index 8dbde4461290a6..8879c381878360 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.Stream.Tests.cs @@ -66,5 +66,42 @@ public async Task NonExistentDirectory_Throws_Async() } } } + + [Fact] + public async Task InvalidFormat_Throws_Async() + { + using TempDirectory source = new TempDirectory(); + await using MemoryStream archive = new MemoryStream(); + + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(source.Path, archive, includeBaseDirectory: false, TarEntryFormat.Unknown)); + await Assert.ThrowsAsync(() => TarFile.CreateFromDirectoryAsync(source.Path, archive, includeBaseDirectory: false, (TarEntryFormat)99)); + } + + [Theory] + [InlineData(TarEntryFormat.V7)] + [InlineData(TarEntryFormat.Ustar)] + [InlineData(TarEntryFormat.Pax)] + [InlineData(TarEntryFormat.Gnu)] + public async Task CreateArchiveWithSpecificFormat_Async(TarEntryFormat format) + { + using TempDirectory source = new TempDirectory(); + + string fileName = "file.txt"; + string filePath = Path.Join(source.Path, fileName); + File.Create(filePath).Dispose(); + + await using MemoryStream archive = new MemoryStream(); + await TarFile.CreateFromDirectoryAsync(source.Path, archive, includeBaseDirectory: false, format); + + archive.Seek(0, SeekOrigin.Begin); + await using TarReader reader = new TarReader(archive); + + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(format, entry.Format); + Assert.Equal(fileName, entry.Name); + + Assert.Null(await reader.GetNextEntryAsync()); + } } }