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 @@
-
+truetrue
@@ -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;
- }
- }
-}