Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tar: restore directory permissions while extracting. #72078

Merged
merged 13 commits into from
Aug 12, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<!-- Windows specific files -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Compile Include="System\Formats\Tar\TarEntry.Windows.cs" />
<Compile Include="System\Formats\Tar\TarHelpers.Windows.cs" />
<Compile Include="System\Formats\Tar\TarWriter.Windows.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Errors.cs" Link="Common\Interop\Windows\Interop.Errors.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
Expand All @@ -51,6 +52,7 @@
<!-- Unix specific files -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'Unix'">
<Compile Include="System\Formats\Tar\TarEntry.Unix.cs" />
<Compile Include="System\Formats\Tar\TarHelpers.Unix.cs" />
<Compile Include="System\Formats\Tar\TarWriter.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.IOErrors.cs" Link="Common\Interop\Unix\Interop.IOErrors.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" Link="Common\Interop\Unix\Interop.Libraries.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -312,24 +313,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<string, UnixFileMode>? 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<string, UnixFileMode>? pendingModes, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Expand All @@ -340,13 +341,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);
}
}
Expand Down Expand Up @@ -435,7 +436,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:
Expand Down
35 changes: 17 additions & 18 deletions src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -279,23 +280,21 @@ 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);

char[] entryNameBuffer = ArrayPool<char>.Shared.Rent(DefaultCapacity);

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
Expand Down Expand Up @@ -337,23 +336,21 @@ 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);

char[] entryNameBuffer = ArrayPool<char>.Shared.Rent(DefaultCapacity);

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
Expand All @@ -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)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
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.
Expand All @@ -392,14 +387,16 @@ private static void ExtractToDirectoryInternal(Stream source, string destination

using TarReader reader = new TarReader(source, leaveOpen);

SortedDictionary<string, UnixFileMode>? 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.
Expand Down Expand Up @@ -430,6 +427,7 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string
VerifyExtractToDirectoryArguments(source, destinationDirectoryPath);
cancellationToken.ThrowIfCancellationRequested();

SortedDictionary<string, UnixFileMode>? pendingModes = TarHelpers.CreatePendingModesDictionary();
TarReader reader = new TarReader(source, leaveOpen);
await using (reader.ConfigureAwait(false))
{
Expand All @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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<UnixFileMode> s_umask = new Lazy<UnixFileMode>(DetermineUMask);
tmds marked this conversation as resolved.
Show resolved Hide resolved

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<string>
{
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<string, UnixFileMode>? CreatePendingModesDictionary()
=> new SortedDictionary<string, UnixFileMode>(s_reverseStringComparer);

internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool overwriteMetadata, SortedDictionary<string, UnixFileMode>? pendingModes)
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved
{
// 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);
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved

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 & ExtractPermissions) == ExtractPermissions;
tmds marked this conversation as resolved.
Show resolved Hide resolved
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 & ExtractPermissions) != ExtractPermissions)
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
tmds marked this conversation as resolved.
Show resolved Hide resolved

if (hasMissingParents)
{
Directory.CreateDirectory(parentDir, ExtractPermissions);
}

Directory.CreateDirectory(fullPath, mode.Value);
}

internal static void SetPendingModes(SortedDictionary<string, UnixFileMode>? pendingModes)
eerhardt marked this conversation as resolved.
Show resolved Hide resolved
{
Debug.Assert(!OperatingSystem.IsWindows());
tmds marked this conversation as resolved.
Show resolved Hide resolved
Debug.Assert(pendingModes is not null);

if (pendingModes.Count == 0)
{
return;
}

UnixFileMode umask = UMask;
foreach (KeyValuePair<string, UnixFileMode> dir in pendingModes)
{
File.SetUnixFileMode(dir.Key, dir.Value & ~umask);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<string, UnixFileMode>? CreatePendingModesDictionary()
=> null;

internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool overwriteMetadata, SortedDictionary<string, UnixFileMode>? pendingModes)
=> Directory.CreateDirectory(fullPath);

internal static void SetPendingModes(SortedDictionary<string, UnixFileMode>? pendingModes)
=> Debug.Assert(pendingModes is null);
}
}
Loading