diff --git a/src/libraries/Common/src/System/IO/PathInternal.Unix.cs b/src/libraries/Common/src/System/IO/PathInternal.Unix.cs index cc1c9bc15f9456..912db66d56d16c 100644 --- a/src/libraries/Common/src/System/IO/PathInternal.Unix.cs +++ b/src/libraries/Common/src/System/IO/PathInternal.Unix.cs @@ -1,44 +1,97 @@ // 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.Diagnostics; using System.Text; +using System.Diagnostics.CodeAnalysis; namespace System.IO { /// Contains internal path helpers that are shared between many projects. internal static partial class PathInternal { + internal const char DirectorySeparatorChar = '/'; + internal const char AltDirectorySeparatorChar = '/'; + internal const char VolumeSeparatorChar = '/'; + internal const char PathSeparator = ':'; + internal const string DirectorySeparatorCharAsString = "/"; + internal const string ParentDirectoryPrefix = @"../"; + internal static int GetRootLength(ReadOnlySpan path) { return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0; } - internal static bool EndsInDirectorySeparator(ReadOnlySpan path) - => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); - - internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => - EndsInDirectorySeparator(path) && !IsRoot(path) ? - path.Slice(0, path.Length - 1) : - path; - - internal static bool IsRoot(ReadOnlySpan path) - => path.Length == GetRootLength(path); - internal static bool IsDirectorySeparator(char c) { // The alternate directory separator char is the same as the directory separator, // so we only need to check one. - Debug.Assert(Path.DirectorySeparatorChar == Path.AltDirectorySeparatorChar); - return c == Path.DirectorySeparatorChar; + Debug.Assert(DirectorySeparatorChar == AltDirectorySeparatorChar); + return c == DirectorySeparatorChar; } - internal static bool IsPartiallyQualified(string path) + /// + /// Normalize separators in the given path. Compresses forward slash runs. + /// + [return: NotNullIfNotNull("path")] + internal static string? NormalizeDirectorySeparators(string? path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // Make a pass to see if we need to normalize so we can potentially skip allocating + bool normalized = true; + + for (int i = 0; i < path.Length; i++) + { + if (IsDirectorySeparator(path[i]) + && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) + { + normalized = false; + break; + } + } + + if (normalized) + return path; + + StringBuilder builder = new StringBuilder(path.Length); + + for (int i = 0; i < path.Length; i++) + { + char current = path[i]; + + // Skip if we have another separator following + if (IsDirectorySeparator(current) + && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) + continue; + + builder.Append(current); + } + + return builder.ToString(); + } + + internal static bool IsPartiallyQualified(ReadOnlySpan path) { // This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative) // As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified. - return string.IsNullOrEmpty(path) || path[0] != Path.DirectorySeparatorChar; + return !Path.IsPathRooted(path); + } + + /// + /// Returns true if the path is effectively empty for the current OS. + /// For unix, this is empty or null. For Windows, this is empty, null, or + /// just spaces ((char)32). + /// + internal static bool IsEffectivelyEmpty(string? path) + { + return string.IsNullOrEmpty(path); + } + + internal static bool IsEffectivelyEmpty(ReadOnlySpan path) + { + return path.IsEmpty; } } } diff --git a/src/libraries/Common/src/System/IO/PathInternal.Windows.cs b/src/libraries/Common/src/System/IO/PathInternal.Windows.cs index 3d5789183fda81..b1767b35adea5a 100644 --- a/src/libraries/Common/src/System/IO/PathInternal.Windows.cs +++ b/src/libraries/Common/src/System/IO/PathInternal.Windows.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Text; namespace System.IO { @@ -38,26 +39,38 @@ internal static partial class PathInternal // Local and Global MS-DOS Device Names // https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx - internal const string ExtendedDevicePathPrefix = @"\\?\"; + internal const char DirectorySeparatorChar = '\\'; + internal const char AltDirectorySeparatorChar = '/'; + internal const char VolumeSeparatorChar = ':'; + internal const char PathSeparator = ';'; + + internal const string DirectorySeparatorCharAsString = "\\"; + + internal const string ExtendedPathPrefix = @"\\?\"; internal const string UncPathPrefix = @"\\"; - internal const string UncDevicePrefixToInsert = @"?\UNC\"; + internal const string UncExtendedPrefixToInsert = @"?\UNC\"; internal const string UncExtendedPathPrefix = @"\\?\UNC\"; internal const string DevicePathPrefix = @"\\.\"; + internal const string ParentDirectoryPrefix = @"..\"; internal const int MaxShortPath = 260; - + internal const int MaxShortDirectoryPath = 248; // \\?\, \\.\, \??\ internal const int DevicePrefixLength = 4; + // \\ + internal const int UncPrefixLength = 2; + // \\?\UNC\, \\.\UNC\ + internal const int UncExtendedPrefixLength = 8; /// /// Returns true if the given character is a valid drive letter /// internal static bool IsValidDriveChar(char value) { - return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); + return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'); } - private static bool EndsWithPeriodOrSpace(string path) + internal static bool EndsWithPeriodOrSpace(string? path) { if (string.IsNullOrEmpty(path)) return false; @@ -101,20 +114,20 @@ internal static string EnsureExtendedPrefix(string path) // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a // normalized base path.) - if (IsPartiallyQualified(path) || IsDevice(path)) + if (IsPartiallyQualified(path.AsSpan()) || IsDevice(path.AsSpan())) return path; // Given \\server\share in longpath becomes \\?\UNC\server\share if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase)) - return path.Insert(2, UncDevicePrefixToInsert); + return path.Insert(2, UncExtendedPrefixToInsert); - return ExtendedDevicePathPrefix + path; + return ExtendedPathPrefix + path; } /// /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") /// - internal static bool IsDevice(string path) + internal static bool IsDevice(ReadOnlySpan path) { // If the path begins with any two separators is will be recognized and normalized and prepped with // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. @@ -129,12 +142,25 @@ internal static bool IsDevice(string path) ); } + /// + /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\) + /// + internal static bool IsDeviceUNC(ReadOnlySpan path) + { + return path.Length >= UncExtendedPrefixLength + && IsDevice(path) + && IsDirectorySeparator(path[7]) + && path[4] == 'U' + && path[5] == 'N' + && path[6] == 'C'; + } + /// /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization /// and path length checks. /// - internal static bool IsExtended(string path) + internal static bool IsExtended(ReadOnlySpan path) { // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. // Skipping of normalization will *only* occur if back slashes ('\') are used. @@ -145,6 +171,66 @@ internal static bool IsExtended(string path) && path[3] == '\\'; } + /// + /// Gets the length of the root of the path (drive, share, etc.). + /// + internal static int GetRootLength(ReadOnlySpan path) + { + int pathLength = path.Length; + int i = 0; + + bool deviceSyntax = IsDevice(path); + bool deviceUnc = deviceSyntax && IsDeviceUNC(path); + + if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0])) + { + // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") + if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1]))) + { + // UNC (\\?\UNC\ or \\), scan past server\share + + // Start past the prefix ("\\" or "\\?\UNC\") + i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength; + + // Skip two separators at most + int n = 2; + while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) + i++; + } + else + { + // Current drive rooted (e.g. "\foo") + i = 1; + } + } + else if (deviceSyntax) + { + // Device path (e.g. "\\?\.", "\\.\") + // Skip any characters following the prefix that aren't a separator + i = DevicePrefixLength; + while (i < pathLength && !IsDirectorySeparator(path[i])) + i++; + + // If there is another separator take it, as long as we have had at least one + // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\") + if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i])) + i++; + } + else if (pathLength >= 2 + && path[1] == VolumeSeparatorChar + && IsValidDriveChar(path[0])) + { + // Valid drive specified path ("C:", "D:", etc.) + i = 2; + + // If the colon is followed by a directory separator, move past it (e.g "C:\") + if (pathLength > 2 && IsDirectorySeparator(path[2])) + i++; + } + + return i; + } + /// /// Returns true if the path specified is relative to the current drive or working directory. /// Returns false if the path is fixed to a specific drive or UNC path. This method does no @@ -157,7 +243,7 @@ internal static bool IsExtended(string path) /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory /// will not be used to modify the path). /// - internal static bool IsPartiallyQualified(string path) + internal static bool IsPartiallyQualified(ReadOnlySpan path) { if (path.Length < 2) { @@ -176,7 +262,7 @@ internal static bool IsPartiallyQualified(string path) // The only way to specify a fixed path that doesn't begin with two slashes // is the drive, colon, slash format- i.e. C:\ return !((path.Length >= 3) - && (path[1] == Path.VolumeSeparatorChar) + && (path[1] == VolumeSeparatorChar) && IsDirectorySeparator(path[2]) // To match old behavior we'll check the drive character for validity as the path is technically // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. @@ -189,7 +275,116 @@ internal static bool IsPartiallyQualified(string path) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsDirectorySeparator(char c) { - return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; + } + + /// + /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present. + /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip). + /// + /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false. + /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as + /// such can't be used here (and is overkill for our uses). + /// + /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments. + /// + /// + /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do + /// not need trimming of trailing whitespace here. + /// + /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization. + /// + /// For legacy .NET Framework behavior with ExpandShortPaths: + /// - It has no impact on GetPathRoot() so doesn't need consideration. + /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share). + /// + /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was + /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you + /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by + /// this undocumented behavior. + /// + /// We won't match this old behavior because: + /// + /// 1. It was undocumented + /// 2. It was costly (extremely so if it actually contained '~') + /// 3. Doesn't play nice with string logic + /// 4. Isn't a cross-plat friendly concept/behavior + /// + [return: NotNullIfNotNull("path")] + internal static string? NormalizeDirectorySeparators(string? path) + { + if (string.IsNullOrEmpty(path)) + return path; + + char current; + + // Make a pass to see if we need to normalize so we can potentially skip allocating + bool normalized = true; + + for (int i = 0; i < path.Length; i++) + { + current = path[i]; + if (IsDirectorySeparator(current) + && (current != DirectorySeparatorChar + // Check for sequential separators past the first position (we need to keep initial two for UNC/extended) + || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])))) + { + normalized = false; + break; + } + } + + if (normalized) + return path; + + var builder = new ValueStringBuilder(stackalloc char[MaxShortPath]); + + int start = 0; + if (IsDirectorySeparator(path[start])) + { + start++; + builder.Append(DirectorySeparatorChar); + } + + for (int i = start; i < path.Length; i++) + { + current = path[i]; + + // If we have a separator + if (IsDirectorySeparator(current)) + { + // If the next is a separator, skip adding this + if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) + { + continue; + } + + // Ensure it is the primary separator + current = DirectorySeparatorChar; + } + + builder.Append(current); + } + + return builder.ToString(); + } + + /// + /// Returns true if the path is effectively empty for the current OS. + /// For unix, this is empty or null. For Windows, this is empty, null, or + /// just spaces ((char)32). + /// + internal static bool IsEffectivelyEmpty(ReadOnlySpan path) + { + if (path.IsEmpty) + return true; + + foreach (char c in path) + { + if (c != ' ') + return false; + } + return true; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs b/src/libraries/Common/src/System/IO/PathInternal.cs similarity index 100% rename from src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs rename to src/libraries/Common/src/System/IO/PathInternal.cs diff --git a/src/libraries/Common/tests/Common.Tests.csproj b/src/libraries/Common/tests/Common.Tests.csproj index ba76497cfc5568..ff71eff871a294 100644 --- a/src/libraries/Common/tests/Common.Tests.csproj +++ b/src/libraries/Common/tests/Common.Tests.csproj @@ -1,4 +1,4 @@ - + true true @@ -100,7 +100,7 @@ Link="Common\System\Net\Logging\NetEventSource.Common.cs" /> - @@ -109,7 +109,7 @@ - @@ -126,7 +126,7 @@ - diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index 674d785cb7e8bb..5c278ee3b83d2b 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -130,9 +130,9 @@ Link="System\IO\Path.Windows.cs" /> - - diff --git a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj index 0ffa0044e2a167..e8f806f3fbe47d 100644 --- a/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj +++ b/src/libraries/System.IO.Compression/src/System.IO.Compression.csproj @@ -1,4 +1,4 @@ - + true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser @@ -41,8 +41,6 @@ - @@ -51,8 +49,6 @@ - diff --git a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj index 6e662893f353c2..0d28f28e065c27 100644 --- a/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj +++ b/src/libraries/System.IO.FileSystem.AccessControl/src/System.IO.FileSystem.AccessControl.csproj @@ -64,9 +64,9 @@ Link="Common\System\IO\DisableMediaInsertionPrompt.cs" /> - - diff --git a/src/libraries/System.IO.FileSystem.DriveInfo/src/System.IO.FileSystem.DriveInfo.csproj b/src/libraries/System.IO.FileSystem.DriveInfo/src/System.IO.FileSystem.DriveInfo.csproj index d07e89ec979be1..c00e72c5513f4f 100644 --- a/src/libraries/System.IO.FileSystem.DriveInfo/src/System.IO.FileSystem.DriveInfo.csproj +++ b/src/libraries/System.IO.FileSystem.DriveInfo/src/System.IO.FileSystem.DriveInfo.csproj @@ -46,8 +46,6 @@ Link="Common\System\IO\Win32Marshal.cs" /> - @@ -60,8 +58,6 @@ Link="Common\Interop\Unix\Interop.UnixFileSystemTypes.cs" /> - + true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Linux;$(NetCoreAppCurrent);$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-MacCatalyst;$(NetCoreAppCurrent)-FreeBSD @@ -27,6 +27,8 @@ Link="Common\System\IO\PathInternal.CaseSensitivity.cs" /> + - - + + diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 736d3b1ee5654b..95d6943c20ce74 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -93,8 +93,8 @@ - - + + diff --git a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj index e017d4db8bf40a..1ab0b8cdd9161b 100644 --- a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj +++ b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj @@ -1,4 +1,4 @@ - + true $(DefineConstants);SERIAL_PORTS @@ -90,6 +90,8 @@ System.IO.Ports.SerialPort Link="Common\Interop\Windows\Interop.SECURITY_ATTRIBUTES.cs" /> + + diff --git a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs index 85eb2f4eaf8a52..56f5ef58522266 100644 --- a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs +++ b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs @@ -564,7 +564,13 @@ internal SerialStream(string portName, int baudRate, Parity parity, int dataBits } if (!portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase) || - !uint.TryParse(portName.Substring(3), out uint portNumber)) + !uint.TryParse( +#if NETCOREAPP + portName.AsSpan(3), +#else + portName.Substring(3), +#endif + out uint portNumber)) { throw new ArgumentException(SR.Format(SR.Arg_InvalidSerialPort, portName), nameof(portName)); } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index a1801b9c04b921..d86d04c886c825 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -433,7 +433,6 @@ - @@ -1213,6 +1212,9 @@ Common\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs + + Common\System\IO\PathInternal.cs + Common\System\IO\PathInternal.CaseSensitivity.cs @@ -1797,6 +1799,9 @@ Common\System\IO\FileSystem.DirectoryCreation.Windows.cs + + Common\System\IO\PathInternal.Windows.cs + @@ -1826,7 +1831,6 @@ - @@ -2085,6 +2089,9 @@ Common\System\Text\ValueUtf8Converter.cs + + Common\System\IO\PathInternal.Unix.cs + @@ -2113,7 +2120,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Unix.cs deleted file mode 100644 index 912db66d56d16c..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Unix.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Text; -using System.Diagnostics.CodeAnalysis; - -namespace System.IO -{ - /// Contains internal path helpers that are shared between many projects. - internal static partial class PathInternal - { - internal const char DirectorySeparatorChar = '/'; - internal const char AltDirectorySeparatorChar = '/'; - internal const char VolumeSeparatorChar = '/'; - internal const char PathSeparator = ':'; - internal const string DirectorySeparatorCharAsString = "/"; - internal const string ParentDirectoryPrefix = @"../"; - - internal static int GetRootLength(ReadOnlySpan path) - { - return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0; - } - - internal static bool IsDirectorySeparator(char c) - { - // The alternate directory separator char is the same as the directory separator, - // so we only need to check one. - Debug.Assert(DirectorySeparatorChar == AltDirectorySeparatorChar); - return c == DirectorySeparatorChar; - } - - /// - /// Normalize separators in the given path. Compresses forward slash runs. - /// - [return: NotNullIfNotNull("path")] - internal static string? NormalizeDirectorySeparators(string? path) - { - if (string.IsNullOrEmpty(path)) - return path; - - // Make a pass to see if we need to normalize so we can potentially skip allocating - bool normalized = true; - - for (int i = 0; i < path.Length; i++) - { - if (IsDirectorySeparator(path[i]) - && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) - { - normalized = false; - break; - } - } - - if (normalized) - return path; - - StringBuilder builder = new StringBuilder(path.Length); - - for (int i = 0; i < path.Length; i++) - { - char current = path[i]; - - // Skip if we have another separator following - if (IsDirectorySeparator(current) - && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) - continue; - - builder.Append(current); - } - - return builder.ToString(); - } - - internal static bool IsPartiallyQualified(ReadOnlySpan path) - { - // This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative) - // As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified. - return !Path.IsPathRooted(path); - } - - /// - /// Returns true if the path is effectively empty for the current OS. - /// For unix, this is empty or null. For Windows, this is empty, null, or - /// just spaces ((char)32). - /// - internal static bool IsEffectivelyEmpty(string? path) - { - return string.IsNullOrEmpty(path); - } - - internal static bool IsEffectivelyEmpty(ReadOnlySpan path) - { - return path.IsEmpty; - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs deleted file mode 100644 index b1767b35adea5a..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs +++ /dev/null @@ -1,390 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text; - -namespace System.IO -{ - /// Contains internal path helpers that are shared between many projects. - internal static partial class PathInternal - { - // All paths in Win32 ultimately end up becoming a path to a File object in the Windows object manager. Passed in paths get mapped through - // DosDevice symbolic links in the object tree to actual File objects under \Devices. To illustrate, this is what happens with a typical - // path "Foo" passed as a filename to any Win32 API: - // - // 1. "Foo" is recognized as a relative path and is appended to the current directory (say, "C:\" in our example) - // 2. "C:\Foo" is prepended with the DosDevice namespace "\??\" - // 3. CreateFile tries to create an object handle to the requested file "\??\C:\Foo" - // 4. The Object Manager recognizes the DosDevices prefix and looks - // a. First in the current session DosDevices ("\Sessions\1\DosDevices\" for example, mapped network drives go here) - // b. If not found in the session, it looks in the Global DosDevices ("\GLOBAL??\") - // 5. "C:" is found in DosDevices (in our case "\GLOBAL??\C:", which is a symbolic link to "\Device\HarddiskVolume6") - // 6. The full path is now "\Device\HarddiskVolume6\Foo", "\Device\HarddiskVolume6" is a File object and parsing is handed off - // to the registered parsing method for Files - // 7. The registered open method for File objects is invoked to create the file handle which is then returned - // - // There are multiple ways to directly specify a DosDevices path. The final format of "\??\" is one way. It can also be specified - // as "\\.\" (the most commonly documented way) and "\\?\". If the question mark syntax is used the path will skip normalization - // (essentially GetFullPathName()) and path length checks. - - // Windows Kernel-Mode Object Manager - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff565763.aspx - // https://channel9.msdn.com/Shows/Going+Deep/Windows-NT-Object-Manager - // - // Introduction to MS-DOS Device Names - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff548088.aspx - // - // Local and Global MS-DOS Device Names - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx - - internal const char DirectorySeparatorChar = '\\'; - internal const char AltDirectorySeparatorChar = '/'; - internal const char VolumeSeparatorChar = ':'; - internal const char PathSeparator = ';'; - - internal const string DirectorySeparatorCharAsString = "\\"; - - internal const string ExtendedPathPrefix = @"\\?\"; - internal const string UncPathPrefix = @"\\"; - internal const string UncExtendedPrefixToInsert = @"?\UNC\"; - internal const string UncExtendedPathPrefix = @"\\?\UNC\"; - internal const string DevicePathPrefix = @"\\.\"; - internal const string ParentDirectoryPrefix = @"..\"; - - internal const int MaxShortPath = 260; - internal const int MaxShortDirectoryPath = 248; - // \\?\, \\.\, \??\ - internal const int DevicePrefixLength = 4; - // \\ - internal const int UncPrefixLength = 2; - // \\?\UNC\, \\.\UNC\ - internal const int UncExtendedPrefixLength = 8; - - /// - /// Returns true if the given character is a valid drive letter - /// - internal static bool IsValidDriveChar(char value) - { - return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'); - } - - internal static bool EndsWithPeriodOrSpace(string? path) - { - if (string.IsNullOrEmpty(path)) - return false; - - char c = path[path.Length - 1]; - return c == ' ' || c == '.'; - } - - /// - /// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative, - /// AND the path is more than 259 characters. (> MAX_PATH + null). This will also insert the extended - /// prefix if the path ends with a period or a space. Trailing periods and spaces are normally eaten - /// away from paths during normalization, but if we see such a path at this point it should be - /// normalized and has retained the final characters. (Typically from one of the *Info classes) - /// - [return: NotNullIfNotNull("path")] - internal static string? EnsureExtendedPrefixIfNeeded(string? path) - { - if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path))) - { - return EnsureExtendedPrefix(path); - } - else - { - return path; - } - } - - /// - /// Adds the extended path prefix (\\?\) if not relative or already a device path. - /// - internal static string EnsureExtendedPrefix(string path) - { - // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which - // means adding to relative paths will prevent them from getting the appropriate current directory inserted. - - // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it - // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly - // in the future we wouldn't want normalization to come back and break existing code. - - // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this - // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a - // normalized base path.) - if (IsPartiallyQualified(path.AsSpan()) || IsDevice(path.AsSpan())) - return path; - - // Given \\server\share in longpath becomes \\?\UNC\server\share - if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase)) - return path.Insert(2, UncExtendedPrefixToInsert); - - return ExtendedPathPrefix + path; - } - - /// - /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") - /// - internal static bool IsDevice(ReadOnlySpan path) - { - // If the path begins with any two separators is will be recognized and normalized and prepped with - // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. - return IsExtended(path) - || - ( - path.Length >= DevicePrefixLength - && IsDirectorySeparator(path[0]) - && IsDirectorySeparator(path[1]) - && (path[2] == '.' || path[2] == '?') - && IsDirectorySeparator(path[3]) - ); - } - - /// - /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\) - /// - internal static bool IsDeviceUNC(ReadOnlySpan path) - { - return path.Length >= UncExtendedPrefixLength - && IsDevice(path) - && IsDirectorySeparator(path[7]) - && path[4] == 'U' - && path[5] == 'N' - && path[6] == 'C'; - } - - /// - /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the - /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization - /// and path length checks. - /// - internal static bool IsExtended(ReadOnlySpan path) - { - // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. - // Skipping of normalization will *only* occur if back slashes ('\') are used. - return path.Length >= DevicePrefixLength - && path[0] == '\\' - && (path[1] == '\\' || path[1] == '?') - && path[2] == '?' - && path[3] == '\\'; - } - - /// - /// Gets the length of the root of the path (drive, share, etc.). - /// - internal static int GetRootLength(ReadOnlySpan path) - { - int pathLength = path.Length; - int i = 0; - - bool deviceSyntax = IsDevice(path); - bool deviceUnc = deviceSyntax && IsDeviceUNC(path); - - if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0])) - { - // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") - if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1]))) - { - // UNC (\\?\UNC\ or \\), scan past server\share - - // Start past the prefix ("\\" or "\\?\UNC\") - i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength; - - // Skip two separators at most - int n = 2; - while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) - i++; - } - else - { - // Current drive rooted (e.g. "\foo") - i = 1; - } - } - else if (deviceSyntax) - { - // Device path (e.g. "\\?\.", "\\.\") - // Skip any characters following the prefix that aren't a separator - i = DevicePrefixLength; - while (i < pathLength && !IsDirectorySeparator(path[i])) - i++; - - // If there is another separator take it, as long as we have had at least one - // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\") - if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i])) - i++; - } - else if (pathLength >= 2 - && path[1] == VolumeSeparatorChar - && IsValidDriveChar(path[0])) - { - // Valid drive specified path ("C:", "D:", etc.) - i = 2; - - // If the colon is followed by a directory separator, move past it (e.g "C:\") - if (pathLength > 2 && IsDirectorySeparator(path[2])) - i++; - } - - return i; - } - - /// - /// Returns true if the path specified is relative to the current drive or working directory. - /// Returns false if the path is fixed to a specific drive or UNC path. This method does no - /// validation of the path (URIs will be returned as relative as a result). - /// - /// - /// Handles paths that use the alternate directory separator. It is a frequent mistake to - /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. - /// "C:a" is drive relative- meaning that it will be resolved against the current directory - /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory - /// will not be used to modify the path). - /// - internal static bool IsPartiallyQualified(ReadOnlySpan path) - { - if (path.Length < 2) - { - // It isn't fixed, it must be relative. There is no way to specify a fixed - // path with one character (or less). - return true; - } - - if (IsDirectorySeparator(path[0])) - { - // There is no valid way to specify a relative path with two initial slashes or - // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ - return !(path[1] == '?' || IsDirectorySeparator(path[1])); - } - - // The only way to specify a fixed path that doesn't begin with two slashes - // is the drive, colon, slash format- i.e. C:\ - return !((path.Length >= 3) - && (path[1] == VolumeSeparatorChar) - && IsDirectorySeparator(path[2]) - // To match old behavior we'll check the drive character for validity as the path is technically - // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. - && IsValidDriveChar(path[0])); - } - - /// - /// True if the given character is a directory separator. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsDirectorySeparator(char c) - { - return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; - } - - /// - /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present. - /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip). - /// - /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false. - /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as - /// such can't be used here (and is overkill for our uses). - /// - /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments. - /// - /// - /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do - /// not need trimming of trailing whitespace here. - /// - /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization. - /// - /// For legacy .NET Framework behavior with ExpandShortPaths: - /// - It has no impact on GetPathRoot() so doesn't need consideration. - /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share). - /// - /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was - /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you - /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by - /// this undocumented behavior. - /// - /// We won't match this old behavior because: - /// - /// 1. It was undocumented - /// 2. It was costly (extremely so if it actually contained '~') - /// 3. Doesn't play nice with string logic - /// 4. Isn't a cross-plat friendly concept/behavior - /// - [return: NotNullIfNotNull("path")] - internal static string? NormalizeDirectorySeparators(string? path) - { - if (string.IsNullOrEmpty(path)) - return path; - - char current; - - // Make a pass to see if we need to normalize so we can potentially skip allocating - bool normalized = true; - - for (int i = 0; i < path.Length; i++) - { - current = path[i]; - if (IsDirectorySeparator(current) - && (current != DirectorySeparatorChar - // Check for sequential separators past the first position (we need to keep initial two for UNC/extended) - || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])))) - { - normalized = false; - break; - } - } - - if (normalized) - return path; - - var builder = new ValueStringBuilder(stackalloc char[MaxShortPath]); - - int start = 0; - if (IsDirectorySeparator(path[start])) - { - start++; - builder.Append(DirectorySeparatorChar); - } - - for (int i = start; i < path.Length; i++) - { - current = path[i]; - - // If we have a separator - if (IsDirectorySeparator(current)) - { - // If the next is a separator, skip adding this - if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) - { - continue; - } - - // Ensure it is the primary separator - current = DirectorySeparatorChar; - } - - builder.Append(current); - } - - return builder.ToString(); - } - - /// - /// Returns true if the path is effectively empty for the current OS. - /// For unix, this is empty or null. For Windows, this is empty, null, or - /// just spaces ((char)32). - /// - internal static bool IsEffectivelyEmpty(ReadOnlySpan path) - { - if (path.IsEmpty) - return true; - - foreach (char c in path) - { - if (c != ' ') - return false; - } - return true; - } - } -}