Skip to content

Commit b284fa6

Browse files
authored
Directory.CreateDirectory: create missing parents using default UnixFileMode. (#74002)
* Directory.CreateDirectory: create missing parents using default UnixFileMode. * Simplify TarHelpers.CreateDirectory. * PR feedback. * AssertFileModeEquals: don't skip on Android. * Always overwite directory metadata. * TarHelpers.Windows: remove CreateDirectory overwriteMetadata. * Fix Assert indentation.
1 parent d5af379 commit b284fa6

File tree

8 files changed

+82
-72
lines changed

8 files changed

+82
-72
lines changed

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,12 @@ internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool o
287287

288288
if (EntryType == TarEntryType.Directory)
289289
{
290-
TarHelpers.CreateDirectory(fileDestinationPath, Mode, overwrite, pendingModes);
290+
TarHelpers.CreateDirectory(fileDestinationPath, Mode, pendingModes);
291291
}
292292
else
293293
{
294294
// If it is a file, create containing directory.
295-
TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, overwrite, pendingModes);
295+
TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, pendingModes);
296296
ExtractToFileInternal(fileDestinationPath, linkTargetPath, overwrite);
297297
}
298298
}
@@ -309,13 +309,13 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b
309309

310310
if (EntryType == TarEntryType.Directory)
311311
{
312-
TarHelpers.CreateDirectory(fileDestinationPath, Mode, overwrite, pendingModes);
312+
TarHelpers.CreateDirectory(fileDestinationPath, Mode, pendingModes);
313313
return Task.CompletedTask;
314314
}
315315
else
316316
{
317317
// If it is a file, create containing directory.
318-
TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, overwrite, pendingModes);
318+
TarHelpers.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!, mode: null, pendingModes);
319319
return ExtractToFileInternalAsync(fileDestinationPath, linkTargetPath, overwrite, cancellationToken);
320320
}
321321
}

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,57 +47,41 @@ public int Compare (string? x, string? y)
4747

4848
private static UnixFileMode UMask => s_umask.Value;
4949

50-
/*
51-
Tar files are usually ordered: parent directories come before their child entries.
52-
53-
They may be unordered. In that case we need to create parent directories before
54-
we know the proper permissions for these directories.
55-
56-
We create these directories with restrictive permissions. If we encounter an entry for
57-
the directory later, we store the mode to apply it later.
58-
59-
If the archive doesn't have an entry for the parent directory, we use the default mask.
60-
61-
The pending modes to be applied are tracked through a reverse-sorted dictionary.
62-
The reverse order is needed to apply permissions to children before their parent.
63-
Otherwise we may apply a restrictive mask to the parent, that prevents us from
64-
changing a child.
65-
*/
66-
50+
// Use a reverse-sorted dictionary to apply permission to children before their parents.
51+
// Otherwise we may apply a restrictive mask to the parent, that prevents us from changing a child.
6752
internal static SortedDictionary<string, UnixFileMode>? CreatePendingModesDictionary()
6853
=> new SortedDictionary<string, UnixFileMode>(s_reverseStringComparer);
6954

70-
internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool overwriteMetadata, SortedDictionary<string, UnixFileMode>? pendingModes)
55+
internal static void CreateDirectory(string fullPath, UnixFileMode? mode, SortedDictionary<string, UnixFileMode>? pendingModes)
7156
{
72-
// Restrictive mask for creating the missing parent directories while extracting.
57+
// Minimal permissions required for extracting.
7358
const UnixFileMode ExtractPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
7459

7560
Debug.Assert(pendingModes is not null);
7661

7762
if (Directory.Exists(fullPath))
7863
{
79-
// Apply permissions to an existing directory when we're overwriting metadata
80-
// or the directory was created as a missing parent (stored in pendingModes).
64+
// Apply permissions to an existing directory.
8165
if (mode.HasValue)
8266
{
67+
// Ensure we have sufficient permissions to extract in the directory.
8368
bool hasExtractPermissions = (mode.Value & ExtractPermissions) == ExtractPermissions;
8469
if (hasExtractPermissions)
8570
{
86-
bool removed = pendingModes.Remove(fullPath);
87-
if (overwriteMetadata || removed)
88-
{
89-
UnixFileMode umask = UMask;
90-
File.SetUnixFileMode(fullPath, mode.Value & ~umask);
91-
}
71+
pendingModes.Remove(fullPath);
72+
73+
UnixFileMode umask = UMask;
74+
File.SetUnixFileMode(fullPath, mode.Value & ~umask);
9275
}
93-
else if (overwriteMetadata || pendingModes.ContainsKey(fullPath))
76+
else
9477
{
95-
pendingModes[fullPath] = mode.Value;
78+
pendingModes[fullPath] = mode.Value;
9679
}
9780
}
9881
return;
9982
}
10083

84+
// If there are missing parents, Directory.CreateDirectory will create them using default permissions.
10185
if (mode.HasValue)
10286
{
10387
// Ensure we have sufficient permissions to extract in the directory.
@@ -106,28 +90,13 @@ internal static void CreateDirectory(string fullPath, UnixFileMode? mode, bool o
10690
pendingModes[fullPath] = mode.Value;
10791
mode = ExtractPermissions;
10892
}
109-
}
110-
else
111-
{
112-
pendingModes.Add(fullPath, DefaultDirectoryMode);
113-
mode = ExtractPermissions;
114-
}
11593

116-
string parentDir = Path.GetDirectoryName(fullPath)!;
117-
string rootDir = Path.GetPathRoot(parentDir)!;
118-
bool hasMissingParents = false;
119-
for (string dir = parentDir; dir != rootDir && !Directory.Exists(dir); dir = Path.GetDirectoryName(dir)!)
120-
{
121-
pendingModes.Add(dir, DefaultDirectoryMode);
122-
hasMissingParents = true;
94+
Directory.CreateDirectory(fullPath, mode.Value);
12395
}
124-
125-
if (hasMissingParents)
96+
else
12697
{
127-
Directory.CreateDirectory(parentDir, ExtractPermissions);
98+
Directory.CreateDirectory(fullPath);
12899
}
129-
130-
Directory.CreateDirectory(fullPath, mode.Value);
131100
}
132101

133102
internal static void SetPendingModes(SortedDictionary<string, UnixFileMode>? pendingModes)

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal static partial class TarHelpers
1313
internal static SortedDictionary<string, UnixFileMode>? CreatePendingModesDictionary()
1414
=> null;
1515

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

1919
internal static void SetPendingModes(SortedDictionary<string, UnixFileMode>? pendingModes)

src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,9 @@ public void UnixFileModes(bool overwrite)
216216
Assert.True(File.Exists(filePath), $"{filePath}' does not exist.");
217217
AssertFileModeEquals(filePath, TestPermission2);
218218

219-
// Missing parents are created with DefaultDirectoryMode.
220-
// The mode is not set when overwrite == true if there is no entry and the directory exists before extracting.
219+
// Missing parents are created with CreateDirectoryDefaultMode.
221220
Assert.True(Directory.Exists(missingParentPath), $"{missingParentPath}' does not exist.");
222-
if (!overwrite)
223-
{
224-
AssertFileModeEquals(missingParentPath, DefaultDirectoryMode);
225-
}
221+
AssertFileModeEquals(missingParentPath, CreateDirectoryDefaultMode);
226222

227223
Assert.True(Directory.Exists(missingParentDirPath), $"{missingParentDirPath}' does not exist.");
228224
AssertFileModeEquals(missingParentDirPath, TestPermission3);

src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.File.Tests.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,10 @@ public async Task ExtractArchiveWithEntriesThatStartWithSlashDotPrefix_Async()
191191
}
192192
}
193193

194-
[Fact]
195-
public async Task UnixFileModes_Async()
194+
[Theory]
195+
[InlineData(true)]
196+
[InlineData(false)]
197+
public async Task UnixFileModes_Async(bool overwrite)
196198
{
197199
using TempDirectory source = new TempDirectory();
198200
using TempDirectory destination = new TempDirectory();
@@ -223,27 +225,36 @@ public async Task UnixFileModes_Async()
223225
writer.WriteEntry(outOfOrderDir);
224226
}
225227

226-
await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: false);
227-
228228
string dirPath = Path.Join(destination.Path, "dir");
229+
string filePath = Path.Join(destination.Path, "file");
230+
string missingParentPath = Path.Join(destination.Path, "missing_parent");
231+
string missingParentDirPath = Path.Join(missingParentPath, "dir");
232+
string outOfOrderDirPath = Path.Join(destination.Path, "out_of_order_parent");
233+
234+
if (overwrite)
235+
{
236+
File.OpenWrite(filePath).Dispose();
237+
Directory.CreateDirectory(dirPath);
238+
Directory.CreateDirectory(missingParentDirPath);
239+
Directory.CreateDirectory(outOfOrderDirPath);
240+
}
241+
242+
await TarFile.ExtractToDirectoryAsync(archivePath, destination.Path, overwriteFiles: overwrite);
243+
229244
Assert.True(Directory.Exists(dirPath), $"{dirPath}' does not exist.");
230245
AssertFileModeEquals(dirPath, TestPermission1);
231246

232-
string filePath = Path.Join(destination.Path, "file");
233247
Assert.True(File.Exists(filePath), $"{filePath}' does not exist.");
234248
AssertFileModeEquals(filePath, TestPermission2);
235249

236-
// Missing parents are created with DefaultDirectoryMode.
237-
string missingParentPath = Path.Join(destination.Path, "missing_parent");
250+
// Missing parents are created with CreateDirectoryDefaultMode.
238251
Assert.True(Directory.Exists(missingParentPath), $"{missingParentPath}' does not exist.");
239-
AssertFileModeEquals(missingParentPath, DefaultDirectoryMode);
252+
AssertFileModeEquals(missingParentPath, CreateDirectoryDefaultMode);
240253

241-
string missingParentDirPath = Path.Join(missingParentPath, "dir");
242254
Assert.True(Directory.Exists(missingParentDirPath), $"{missingParentDirPath}' does not exist.");
243255
AssertFileModeEquals(missingParentDirPath, TestPermission3);
244256

245257
// Directory modes that are out-of-order are still applied.
246-
string outOfOrderDirPath = Path.Join(destination.Path, "out_of_order_parent");
247258
Assert.True(Directory.Exists(outOfOrderDirPath), $"{outOfOrderDirPath}' does not exist.");
248259
AssertFileModeEquals(outOfOrderDirPath, TestPermission4);
249260
}

src/libraries/System.Formats.Tar/tests/TarTestsBase.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public abstract partial class TarTestsBase : FileCleanupTestBase
1717
protected const UnixFileMode DefaultFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead; // 644 in octal, internally used as default
1818
protected const UnixFileMode DefaultDirectoryMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; // 755 in octal, internally used as default
1919

20+
protected readonly UnixFileMode CreateDirectoryDefaultMode; // Mode of directories created using Directory.CreateDirectory(string).
21+
protected readonly UnixFileMode UMask;
22+
2023
// Mode assumed for files and directories on Windows.
2124
protected const UnixFileMode DefaultWindowsMode = DefaultFileMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; // 755 in octal, internally used as default
2225

@@ -115,6 +118,12 @@ public enum TestTarFormat
115118

116119
protected static bool IsNotLinuxBionic => !PlatformDetection.IsLinuxBionic;
117120

121+
protected TarTestsBase()
122+
{
123+
CreateDirectoryDefaultMode = Directory.CreateDirectory(GetRandomDirPath()).UnixFileMode; // '0777 & ~umask'
124+
UMask = ~CreateDirectoryDefaultMode & (UnixFileMode)Convert.ToInt32("777", 8);
125+
}
126+
118127
protected static string GetTestCaseUnarchivedFolderPath(string testCaseName) =>
119128
Path.Join(Directory.GetCurrentDirectory(), "unarchived", testCaseName);
120129

@@ -407,11 +416,20 @@ protected static void AssertEntryModeFromFileSystemEquals(TarEntry entry, UnixFi
407416
Assert.Equal(fileMode, entry.Mode);
408417
}
409418

410-
protected static void AssertFileModeEquals(string path, UnixFileMode mode)
419+
protected void AssertFileModeEquals(string path, UnixFileMode archiveMode)
411420
{
412421
if (!PlatformDetection.IsWindows)
413422
{
414-
Assert.Equal(mode, File.GetUnixFileMode(path));
423+
UnixFileMode expectedMode = archiveMode & ~UMask;
424+
425+
UnixFileMode actualMode = File.GetUnixFileMode(path);
426+
// Ignore SetGroup: it may have been added to preserve group ownership.
427+
if ((expectedMode & UnixFileMode.SetGroup) == 0)
428+
{
429+
actualMode &= ~UnixFileMode.SetGroup;
430+
}
431+
432+
Assert.Equal(expectedMode, actualMode);
415433
}
416434
}
417435

src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Unix.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ public void CreateDoesntChangeExistingMode()
3939
Assert.Equal(initialMode, sameDir.UnixFileMode);
4040
}
4141

42+
[Fact]
43+
public void MissingParentsHaveDefaultPermissions()
44+
{
45+
string parent = GetRandomDirPath();
46+
string child = Path.Combine(parent, "child");
47+
48+
const UnixFileMode childMode = UnixFileMode.UserRead | UnixFileMode.UserExecute;
49+
DirectoryInfo childDir = Directory.CreateDirectory(child, childMode);
50+
51+
Assert.Equal(childMode, childDir.UnixFileMode);
52+
53+
UnixFileMode defaultPermissions = Directory.CreateDirectory(GetRandomDirPath()).UnixFileMode;
54+
Assert.Equal(defaultPermissions, File.GetUnixFileMode(parent));
55+
}
56+
4257
[Theory]
4358
[InlineData((UnixFileMode)(1 << 12), false)]
4459
[InlineData((UnixFileMode)(1 << 12), true)]

src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ private static void CreateParentsAndDirectory(string fullPath, UnixFileMode unix
328328
}
329329

330330
ReadOnlySpan<char> mkdirPath = fullPath.AsSpan(0, i);
331-
int result = Interop.Sys.MkDir(mkdirPath, (int)unixCreateMode);
331+
int result = Interop.Sys.MkDir(mkdirPath, (int)DefaultUnixCreateDirectoryMode);
332332
if (result == 0)
333333
{
334334
break; // Created parent.
@@ -360,7 +360,8 @@ private static void CreateParentsAndDirectory(string fullPath, UnixFileMode unix
360360
for (i = stackDir.Length - 1; i >= 0; i--)
361361
{
362362
ReadOnlySpan<char> mkdirPath = fullPath.AsSpan(0, stackDir[i]);
363-
int result = Interop.Sys.MkDir(mkdirPath, (int)unixCreateMode);
363+
UnixFileMode mode = i == 0 ? unixCreateMode : DefaultUnixCreateDirectoryMode;
364+
int result = Interop.Sys.MkDir(mkdirPath, (int)mode);
364365
if (result < 0)
365366
{
366367
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();

0 commit comments

Comments
 (0)