diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateDirectory.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateDirectory.cs index c679fbbb023dbd..b0a75243048b7d 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateDirectory.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateDirectory.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Win32.SafeHandles; using System.IO; using System.Runtime.InteropServices; @@ -20,8 +19,9 @@ private static partial bool CreateDirectoryPrivate( internal static bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes) { - // We always want to add for CreateDirectory to get around the legacy 248 character limitation - path = PathInternal.EnsureExtendedPrefix(path); + // If length is greater than `MaxShortDirectoryPath` we add a extended prefix to get around the legacy character limitation + path = PathInternal.EnsureExtendedPrefixIfNeeded(path); + return CreateDirectoryPrivate(path, ref lpSecurityAttributes); } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFileAttributesEx.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFileAttributesEx.cs index 59f6d4992e89f0..914bef9040a54e 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFileAttributesEx.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFileAttributesEx.cs @@ -13,7 +13,7 @@ internal static partial class Kernel32 /// [LibraryImport(Libraries.Kernel32, EntryPoint = "GetFileAttributesExW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool GetFileAttributesExPrivate( + internal static partial bool GetFileAttributesExPrivate( string? name, GET_FILEEX_INFO_LEVELS fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); diff --git a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs index dab4cf7a8f6c5c..ae1cf616840a19 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs @@ -29,6 +29,17 @@ private static bool DirectoryExists(string? path, out int lastError) ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0); } + private static bool DirectoryExistsSlim(string? path, out int lastError) + { + Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; + lastError = FillAttributeInfoSlim(path, ref data, returnErrorOnNotFound: true); + + return + (lastError == 0) && + (data.dwFileAttributes != -1) && + ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0); + } + public static bool FileExists(string fullPath) { Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; @@ -49,53 +60,64 @@ public static bool FileExists(string fullPath) /// Return the error code for not found errors? internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data, bool returnErrorOnNotFound) { - int errorCode = Interop.Errors.ERROR_SUCCESS; - // Neither GetFileAttributes or FindFirstFile like trailing separators path = PathInternal.TrimEndingDirectorySeparator(path); using (DisableMediaInsertionPrompt.Create()) { - if (!Interop.Kernel32.GetFileAttributesEx(path, Interop.Kernel32.GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, ref data)) + return FillAttributeInfoSlim(path, ref data, returnErrorOnNotFound); + } + } + + /// + /// Same as without separator-trimming and media-insertion blocking + /// + /// The file path from which the file attribute information will be filled. + /// A struct that will contain the attribute information. + /// Return the error code for not found errors? + internal static int FillAttributeInfoSlim(string? path, ref Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data, bool returnErrorOnNotFound) + { + int errorCode = Interop.Errors.ERROR_SUCCESS; + string? prefixedString = PathInternal.EnsureExtendedPrefixIfNeeded(path); + + if (!Interop.Kernel32.GetFileAttributesExPrivate(prefixedString, Interop.Kernel32.GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, ref data)) + { + errorCode = Marshal.GetLastWin32Error(); + + Interop.Kernel32.WIN32_FIND_DATA findData = default; + if (!IsPathUnreachableError(errorCode)) { - errorCode = Marshal.GetLastWin32Error(); + // Assert so we can track down other cases (if any) to add to our test suite + Debug.Assert(errorCode == Interop.Errors.ERROR_ACCESS_DENIED || errorCode == Interop.Errors.ERROR_SHARING_VIOLATION || errorCode == Interop.Errors.ERROR_SEM_TIMEOUT, + $"Unexpected error code getting attributes {errorCode} from path {path}"); + + // Files that are marked for deletion will not let you GetFileAttributes, + // ERROR_ACCESS_DENIED is given back without filling out the data struct. + // FindFirstFile, however, will. Historically we always gave back attributes + // for marked-for-deletion files. + // + // Another case where enumeration works is with special system files such as + // pagefile.sys that give back ERROR_SHARING_VIOLATION on GetAttributes. + // + // Ideally we'd only try again for known cases due to the potential performance + // hit. The last attempt to do so baked for nearly a year before we found the + // pagefile.sys case. As such we're probably stuck filtering out specific + // cases that we know we don't want to retry on. - if (!IsPathUnreachableError(errorCode)) + using SafeFindHandle handle = Interop.Kernel32.FindFirstFile(prefixedString!, ref findData); + if (handle.IsInvalid) { - // Assert so we can track down other cases (if any) to add to our test suite - Debug.Assert(errorCode == Interop.Errors.ERROR_ACCESS_DENIED || errorCode == Interop.Errors.ERROR_SHARING_VIOLATION || errorCode == Interop.Errors.ERROR_SEM_TIMEOUT, - $"Unexpected error code getting attributes {errorCode} from path {path}"); - - // Files that are marked for deletion will not let you GetFileAttributes, - // ERROR_ACCESS_DENIED is given back without filling out the data struct. - // FindFirstFile, however, will. Historically we always gave back attributes - // for marked-for-deletion files. - // - // Another case where enumeration works is with special system files such as - // pagefile.sys that give back ERROR_SHARING_VIOLATION on GetAttributes. - // - // Ideally we'd only try again for known cases due to the potential performance - // hit. The last attempt to do so baked for nearly a year before we found the - // pagefile.sys case. As such we're probably stuck filtering out specific - // cases that we know we don't want to retry on. - - Interop.Kernel32.WIN32_FIND_DATA findData = default; - using (SafeFindHandle handle = Interop.Kernel32.FindFirstFile(path!, ref findData)) - { - if (handle.IsInvalid) - { - errorCode = Marshal.GetLastWin32Error(); - } - else - { - errorCode = Interop.Errors.ERROR_SUCCESS; - data.PopulateFrom(ref findData); - } - } + errorCode = Marshal.GetLastWin32Error(); + } + else + { + errorCode = Interop.Errors.ERROR_SUCCESS; + data.PopulateFrom(ref findData); } } } + if (errorCode != Interop.Errors.ERROR_SUCCESS && !returnErrorOnNotFound) { switch (errorCode) diff --git a/src/libraries/Common/src/System/IO/FileSystem.DirectoryCreation.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.DirectoryCreation.Windows.cs index dba07a8fb41567..9c688358330008 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.DirectoryCreation.Windows.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.DirectoryCreation.Windows.cs @@ -37,36 +37,44 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr int length = fullPath.Length; // We need to trim the trailing slash or the code will try to create 2 directories of the same name. - if (length >= 2 && PathInternal.EndsInDirectorySeparator(fullPath.AsSpan())) + if (length >= 2 && PathInternal.IsDirectorySeparator(fullPath.AsSpan()[^1])) { length--; } int lengthRoot = PathInternal.GetRootLength(fullPath.AsSpan()); - if (length > lengthRoot) + using (DisableMediaInsertionPrompt.Create()) { - // Special case root (fullpath = X:\\) - int i = length - 1; - while (i >= lengthRoot && !somepathexists) + if (length > lengthRoot) { - string dir = fullPath.Substring(0, i + 1); - - if (!DirectoryExists(dir)) // Create only the ones missing - { - stackDir.Add(dir); - } - else + // Special case root (fullpath = X:\\) + int i = length - 1; + while (i >= lengthRoot && !somepathexists) { - somepathexists = true; - } + ReadOnlySpan dir = fullPath.AsSpan()[..(i + 1)]; + + // Neither GetFileAttributes or FindFirstFile like trailing separators + dir = PathInternal.TrimEndingDirectorySeparator(dir); + + string dirString = new(dir); + + if (!DirectoryExistsSlim(dirString, out _)) // Create only the ones missing + { + stackDir.Add(dirString); + } + else + { + somepathexists = true; + } + + while (i > lengthRoot && !PathInternal.IsDirectorySeparator(fullPath[i])) + { + i--; + } - while (i > lengthRoot && !PathInternal.IsDirectorySeparator(fullPath[i])) - { i--; } - - i--; } } @@ -83,10 +91,9 @@ public static unsafe void CreateDirectory(string fullPath, byte[]? securityDescr lpSecurityDescriptor = (IntPtr)pSecurityDescriptor }; - while (stackDir.Count > 0) + for (int i = count - 1; i >= 0; i--) { - string name = stackDir[stackDir.Count - 1]; - stackDir.RemoveAt(stackDir.Count - 1); + string name = stackDir[i]; r = Interop.Kernel32.CreateDirectory(name, ref secAttrs); if (!r && (firstError == 0)) diff --git a/src/libraries/Common/src/System/IO/PathInternal.Windows.cs b/src/libraries/Common/src/System/IO/PathInternal.Windows.cs index 9015df805583e5..9ee11d0ab85607 100644 --- a/src/libraries/Common/src/System/IO/PathInternal.Windows.cs +++ b/src/libraries/Common/src/System/IO/PathInternal.Windows.cs @@ -72,11 +72,10 @@ internal static bool IsValidDriveChar(char value) return (uint)((value | 0x20) - 'a') <= (uint)('z' - 'a'); } - internal static bool EndsWithPeriodOrSpace(string? path) + internal static bool EndsWithPeriodOrSpace(string path) { if (string.IsNullOrEmpty(path)) return false; - char c = path[path.Length - 1]; return c == ' ' || c == '.'; } diff --git a/src/libraries/Common/src/System/IO/PathInternal.cs b/src/libraries/Common/src/System/IO/PathInternal.cs index 52aaa59072dd7b..a389dde7117340 100644 --- a/src/libraries/Common/src/System/IO/PathInternal.cs +++ b/src/libraries/Common/src/System/IO/PathInternal.cs @@ -16,7 +16,7 @@ internal static partial class PathInternal internal static bool StartsWithDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && IsDirectorySeparator(path[0]); internal static string EnsureTrailingSeparator(string path) - => EndsInDirectorySeparator(path.AsSpan()) ? path : path + DirectorySeparatorCharAsString; + => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]) ? path : path + DirectorySeparatorCharAsString; internal static bool IsRoot(ReadOnlySpan path) => path.Length == GetRootLength(path); @@ -231,16 +231,10 @@ internal static bool EndsInDirectorySeparator(string? path) => /// Trims one trailing directory separator beyond the root of the path. /// internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => - EndsInDirectorySeparator(path) && !IsRoot(path) ? + path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]) && !IsRoot(path) ? path.Slice(0, path.Length - 1) : path; - /// - /// Returns true if the path ends in a directory separator. - /// - internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => - path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); - internal static string GetLinkTargetFullPath(string path, string pathToTarget) => IsPartiallyQualified(pathToTarget.AsSpan()) ? Path.Join(Path.GetDirectoryName(path.AsSpan()), pathToTarget.AsSpan()) : pathToTarget; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs index b6edb311eac4cf..566971994b0f3f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs @@ -986,7 +986,7 @@ private static string GetRelativePath(string relativeTo, string path, StringComp /// /// Returns true if the path ends in a directory separator. /// - public static bool EndsInDirectorySeparator(ReadOnlySpan path) => PathInternal.EndsInDirectorySeparator(path); + public static bool EndsInDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]); /// /// Returns true if the path ends in a directory separator.