From 820ffd46ba476a023b45e040e0b909f50d99d613 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Fri, 12 Aug 2022 16:54:02 +0200 Subject: [PATCH] Tar: restore directory permissions while extracting. (#72078) * Tar: restore directory permissions while extracting. * PR feedback. * On Windows, as on Unix: don't set group/other write permission by default. * Fix Windows compilation. * Fix Windows compilation II. * Update test for Windows. * Fix test WindowsFileMode value. * Remove branch. * Apply suggestions from code review Co-authored-by: Eric Erhardt * Add back DefaultWindowsMode. * Fix build failure in TarWriter due to DefaultWindowsMode usage. * Fix DefaultWindowsMode. * Apply suggestions from code review Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com> Co-authored-by: Eric Erhardt Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com> --- .../src/System.Formats.Tar.csproj | 2 + .../src/System/Formats/Tar/TarEntry.cs | 27 +++- .../src/System/Formats/Tar/TarFile.cs | 35 ++-- .../src/System/Formats/Tar/TarHelpers.Unix.cs | 149 ++++++++++++++++++ .../System/Formats/Tar/TarHelpers.Windows.cs | 22 +++ .../src/System/Formats/Tar/TarHelpers.cs | 4 +- .../System/Formats/Tar/TarWriter.Windows.cs | 4 +- .../TarEntry.ExtractToFile.Tests.Unix.cs | 3 + .../TarEntry/TarEntry.ExtractToFile.Tests.cs | 15 ++ .../TarEntry.ExtractToFileAsync.Tests.cs | 13 ++ .../TarFile.CreateFromDirectory.File.Tests.cs | 36 ++++- ...ile.CreateFromDirectoryAsync.File.Tests.cs | 121 ++++++++------ .../TarFile.ExtractToDirectory.File.Tests.cs | 114 ++++++++++++++ ...File.ExtractToDirectoryAsync.File.Tests.cs | 90 +++++++++++ .../System.Formats.Tar/tests/TarTestsBase.cs | 85 +++++++++- .../TarWriter/TarWriter.File.Base.Windows.cs | 5 +- 16 files changed, 646 insertions(+), 79 deletions(-) create mode 100644 src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs create mode 100644 src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index 7d122684fcef9f..c0487caf28049e 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -38,6 +38,7 @@ + @@ -51,6 +52,7 @@ + diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index ea965322d78095..00c790762e4b65 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; @@ -280,24 +281,24 @@ public Stream? DataStream internal abstract bool IsDataStreamSetterSupported(); // Extracts the current entry to a location relative to the specified directory. - internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool overwrite) + internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool overwrite, SortedDictionary? pendingModes) { (string fileDestinationPath, string? linkTargetPath) = GetDestinationAndLinkPaths(destinationDirectoryPath); if (EntryType == TarEntryType.Directory) { - Directory.CreateDirectory(fileDestinationPath); + TarHelpers.CreateDirectory(fileDestinationPath, Mode, overwrite, pendingModes); } else { // If it is a file, create containing directory. - Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, overwrite, pendingModes); ExtractToFileInternal(fileDestinationPath, linkTargetPath, overwrite); } } // Asynchronously extracts the current entry to a location relative to the specified directory. - internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, CancellationToken cancellationToken) + internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, SortedDictionary? pendingModes, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -308,13 +309,13 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b if (EntryType == TarEntryType.Directory) { - Directory.CreateDirectory(fileDestinationPath); + TarHelpers.CreateDirectory(fileDestinationPath, Mode, overwrite, pendingModes); return Task.CompletedTask; } else { // If it is a file, create containing directory. - Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, overwrite, pendingModes); return ExtractToFileInternalAsync(fileDestinationPath, linkTargetPath, overwrite, cancellationToken); } } @@ -403,7 +404,19 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath) { case TarEntryType.Directory: case TarEntryType.DirectoryList: - Directory.CreateDirectory(filePath); + // Mode must only be used for the leaf directory. + // VerifyPathsForEntryType ensures we're only creating a leaf. + Debug.Assert(Directory.Exists(Path.GetDirectoryName(filePath))); + Debug.Assert(!Directory.Exists(filePath)); + + if (!OperatingSystem.IsWindows()) + { + Directory.CreateDirectory(filePath, Mode); + } + else + { + Directory.CreateDirectory(filePath); + } break; case TarEntryType.SymbolicLink: 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 8ce12347d73a3c..a77cb2c36a65ea 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; @@ -279,7 +280,6 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre using (TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen)) { - bool baseDirectoryIsEmpty = true; DirectoryInfo di = new(sourceDirectoryName); string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory); @@ -287,15 +287,14 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre try { - foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + if (includeBaseDirectory) { - baseDirectoryIsEmpty = false; - writer.WriteEntry(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer)); + writer.WriteEntry(di.FullName, GetEntryNameForBaseDirectory(di.Name, ref entryNameBuffer)); } - if (includeBaseDirectory && baseDirectoryIsEmpty) + foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { - writer.WriteEntry(GetEntryForBaseDirectory(di.Name, ref entryNameBuffer)); + writer.WriteEntry(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer)); } } finally @@ -337,7 +336,6 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector TarWriter writer = new TarWriter(destination, TarEntryFormat.Pax, leaveOpen); await using (writer.ConfigureAwait(false)) { - bool baseDirectoryIsEmpty = true; DirectoryInfo di = new(sourceDirectoryName); string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory); @@ -345,15 +343,14 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector try { - foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + if (includeBaseDirectory) { - baseDirectoryIsEmpty = false; - await writer.WriteEntryAsync(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); + await writer.WriteEntryAsync(di.FullName, GetEntryNameForBaseDirectory(di.Name, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); } - if (includeBaseDirectory && baseDirectoryIsEmpty) + foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { - await writer.WriteEntryAsync(GetEntryForBaseDirectory(di.Name, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); + await writer.WriteEntryAsync(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length, ref entryNameBuffer), cancellationToken).ConfigureAwait(false); } } finally @@ -377,11 +374,9 @@ private static string GetEntryNameForFileSystemInfo(FileSystemInfo file, int bas return ArchivingUtils.EntryFromPath(file.FullName, basePathLength, entryNameLength, ref entryNameBuffer, appendPathSeparator: isDirectory); } - // Constructs a PaxTarEntry for a base directory entry when creating an archive. - private static PaxTarEntry GetEntryForBaseDirectory(string name, ref char[] entryNameBuffer) + private static string GetEntryNameForBaseDirectory(string name, ref char[] entryNameBuffer) { - string entryName = ArchivingUtils.EntryFromPath(name, 0, name.Length, ref entryNameBuffer, appendPathSeparator: true); - return new PaxTarEntry(TarEntryType.Directory, entryName); + return ArchivingUtils.EntryFromPath(name, 0, name.Length, ref entryNameBuffer, appendPathSeparator: true); } // Extracts an archive into the specified directory. @@ -392,14 +387,16 @@ private static void ExtractToDirectoryInternal(Stream source, string destination using TarReader reader = new TarReader(source, leaveOpen); + SortedDictionary? pendingModes = TarHelpers.CreatePendingModesDictionary(); TarEntry? entry; while ((entry = reader.GetNextEntry()) != null) { if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) { - entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles); + entry.ExtractRelativeToDirectory(destinationDirectoryPath, overwriteFiles, pendingModes); } } + TarHelpers.SetPendingModes(pendingModes); } // Asynchronously extracts the contents of a tar file into the specified directory. @@ -430,6 +427,7 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string VerifyExtractToDirectoryArguments(source, destinationDirectoryPath); cancellationToken.ThrowIfCancellationRequested(); + SortedDictionary? pendingModes = TarHelpers.CreatePendingModesDictionary(); TarReader reader = new TarReader(source, leaveOpen); await using (reader.ConfigureAwait(false)) { @@ -438,10 +436,11 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string { if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, cancellationToken).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryPath, overwriteFiles, pendingModes, cancellationToken).ConfigureAwait(false); } } } + TarHelpers.SetPendingModes(pendingModes); } [Conditional("DEBUG")] diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs new file mode 100644 index 00000000000000..f684dd78fde1c6 --- /dev/null +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Diagnostics; + +namespace System.Formats.Tar +{ + internal static partial class TarHelpers + { + private static readonly Lazy s_umask = new Lazy(DetermineUMask); + + private static UnixFileMode DetermineUMask() + { + // To determine the umask, we'll create a file with full permissions and see + // what gets filtered out. + // note: only the owner of a file, and root can change file permissions. + + const UnixFileMode OwnershipPermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + string filename = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + FileStreamOptions options = new() + { + Mode = FileMode.CreateNew, + UnixCreateMode = OwnershipPermissions, + Options = FileOptions.DeleteOnClose, + Access = FileAccess.Write, + BufferSize = 0 + }; + using var fs = new FileStream(filename, options); + UnixFileMode actual = File.GetUnixFileMode(fs.SafeFileHandle); + + return OwnershipPermissions & ~actual; + } + + private sealed class ReverseStringComparer : IComparer + { + public int Compare (string? x, string? y) + => StringComparer.Ordinal.Compare(y, x); + } + + private static readonly ReverseStringComparer s_reverseStringComparer = new(); + + private static UnixFileMode UMask => s_umask.Value; + + /* + Tar files are usually ordered: parent directories come before their child entries. + + They may be unordered. In that case we need to create parent directories before + we know the proper permissions for these directories. + + We create these directories with restrictive permissions. If we encounter an entry for + the directory later, we store the mode to apply it later. + + If the archive doesn't have an entry for the parent directory, we use the default mask. + + The pending modes to be applied are tracked through a reverse-sorted dictionary. + The reverse order is needed to apply permissions to children before their parent. + Otherwise we may apply a restrictive mask to the parent, that prevents us from + changing a child. + */ + + internal static SortedDictionary? CreatePendingModesDictionary() + => new SortedDictionary(s_reverseStringComparer); + + internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool overwriteMetadata, SortedDictionary? pendingModes) + { + // Restrictive mask for creating the missing parent directories while extracting. + const UnixFileMode ExtractPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + + Debug.Assert(pendingModes is not null); + + if (Directory.Exists(fullPath)) + { + // Apply permissions to an existing directory when we're overwriting metadata + // or the directory was created as a missing parent (stored in pendingModes). + if (mode.HasValue) + { + bool hasExtractPermissions = (mode.Value & ExtractPermissions) == ExtractPermissions; + if (hasExtractPermissions) + { + bool removed = pendingModes.Remove(fullPath); + if (overwriteMetadata || removed) + { + UnixFileMode umask = UMask; + File.SetUnixFileMode(fullPath, mode.Value & ~umask); + } + } + else if (overwriteMetadata || pendingModes.ContainsKey(fullPath)) + { + pendingModes[fullPath] = mode.Value; + } + } + return; + } + + if (mode.HasValue) + { + // Ensure we have sufficient permissions to extract in the directory. + if ((mode.Value & ExtractPermissions) != ExtractPermissions) + { + pendingModes[fullPath] = mode.Value; + mode = ExtractPermissions; + } + } + else + { + pendingModes.Add(fullPath, DefaultDirectoryMode); + mode = ExtractPermissions; + } + + string parentDir = Path.GetDirectoryName(fullPath)!; + string rootDir = Path.GetPathRoot(parentDir)!; + bool hasMissingParents = false; + for (string dir = parentDir; dir != rootDir && !Directory.Exists(dir); dir = Path.GetDirectoryName(dir)!) + { + pendingModes.Add(dir, DefaultDirectoryMode); + hasMissingParents = true; + } + + if (hasMissingParents) + { + Directory.CreateDirectory(parentDir, ExtractPermissions); + } + + Directory.CreateDirectory(fullPath, mode.Value); + } + + internal static void SetPendingModes(SortedDictionary? pendingModes) + { + Debug.Assert(pendingModes is not null); + + if (pendingModes.Count == 0) + { + return; + } + + UnixFileMode umask = UMask; + foreach (KeyValuePair dir in pendingModes) + { + File.SetUnixFileMode(dir.Key, dir.Value & ~umask); + } + } + } +} diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs new file mode 100644 index 00000000000000..e00f6476764aba --- /dev/null +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Diagnostics; + +namespace System.Formats.Tar +{ + internal static partial class TarHelpers + { + internal static SortedDictionary? CreatePendingModesDictionary() + => null; + + internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool overwriteMetadata, SortedDictionary? pendingModes) + => Directory.CreateDirectory(fullPath); + + internal static void SetPendingModes(SortedDictionary? pendingModes) + => Debug.Assert(pendingModes is null); + } +} diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index a34bdd0506a20e..d4592eb85d4b5f 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -12,7 +12,7 @@ namespace System.Formats.Tar { // Static class containing a variety of helper methods. - internal static class TarHelpers + internal static partial class TarHelpers { internal const short RecordSize = 512; internal const int MaxBufferLength = 4096; @@ -22,11 +22,13 @@ internal static class TarHelpers internal const byte EqualsChar = 0x3d; internal const byte NewLineChar = 0xa; + // Default mode for TarEntry created for a file-type. private const UnixFileMode DefaultFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead; + // Default mode for TarEntry created for a directory-type. private const UnixFileMode DefaultDirectoryMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs index a3fa4ee0f595f8..bfb6cf17f11076 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.Windows.cs @@ -11,8 +11,8 @@ namespace System.Formats.Tar // Windows specific methods for the TarWriter class. public sealed partial class TarWriter : IDisposable { - // Creating archives in Windows always sets the mode to 777 - private const UnixFileMode DefaultWindowsMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.UserExecute; + // Windows files don't have a mode. Use a mode of 755 for directories and files. + private const UnixFileMode DefaultWindowsMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute; // Windows specific implementation of the method that reads an entry from disk and writes it into the archive stream. private TarEntry ConstructEntryForWriting(string fullPath, string entryName, FileOptions fileOptions) diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs index f86136bcc8021d..cee5f6bc7dfd85 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.Unix.cs @@ -67,6 +67,7 @@ public async Task Extract_SpecialFiles_Async(TarEntryFormat format, TarEntryType entry.DeviceMajor = TestCharacterDeviceMajor; entry.DeviceMinor = TestCharacterDeviceMinor; } + entry.Mode = TestPermission1; return (entryName, destination, entry); } @@ -106,6 +107,8 @@ private void Verify_Extract_SpecialFiles(string destination, PosixTarEntry entry Assert.Equal((int)major, entry.DeviceMajor); Assert.Equal((int)minor, entry.DeviceMinor); } + + AssertFileModeEquals(destination, TestPermission1); } } } \ No newline at end of file diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs index 93ba1559d2a7ce..3cfcf4e6e36ca7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFile.Tests.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace System.Formats.Tar.Tests @@ -92,5 +94,18 @@ public void ExtractToFile_Link_Throws(TarEntryFormat format, TarEntryType entryT Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); } + + [Theory] + [MemberData(nameof(GetFormatsAndFiles))] + public void Extract(TarEntryFormat format, TarEntryType entryType) + { + using TempDirectory root = new TempDirectory(); + + (string entryName, string destination, TarEntry entry) = Prepare_Extract(root, format, entryType); + + entry.ExtractToFile(destination, overwrite: true); + + Verify_Extract(destination, entry, entryType); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs index 58d0143b1f6ce2..c5404e9fd4d848 100644 --- a/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarEntry/TarEntry.ExtractToFileAsync.Tests.cs @@ -113,5 +113,18 @@ public async Task ExtractToFile_Link_Throws_Async(TarEntryFormat format, TarEntr Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count()); } } + + [Theory] + [MemberData(nameof(GetFormatsAndFiles))] + public async Task Extract_Async(TarEntryFormat format, TarEntryType entryType) + { + using TempDirectory root = new TempDirectory(); + + (string entryName, string destination, TarEntry entry) = Prepare_Extract(root, format, entryType); + + await entry.ExtractToFileAsync(destination, overwrite: true); + + Verify_Extract(destination, entry, entryType); + } } } 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 d2fe36deaaa3ca..75a3495d94e5e5 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 @@ -52,17 +52,26 @@ public void VerifyIncludeBaseDirectory(bool includeBaseDirectory) using TempDirectory source = new TempDirectory(); using TempDirectory destination = new TempDirectory(); + UnixFileMode baseDirectoryMode = TestPermission1; + SetUnixFileMode(source.Path, baseDirectoryMode); + string fileName1 = "file1.txt"; string filePath1 = Path.Join(source.Path, fileName1); File.Create(filePath1).Dispose(); + UnixFileMode filename1Mode = TestPermission2; + SetUnixFileMode(filePath1, filename1Mode); string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name string subDirectoryPath = Path.Join(source.Path, subDirectoryName); Directory.CreateDirectory(subDirectoryPath); + UnixFileMode subDirectoryMode = TestPermission3; + SetUnixFileMode(subDirectoryPath, subDirectoryMode); string fileName2 = "file2.txt"; string filePath2 = Path.Join(subDirectoryPath, fileName2); File.Create(filePath2).Dispose(); + UnixFileMode filename2Mode = TestPermission4; + SetUnixFileMode(filePath2, filename2Mode); string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); @@ -78,25 +87,38 @@ public void VerifyIncludeBaseDirectory(bool includeBaseDirectory) entries.Add(entry); } - Assert.Equal(3, entries.Count); + int expectedCount = 3 + (includeBaseDirectory ? 1 : 0); + Assert.Equal(expectedCount, entries.Count); string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + if (includeBaseDirectory) + { + TarEntry baseEntry = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.Directory && + x.Name == prefix); + Assert.NotNull(baseEntry); + AssertEntryModeFromFileSystemEquals(baseEntry, baseDirectoryMode); + } + TarEntry entry1 = entries.FirstOrDefault(x => x.EntryType == TarEntryType.RegularFile && x.Name == prefix + fileName1); Assert.NotNull(entry1); + AssertEntryModeFromFileSystemEquals(entry1, filename1Mode); TarEntry directory = entries.FirstOrDefault(x => x.EntryType == TarEntryType.Directory && x.Name == prefix + subDirectoryName); Assert.NotNull(directory); + AssertEntryModeFromFileSystemEquals(directory, subDirectoryMode); string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName TarEntry entry2 = entries.FirstOrDefault(x => x.EntryType == TarEntryType.RegularFile && x.Name == prefix + actualFileName2); Assert.NotNull(entry2); + AssertEntryModeFromFileSystemEquals(entry2, filename2Mode); } [Fact] @@ -144,7 +166,17 @@ public void IncludeAllSegmentsOfPath(bool includeBaseDirectory) string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; - TarEntry entry = reader.GetNextEntry(); + TarEntry entry; + + if (includeBaseDirectory) + { + entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix, entry.Name); + } + + entry = reader.GetNextEntry(); Assert.NotNull(entry); Assert.Equal(TarEntryType.Directory, entry.EntryType); Assert.Equal(prefix + "segment1/", entry.Name); 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 8891ea856ebb6e..c9b4377cd03285 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 @@ -61,65 +61,86 @@ public async Task DestinationExists_Throws_Async() [InlineData(true)] public async Task VerifyIncludeBaseDirectory_Async(bool includeBaseDirectory) { - using (TempDirectory source = new TempDirectory()) - using (TempDirectory destination = new TempDirectory()) - { - string fileName1 = "file1.txt"; - string filePath1 = Path.Join(source.Path, fileName1); - File.Create(filePath1).Dispose(); + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); - string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name - string subDirectoryPath = Path.Join(source.Path, subDirectoryName); - Directory.CreateDirectory(subDirectoryPath); + UnixFileMode baseDirectoryMode = TestPermission1; + SetUnixFileMode(source.Path, baseDirectoryMode); - string fileName2 = "file2.txt"; - string filePath2 = Path.Join(subDirectoryPath, fileName2); - File.Create(filePath2).Dispose(); + string fileName1 = "file1.txt"; + string filePath1 = Path.Join(source.Path, fileName1); + File.Create(filePath1).Dispose(); + UnixFileMode filename1Mode = TestPermission2; + SetUnixFileMode(filePath1, filename1Mode); - string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); - TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); + string subDirectoryName = "dir/"; // The trailing separator is preserved in the TarEntry.Name + string subDirectoryPath = Path.Join(source.Path, subDirectoryName); + Directory.CreateDirectory(subDirectoryPath); + UnixFileMode subDirectoryMode = TestPermission3; + SetUnixFileMode(subDirectoryPath, subDirectoryMode); - List entries = new List(); + string fileName2 = "file2.txt"; + string filePath2 = Path.Join(subDirectoryPath, fileName2); + File.Create(filePath2).Dispose(); + UnixFileMode filename2Mode = TestPermission4; + SetUnixFileMode(filePath2, filename2Mode); - FileStreamOptions readOptions = new() - { - Access = FileAccess.Read, - Mode = FileMode.Open, - Options = FileOptions.Asynchronous, - }; + string destinationArchiveFileName = Path.Join(destination.Path, "output.tar"); + TarFile.CreateFromDirectory(source.Path, destinationArchiveFileName, includeBaseDirectory); - await using (FileStream fileStream = File.Open(destinationArchiveFileName, readOptions)) + List entries = new List(); + + 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)) { - await using (TarReader reader = new TarReader(fileStream)) + TarEntry entry; + while ((entry = await reader.GetNextEntryAsync()) != null) { - TarEntry entry; - while ((entry = await reader.GetNextEntryAsync()) != null) - { - entries.Add(entry); - } + entries.Add(entry); } } + } - Assert.Equal(3, entries.Count); - - string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; + int expectedCount = 3 + (includeBaseDirectory ? 1 : 0); + Assert.Equal(expectedCount, entries.Count); - TarEntry entry1 = entries.FirstOrDefault(x => - x.EntryType == TarEntryType.RegularFile && - x.Name == prefix + fileName1); - Assert.NotNull(entry1); + string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; - TarEntry directory = entries.FirstOrDefault(x => + if (includeBaseDirectory) + { + TarEntry baseEntry = entries.FirstOrDefault(x => x.EntryType == TarEntryType.Directory && - x.Name == prefix + subDirectoryName); - Assert.NotNull(directory); - - string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName - TarEntry entry2 = entries.FirstOrDefault(x => - x.EntryType == TarEntryType.RegularFile && - x.Name == prefix + actualFileName2); - Assert.NotNull(entry2); + x.Name == prefix); + Assert.NotNull(baseEntry); + AssertEntryModeFromFileSystemEquals(baseEntry, baseDirectoryMode); } + + TarEntry entry1 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + fileName1); + Assert.NotNull(entry1); + AssertEntryModeFromFileSystemEquals(entry1, filename1Mode); + + TarEntry directory = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.Directory && + x.Name == prefix + subDirectoryName); + Assert.NotNull(directory); + AssertEntryModeFromFileSystemEquals(directory, subDirectoryMode); + + string actualFileName2 = subDirectoryName + fileName2; // Notice the trailing separator in subDirectoryName + TarEntry entry2 = entries.FirstOrDefault(x => + x.EntryType == TarEntryType.RegularFile && + x.Name == prefix + actualFileName2); + Assert.NotNull(entry2); + AssertEntryModeFromFileSystemEquals(entry2, filename2Mode); } [Fact] @@ -186,7 +207,17 @@ public async Task IncludeAllSegmentsOfPath_Async(bool includeBaseDirectory) { string prefix = includeBaseDirectory ? Path.GetFileName(source.Path) + '/' : string.Empty; - TarEntry entry = await reader.GetNextEntryAsync(); + TarEntry entry; + + if (includeBaseDirectory) + { + entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(TarEntryType.Directory, entry.EntryType); + Assert.Equal(prefix, entry.Name); + } + + entry = await reader.GetNextEntryAsync(); Assert.NotNull(entry); Assert.Equal(TarEntryType.Directory, entry.EntryType); Assert.Equal(prefix + "segment1/", entry.Name); diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs index a753358cb8a394..f741926cdd8ef1 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs @@ -159,5 +159,119 @@ public void ExtractArchiveWithEntriesThatStartWithSlashDotPrefix() Assert.True(Path.Exists(entryPath), $"Entry was not extracted: {entryPath}"); } } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UnixFileModes(bool overwrite) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string archivePath = Path.Join(source.Path, "archive.tar"); + using FileStream archiveStream = File.Create(archivePath); + using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry dir = new PaxTarEntry(TarEntryType.Directory, "dir"); + dir.Mode = TestPermission1; + writer.WriteEntry(dir); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "file"); + file.Mode = TestPermission2; + writer.WriteEntry(file); + + // Archive has no entry for missing_parent. + PaxTarEntry missingParentDir = new PaxTarEntry(TarEntryType.Directory, "missing_parent/dir"); + missingParentDir.Mode = TestPermission3; + writer.WriteEntry(missingParentDir); + + // out_of_order_parent/file entry comes before out_of_order_parent entry. + PaxTarEntry outOfOrderFile = new PaxTarEntry(TarEntryType.RegularFile, "out_of_order_parent/file"); + writer.WriteEntry(outOfOrderFile); + + PaxTarEntry outOfOrderDir = new PaxTarEntry(TarEntryType.Directory, "out_of_order_parent"); + outOfOrderDir.Mode = TestPermission4; + writer.WriteEntry(outOfOrderDir); + } + + string dirPath = Path.Join(destination.Path, "dir"); + string filePath = Path.Join(destination.Path, "file"); + string missingParentPath = Path.Join(destination.Path, "missing_parent"); + string missingParentDirPath = Path.Join(missingParentPath, "dir"); + string outOfOrderDirPath = Path.Join(destination.Path, "out_of_order_parent"); + + if (overwrite) + { + File.OpenWrite(filePath).Dispose(); + Directory.CreateDirectory(dirPath); + Directory.CreateDirectory(missingParentDirPath); + Directory.CreateDirectory(outOfOrderDirPath); + } + + TarFile.ExtractToDirectory(archivePath, destination.Path, overwriteFiles: overwrite); + + Assert.True(Directory.Exists(dirPath), $"{dirPath}' does not exist."); + AssertFileModeEquals(dirPath, TestPermission1); + + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + AssertFileModeEquals(filePath, TestPermission2); + + // Missing parents are created with DefaultDirectoryMode. + // The mode is not set when overwrite == true if there is no entry and the directory exists before extracting. + Assert.True(Directory.Exists(missingParentPath), $"{missingParentPath}' does not exist."); + if (!overwrite) + { + AssertFileModeEquals(missingParentPath, DefaultDirectoryMode); + } + + Assert.True(Directory.Exists(missingParentDirPath), $"{missingParentDirPath}' does not exist."); + AssertFileModeEquals(missingParentDirPath, TestPermission3); + + // Directory modes that are out-of-order are still applied. + Assert.True(Directory.Exists(outOfOrderDirPath), $"{outOfOrderDirPath}' does not exist."); + AssertFileModeEquals(outOfOrderDirPath, TestPermission4); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UnixFileModes_RestrictiveParentDir(bool overwrite) + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string archivePath = Path.Join(source.Path, "archive.tar"); + using FileStream archiveStream = File.Create(archivePath); + using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry dir = new PaxTarEntry(TarEntryType.Directory, "dir"); + dir.Mode = UnixFileMode.None; // Restrict permissions. + writer.WriteEntry(dir); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "dir/file"); + file.Mode = TestPermission1; + writer.WriteEntry(file); + } + + string dirPath = Path.Join(destination.Path, "dir"); + string filePath = Path.Join(dirPath, "file"); + + if (overwrite) + { + Directory.CreateDirectory(dirPath); + File.OpenWrite(filePath).Dispose(); + } + + TarFile.ExtractToDirectory(archivePath, destination.Path, overwriteFiles: overwrite); + + Assert.True(Directory.Exists(dirPath), $"{dirPath}' does not exist."); + AssertFileModeEquals(dirPath, UnixFileMode.None); + + // Set dir permissions so we can access file. + SetUnixFileMode(dirPath, UserAll); + + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + AssertFileModeEquals(filePath, TestPermission1); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs index dfebe737493acc..01d2457018ce24 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs @@ -190,5 +190,95 @@ public async Task ExtractArchiveWithEntriesThatStartWithSlashDotPrefix_Async() } } } + + [Fact] + public async Task UnixFileModes_Async() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string archivePath = Path.Join(source.Path, "archive.tar"); + using FileStream archiveStream = File.Create(archivePath); + using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry dir = new PaxTarEntry(TarEntryType.Directory, "dir"); + dir.Mode = TestPermission1; + writer.WriteEntry(dir); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "file"); + file.Mode = TestPermission2; + writer.WriteEntry(file); + + // Archive has no entry for missing_parent. + PaxTarEntry missingParentDir = new PaxTarEntry(TarEntryType.Directory, "missing_parent/dir"); + missingParentDir.Mode = TestPermission3; + writer.WriteEntry(missingParentDir); + + // out_of_order_parent/file entry comes before out_of_order_parent entry. + PaxTarEntry outOfOrderFile = new PaxTarEntry(TarEntryType.RegularFile, "out_of_order_parent/file"); + writer.WriteEntry(outOfOrderFile); + + PaxTarEntry outOfOrderDir = new PaxTarEntry(TarEntryType.Directory, "out_of_order_parent"); + outOfOrderDir.Mode = TestPermission4; + writer.WriteEntry(outOfOrderDir); + } + + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false); + + string dirPath = Path.Join(destination.Path, "dir"); + Assert.True(Directory.Exists(dirPath), $"{dirPath}' does not exist."); + AssertFileModeEquals(dirPath, TestPermission1); + + string filePath = Path.Join(destination.Path, "file"); + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + AssertFileModeEquals(filePath, TestPermission2); + + // Missing parents are created with DefaultDirectoryMode. + string missingParentPath = Path.Join(destination.Path, "missing_parent"); + Assert.True(Directory.Exists(missingParentPath), $"{missingParentPath}' does not exist."); + AssertFileModeEquals(missingParentPath, DefaultDirectoryMode); + + string missingParentDirPath = Path.Join(missingParentPath, "dir"); + Assert.True(Directory.Exists(missingParentDirPath), $"{missingParentDirPath}' does not exist."); + AssertFileModeEquals(missingParentDirPath, TestPermission3); + + // Directory modes that are out-of-order are still applied. + string outOfOrderDirPath = Path.Join(destination.Path, "out_of_order_parent"); + Assert.True(Directory.Exists(outOfOrderDirPath), $"{outOfOrderDirPath}' does not exist."); + AssertFileModeEquals(outOfOrderDirPath, TestPermission4); + } + + [Fact] + public async Task UnixFileModes_RestrictiveParentDir_Async() + { + using TempDirectory source = new TempDirectory(); + using TempDirectory destination = new TempDirectory(); + + string archivePath = Path.Join(source.Path, "archive.tar"); + using FileStream archiveStream = File.Create(archivePath); + using (TarWriter writer = new TarWriter(archiveStream)) + { + PaxTarEntry dir = new PaxTarEntry(TarEntryType.Directory, "dir"); + dir.Mode = UnixFileMode.None; // Restrict permissions. + writer.WriteEntry(dir); + + PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "dir/file"); + file.Mode = TestPermission1; + writer.WriteEntry(file); + } + + await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false); + + string dirPath = Path.Join(destination.Path, "dir"); + Assert.True(Directory.Exists(dirPath), $"{dirPath}' does not exist."); + AssertFileModeEquals(dirPath, UnixFileMode.None); + + // Set dir permissions so we can access file. + SetUnixFileMode(dirPath, UserAll); + + string filePath = Path.Join(dirPath, "file"); + Assert.True(File.Exists(filePath), $"{filePath}' does not exist."); + AssertFileModeEquals(filePath, TestPermission1); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 1c61d2e3ae1a7f..b167a980acd2b0 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -15,8 +15,18 @@ public abstract partial class TarTestsBase : FileCleanupTestBase // Default values are what a TarEntry created with its constructor will set protected const UnixFileMode DefaultFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead; // 644 in octal, internally used as default - private const UnixFileMode DefaultDirectoryMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; // 755 in octal, internally used as default - protected const UnixFileMode DefaultWindowsMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.UserExecute; // Creating archives in Windows always sets the mode to 777 + protected const UnixFileMode DefaultDirectoryMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; // 755 in octal, internally used as default + + // Mode assumed for files and directories on Windows. + protected const UnixFileMode DefaultWindowsMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; // 755 in octal, internally used as default + + // Permissions used by tests. User has all permissions to avoid permission errors. + protected const UnixFileMode UserAll = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + protected const UnixFileMode TestPermission1 = UserAll | UnixFileMode.GroupRead; + protected const UnixFileMode TestPermission2 = UserAll | UnixFileMode.GroupExecute; + protected const UnixFileMode TestPermission3 = UserAll | UnixFileMode.OtherRead; + protected const UnixFileMode TestPermission4 = UserAll | UnixFileMode.OtherExecute; + protected const int DefaultGid = 0; protected const int DefaultUid = 0; protected const int DefaultDeviceMajor = 0; @@ -363,5 +373,76 @@ public static IEnumerable GetFormatsAndLinks() yield return new object[] { format, TarEntryType.HardLink }; } } + + public static IEnumerable GetFormatsAndFiles() + { + foreach (TarEntryType entryType in new[] { TarEntryType.V7RegularFile, TarEntryType.Directory }) + { + yield return new object[] { TarEntryFormat.V7, entryType }; + } + foreach (TarEntryFormat format in new[] { TarEntryFormat.Ustar, TarEntryFormat.Pax, TarEntryFormat.Gnu }) + { + foreach (TarEntryType entryType in new[] { TarEntryType.RegularFile, TarEntryType.Directory }) + { + yield return new object[] { format, entryType }; + } + } + } + + protected static void SetUnixFileMode(string path, UnixFileMode mode) + { + if (!PlatformDetection.IsWindows) + { + File.SetUnixFileMode(path, mode); + } + } + + protected static void AssertEntryModeFromFileSystemEquals(TarEntry entry, UnixFileMode fileMode) + { + if (PlatformDetection.IsWindows) + { + // Windows files don't have a mode. Set the expected value. + fileMode = DefaultWindowsMode; + } + Assert.Equal(fileMode, entry.Mode); + } + + protected static void AssertFileModeEquals(string path, UnixFileMode mode) + { + if (!PlatformDetection.IsWindows) + { + Assert.Equal(mode, File.GetUnixFileMode(path)); + } + } + + protected (string, string, TarEntry) Prepare_Extract(TempDirectory root, TarEntryFormat format, TarEntryType entryType) + { + string entryName = entryType.ToString(); + string destination = Path.Join(root.Path, entryName); + + TarEntry entry = InvokeTarEntryCreationConstructor(format, entryType, entryName); + Assert.NotNull(entry); + entry.Mode = TestPermission1; + + return (entryName, destination, entry); + } + + protected void Verify_Extract(string destination, TarEntry entry, TarEntryType entryType) + { + if (entryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile) + { + Assert.True(File.Exists(destination)); + } + else if (entryType is TarEntryType.Directory) + { + Assert.True(Directory.Exists(destination)); + } + else + { + Assert.True(false, "Unchecked entry type."); + } + + AssertFileModeEquals(destination, TestPermission1); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs index 16fb8205685e7b..f37354a9ffef7e 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.File.Base.Windows.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; using Xunit; namespace System.Formats.Tar.Tests @@ -11,8 +12,8 @@ protected void VerifyPlatformSpecificMetadata(string filePath, TarEntry entry) { Assert.True(entry.ModificationTime > DateTimeOffset.UnixEpoch); - // Archives created in Windows always set mode to 777 - Assert.Equal(DefaultWindowsMode, entry.Mode); + UnixFileMode expectedMode = DefaultWindowsMode; + Assert.Equal(expectedMode, entry.Mode); Assert.Equal(DefaultUid, entry.Uid); Assert.Equal(DefaultGid, entry.Gid);