diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.LinuxPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.LinuxPath.cs index 82901959..6c8f5da1 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.LinuxPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.LinuxPath.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Linq; +using System.Text; namespace Testably.Abstractions.Testing.Helpers; @@ -75,33 +76,23 @@ protected override bool IsEffectivelyEmpty(string 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) + bool isAlreadyNormalized = Enumerable + .Range(0, path.Length - 1) + .All(i => !IsDirectorySeparator(path[i]) || + !IsDirectorySeparator(path[i + 1])); + if (isAlreadyNormalized) { return path; } StringBuilder builder = new(path.Length); - for (int i = 0; i < path.Length; i++) + for (int j = 0; j < path.Length - 1; j++) { - char current = path[i]; + char current = path[j]; - // Skip if we have another separator following if (IsDirectorySeparator(current) - && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) + && IsDirectorySeparator(path[j + 1])) { continue; } diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs index 2efec637..11c4ef88 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs @@ -162,58 +162,29 @@ public ReadOnlySpan GetDirectoryName(ReadOnlySpan path) /// public string? GetDirectoryName(string? path) { - int GetDirectoryNameOffset(string p) + if (path == null || IsEffectivelyEmpty(path)) { - int rootLength = GetRootLength(p); - int end = p.Length; - if (end <= rootLength) - { - return -1; - } - - while (end > rootLength && !IsDirectorySeparator(p[--end])) - { - // Do nothing - } - - // Trim off any remaining separators (to deal with C:\foo\\bar) - while (end > rootLength && IsDirectorySeparator(p[end - 1])) - { - end--; - } - - return end; + return null; } - if (path == null || IsEffectivelyEmpty(path)) + int rootLength = GetRootLength(path); + if (path.Length <= rootLength) { return null; } - int end = GetDirectoryNameOffset(path); - if (end >= 0) + int end = path.Length; + while (end > rootLength && !IsDirectorySeparator(path[end - 1])) { - return NormalizeDirectorySeparators(path.Substring(0, end)); + end--; } - return null; - //if (path == null || IsEffectivelyEmpty(path)) - //{ - // return null; - //} - - //int rootLength = GetRootLength(path); - //for (int i = path.Length - 1; i >= 0; i--) - //{ - // char ch = path[i]; - - // if (IsDirectorySeparator(ch) && i > rootLength) - // { - // return path.Substring(0, i); - // } - //} + while (end > rootLength && IsDirectorySeparator(path[end - 1])) + { + end--; + } - //return null; + return NormalizeDirectorySeparators(path.Substring(0, end)); } #if FEATURE_SPAN diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.WindowsPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.WindowsPath.cs index bc9f5863..3f07c4d2 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.WindowsPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.WindowsPath.cs @@ -71,115 +71,105 @@ public override bool IsPathRooted(string? path) /// protected override int GetRootLength(string path) { - int pathLength = path.Length; - int i = 0; + bool IsDeviceUNC(string p) + => p.Length >= 8 + && IsDevice(p) + && IsDirectorySeparator(p[7]) + && p[4] == 'U' + && p[5] == 'N' + && p[6] == 'C'; + + bool IsDevice(string p) + => IsExtended(p) + || + ( + p.Length >= 4 + && IsDirectorySeparator(p[0]) + && IsDirectorySeparator(p[1]) + && (p[2] == '.' || p[2] == '?') + && IsDirectorySeparator(p[3]) + ); + + bool IsExtended(string p) + => p.Length >= 4 + && p[0] == '\\' + && (p[1] == '\\' || p[1] == '?') + && p[2] == '?' + && p[3] == '\\'; - bool deviceSyntax = IsDevice(path); - bool deviceUnc = deviceSyntax && IsDeviceUNC(path); + int pathLength = path.Length; - if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0])) + if (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; + bool deviceSyntax = IsDevice(path); + bool deviceUnc = deviceSyntax && IsDeviceUNC(path); - // Skip two separators at most - int n = 2; - while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) - { - i++; - } - } - else + if (deviceSyntax && !deviceUnc) { - // 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++; + return GetRootLengthWithDeviceSyntax(path); } - // 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])) + // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") + if (deviceUnc || (path.Length > 1 && IsDirectorySeparator(path[1]))) { - i++; + return GetRootLengthWithDeviceUncSyntax(path, deviceUnc); } + + // Current drive rooted (e.g. "\foo") + return 1; } - else if (pathLength >= 2 - && path[1] == ':' - && IsValidDriveChar(path[0])) - { - // Valid drive specified path ("C:", "D:", etc.) - i = 2; + if (pathLength >= 2 + && path[1] == ':' + && IsValidDriveChar(path[0])) + { // If the colon is followed by a directory separator, move past it (e.g "C:\") if (pathLength > 2 && IsDirectorySeparator(path[2])) { - i++; + return 3; } + + // Valid drive specified path ("C:", "D:", etc.) + return 2; } - return i; - } - private const int DevicePrefixLength = 4; - private const int UncExtendedPrefixLength = 8; - private const int UncPrefixLength = 2; - /// - /// 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. - /// - private static bool IsExtended(string 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] == '\\'; + return 0; } - /// - /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") - /// - private bool IsDevice(string path) + + private int GetRootLengthWithDeviceSyntax(string 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]) - ); + // Device path (e.g. "\\?\.", "\\.\") + // Skip any characters following the prefix that aren't a separator + int i = 4; + while (i < path.Length && !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 < path.Length && i > 4 && IsDirectorySeparator(path[i])) + { + i++; + } + + return i; } - /// - /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\) - /// - private bool IsDeviceUNC(string path) + private int GetRootLengthWithDeviceUncSyntax(string path, + bool deviceUnc) { - return path.Length >= UncExtendedPrefixLength - && IsDevice(path) - && IsDirectorySeparator(path[7]) - && path[4] == 'U' - && path[5] == 'N' - && path[6] == 'C'; + // Start past the prefix ("\\" or "\\?\UNC\") + int i = deviceUnc ? 8 : 2; + + // Skip two separators at most + int n = 2; + while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) + { + i++; + } + + return i; } /// @@ -201,36 +191,36 @@ protected override bool IsEffectivelyEmpty(string path) return path.All(c => c == ' '); } + /// + /// Returns true if the given character is a valid drive letter + /// + /// https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L72 + private static bool IsValidDriveChar(char value) + => (uint)((value | 0x20) - 'a') <= 'z' - 'a'; + /// /// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L318 /// [return: NotNullIfNotNull(nameof(path))] protected override string? NormalizeDirectorySeparators(string? path) { - if (string.IsNullOrEmpty(path)) + bool IsAlreadyNormalized() { - 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 != '\\' - // 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])))) + for (int i = 1; i < path.Length; i++) { - normalized = false; - break; + char current = path[i]; + if (IsDirectorySeparator(current) + && (current != DirectorySeparatorChar + || (i + 1 < path.Length && IsDirectorySeparator(path[i + 1])))) + { + return false; + } } + + return true; } - if (normalized) + if (string.IsNullOrEmpty(path) || IsAlreadyNormalized()) { return path; } @@ -241,24 +231,21 @@ protected override bool IsEffectivelyEmpty(string path) if (IsDirectorySeparator(path[start])) { start++; - builder.Append('\\'); + builder.Append(DirectorySeparatorChar); } for (int i = start; i < path.Length; i++) { - current = path[i]; + char 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 = '\\'; + current = DirectorySeparatorChar; } builder.Append(current); @@ -266,12 +253,5 @@ protected override bool IsEffectivelyEmpty(string path) return builder.ToString(); } - - /// - /// Returns true if the given character is a valid drive letter - /// - /// https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L72 - private static bool IsValidDriveChar(char value) - => (uint)((value | 0x20) - 'a') <= 'z' - 'a'; } }