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

Add Directory.CreateTempSubdirectory #73408

Merged
merged 7 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Sys
{
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_MkdTemp", SetLastError = true)]
internal static unsafe partial byte* MkdTemp(byte* template);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ internal static partial class Interop
internal static partial class Sys
{
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_MksTemps", SetLastError = true)]
internal static partial IntPtr MksTemps(
byte[] template,
internal static unsafe partial IntPtr MksTemps(
byte* template,
int suffixlen);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ internal static partial class Kernel32
/// </summary>
[LibraryImport(Libraries.Kernel32, EntryPoint = "CreateDirectoryW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CreateDirectoryPrivate(
private static unsafe partial bool CreateDirectoryPrivate(
string path,
ref SECURITY_ATTRIBUTES lpSecurityAttributes);
SECURITY_ATTRIBUTES* lpSecurityAttributes);

internal static bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes)
internal static unsafe bool CreateDirectory(string path, SECURITY_ATTRIBUTES* lpSecurityAttributes)
{
// We always want to add for CreateDirectory to get around the legacy 248 character limitation
path = PathInternal.EnsureExtendedPrefix(path);
return CreateDirectoryPrivate(path, ref lpSecurityAttributes);
return CreateDirectoryPrivate(path, lpSecurityAttributes);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr
string name = stackDir[stackDir.Count - 1];
stackDir.RemoveAt(stackDir.Count - 1);

r = Interop.Kernel32.CreateDirectory(name, ref secAttrs);
r = Interop.Kernel32.CreateDirectory(name, &secAttrs);
if (!r && (firstError == 0))
{
int currentError = Marshal.GetLastWin32Error();
Expand Down
1 change: 1 addition & 0 deletions src/libraries/Common/src/System/IO/PathInternal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal static partial class PathInternal
internal const char PathSeparator = ':';
internal const string DirectorySeparatorCharAsString = "/";
internal const string ParentDirectoryPrefix = @"../";
internal const string DirectorySeparators = DirectorySeparatorCharAsString;

internal static int GetRootLength(ReadOnlySpan<char> path)
{
Expand Down
1 change: 1 addition & 0 deletions src/libraries/Common/src/System/IO/PathInternal.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal static partial class PathInternal
internal const string UncNTPathPrefix = @"\??\UNC\";
internal const string DevicePathPrefix = @"\\.\";
internal const string ParentDirectoryPrefix = @"..\";
internal const string DirectorySeparators = @"\/";

internal const int MaxShortPath = 260;
internal const int MaxShortDirectoryPath = 248;
Expand Down
38 changes: 37 additions & 1 deletion src/libraries/Common/src/System/IO/TempFileCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TempFileCollection : ICollection, IDisposable
private string _basePath;
private readonly string _tempDir;
private readonly Hashtable _files;
private bool _createdTempDirectory;

public TempFileCollection() : this(null, false)
{
Expand Down Expand Up @@ -127,7 +128,7 @@ private void EnsureTempNameCreated()
do
{
_basePath = Path.Combine(
string.IsNullOrEmpty(TempDir) ? Path.GetTempPath() : TempDir,
string.IsNullOrEmpty(TempDir) ? GetTempDirectory() : TempDir,
Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
tempFileName = _basePath + ".tmp";

Expand All @@ -150,6 +151,19 @@ private void EnsureTempNameCreated()
}
}

#if NET7_0_OR_GREATER
private string GetTempDirectory()
{
_createdTempDirectory = true;
return Directory.CreateTempSubdirectory().FullName;
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
}
#else
private static string GetTempDirectory()
{
return Path.GetTempPath();
}
#endif

public bool KeepFiles { get; set; }

private bool KeepFile(string fileName)
Expand Down Expand Up @@ -177,6 +191,8 @@ private static void Delete(string fileName)

internal void SafeDelete()
{
bool allFilesDeleted = true;

if (_files != null && _files.Count > 0)
{
string[] fileNames = new string[_files.Count];
Expand All @@ -188,8 +204,28 @@ internal void SafeDelete()
Delete(fileName);
_files.Remove(fileName);
}
else
{
allFilesDeleted = false;
}
}
}

// if we created a temp directory, delete it and clear the basePath, so a new directory will be created for the next request.
if (_createdTempDirectory && allFilesDeleted)
{
try
{
Directory.Delete(Path.GetDirectoryName(BasePath));
}
catch
{
// Ignore all exceptions
}

_createdTempDirectory = false;
_basePath = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ public class DisposableFileSystem : IDisposable

public DisposableFileSystem()
{
RootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
CreateFolder("");
DirectoryInfo = new DirectoryInfo(RootPath);
#if NETCOREAPP
DirectoryInfo = Directory.CreateTempSubdirectory();
#else
DirectoryInfo = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
DirectoryInfo.Create();
#endif
RootPath = DirectoryInfo.FullName;
}

public string RootPath { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ public class DisposableFileSystem : IDisposable
{
public DisposableFileSystem()
{
RootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(RootPath);
DirectoryInfo = new DirectoryInfo(RootPath);
#if NETCOREAPP
DirectoryInfo = Directory.CreateTempSubdirectory();
#else
DirectoryInfo = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
DirectoryInfo.Create();
#endif
RootPath = DirectoryInfo.FullName;
}

public string RootPath { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,13 @@ private static string TempDirectory()
{
if (s_tempDirectory == null)
{
#if NETCOREAPP
string tempDirectory = Directory.CreateTempSubdirectory().FullName;
#else
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
#endif

s_tempDirectory = tempDirectory;
}
return s_tempDirectory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ private DirectoryCatalog CreateDirectoryCatalog(string path, string filter)

private string GetTemporaryDirectory(string location = null)
{
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
return tempDirectory;
return Directory.CreateTempSubdirectory().FullName;
}
}

Expand All @@ -193,16 +191,12 @@ public static string GetRootTemporaryDirectory()

public static string GetNewTemporaryDirectory()
{
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
return tempDirectory;
return Directory.CreateTempSubdirectory().FullName;
}

public static string GetTemporaryDirectory()
{
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
return tempDirectory;
return Directory.CreateTempSubdirectory().FullName;
eerhardt marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace System.IO.Tests
{
public class Directory_CreateTempSubdirectory : FileSystemTest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe verify that the filemode of the created directory is 700 on Unix. That is an important trait of this API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: is it on Windows also guaranteed the resulting directory is only accessible to the current user?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe verify that the filemode of the created directory is 700 on Unix.

Done.

Q: is it on Windows also guaranteed the resulting directory is only accessible to the current user?

No, on Windows there is no guarantee on the permissions of the directory. Just that it is empty, and it is in the TEMP directory.

{
public static TheoryData<string> CreateTempSubdirectoryData
{
get
{
var result = new TheoryData<string>() { null, "", "myDir", "my.Dir", "H\u00EBllo" };
if (!OperatingSystem.IsWindows())
{
// ensure we can use backslashes on Unix since that isn't a directory separator
result.Add(@"my\File");
result.Add(@"\");
}
return result;
}
}

[Theory]
[MemberData(nameof(CreateTempSubdirectoryData))]
public void CreateTempSubdirectory(string prefix)
{
DirectoryInfo tmpDir = Directory.CreateTempSubdirectory(prefix);
try
{
Assert.True(tmpDir.Exists);
Assert.Equal(-1, tmpDir.FullName.IndexOfAny(Path.GetInvalidPathChars()));
Assert.Empty(Directory.GetFileSystemEntries(tmpDir.FullName));
Assert.Equal(Path.TrimEndingDirectorySeparator(Path.GetTempPath()), tmpDir.Parent.FullName);

if (!string.IsNullOrEmpty(prefix))
{
Assert.StartsWith(prefix, tmpDir.Name);
int expectedNameLength = prefix.Length + (OperatingSystem.IsWindows() ? 12 : 6);
Assert.Equal(expectedNameLength, tmpDir.Name.Length);
}

if (!OperatingSystem.IsWindows())
{
UnixFileMode userRWX = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
Assert.Equal(userRWX, tmpDir.UnixFileMode);
}
eerhardt marked this conversation as resolved.
Show resolved Hide resolved

// Ensure a file can be written to the directory
string tempFile = Path.Combine(tmpDir.FullName, "newFile");
using (FileStream fs = File.Create(tempFile, bufferSize: 1024, FileOptions.DeleteOnClose))
{
Assert.Equal(0, fs.Length);
}
}
finally
{
tmpDir.Delete(recursive: true);
}
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void CreateTempSubdirectoryTempUnicode()
{
RemoteExecutor.Invoke(() =>
{
DirectoryInfo tempPathWithUnicode = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + "\u00F6"));
tempPathWithUnicode.Create();
string tempEnvVar = OperatingSystem.IsWindows() ? "TMP" : "TMPDIR";
Environment.SetEnvironmentVariable(tempEnvVar, tempPathWithUnicode.FullName);
try
{
DirectoryInfo tmpDir = Directory.CreateTempSubdirectory();
Assert.True(tmpDir.Exists);
Assert.Equal(tempPathWithUnicode.FullName, tmpDir.Parent.FullName);
Environment.SetEnvironmentVariable(tempEnvVar, tempPathWithUnicode.Parent.FullName);
}
finally
{
tempPathWithUnicode.Delete(recursive: true);
}
}).Dispose();
}

public static TheoryData<string> InvalidPrefixData
{
get
{
var result = new TheoryData<string>() { "/", "myDir/", "my/Dir" };
if (OperatingSystem.IsWindows())
{
result.Add(@"\");
result.Add(@"myDir\");
result.Add(@"my\Dir");
}
return result;
}
}

[Theory]
[MemberData(nameof(InvalidPrefixData))]
public void CreateTempSubdirectoryThrowsWithPrefixContainingDirectorySeparator(string prefix)
{
AssertExtensions.Throws<ArgumentException>("prefix", () => Directory.CreateTempSubdirectory(prefix));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.cs" />
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.FileSystem.cs" />
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.FileSystemInfo.cs" />
<Compile Include="Directory\CreateTempSubdirectory.cs" />
<Compile Include="Directory\EnumerableTests.cs" />
<Compile Include="Directory\SymbolicLinks.cs" />
<Compile Include="DirectoryInfo\SymbolicLinks.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,9 @@
<data name="IO_UnknownFileName" xml:space="preserve">
<value>[Unknown]</value>
</data>
<data name="IO_MaxAttemptsReached" xml:space="preserve">
<value>Unable to complete the operation after the maximum number of attempts.</value>
</data>
<data name="Lazy_CreateValue_NoParameterlessCtorForT" xml:space="preserve">
<value>The lazily-initialized type does not have a public, parameterless constructor.</value>
</data>
Expand Down Expand Up @@ -3988,4 +3991,7 @@
<data name="FileNotFound_AssemblyNotFound" xml:space="preserve">
<value>Cannot load assembly '{0}'. No metadata found for this assembly.</value>
</data>
<data name="Argument_DirectorySeparatorInvalid" xml:space="preserve">
<value>The value may not contain directory separator characters.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -2093,6 +2093,9 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.MkDir.cs">
<Link>Common\Interop\Unix\System.Native\Interop.MkDir.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.MkdTemp.cs">
<Link>Common\Interop\Unix\System.Native\Interop.MkdTemp.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.MksTemps.cs">
<Link>Common\Interop\Unix\System.Native\Interop.MksTemps.cs</Link>
</Compile>
Expand Down
Loading