diff --git a/src/Common/src/CoreLib/System/IO/Path.Unix.cs b/src/Common/src/CoreLib/System/IO/Path.Unix.cs index 12949308c87f..fd24cc810c24 100644 --- a/src/Common/src/CoreLib/System/IO/Path.Unix.cs +++ b/src/Common/src/CoreLib/System/IO/Path.Unix.cs @@ -61,7 +61,7 @@ public static string GetFullPath(string path, string basePath) if (IsPathFullyQualified(path)) return GetFullPath(path); - return GetFullPath(CombineNoChecks(basePath, path)); + return GetFullPath(CombineInternal(basePath, path)); } private static string RemoveLongPathPrefix(string path) diff --git a/src/Common/src/CoreLib/System/IO/Path.Windows.cs b/src/Common/src/CoreLib/System/IO/Path.Windows.cs index c92211f73150..b921db9e615f 100644 --- a/src/Common/src/CoreLib/System/IO/Path.Windows.cs +++ b/src/Common/src/CoreLib/System/IO/Path.Windows.cs @@ -80,26 +80,30 @@ public static string GetFullPath(string path, string basePath) // Path is current drive rooted i.e. starts with \: // "\Foo" and "C:\Bar" => "C:\Foo" // "\Foo" and "\\?\C:\Bar" => "\\?\C:\Foo" - combinedPath = CombineNoChecks(GetPathRoot(basePath), path.AsSpan().Slice(1)); + combinedPath = Join(GetPathRoot(basePath.AsSpan()), path); } else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar) { // Drive relative paths Debug.Assert(length == 2 || !PathInternal.IsDirectorySeparator(path[2])); - if (StringSpanHelpers.Equals(GetVolumeName(path.AsSpan()), GetVolumeName(basePath.AsSpan()))) + if (StringSpanHelpers.Equals(GetVolumeName(path), GetVolumeName(basePath))) { // Matching root // "C:Foo" and "C:\Bar" => "C:\Bar\Foo" // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo" - combinedPath = CombineNoChecks(basePath, path.AsSpan().Slice(2)); + combinedPath = Join(basePath, path.AsSpan().Slice(2)); } else { // No matching root, root to specified drive // "D:Foo" and "C:\Bar" => "D:Foo" // "D:Foo" and "\\?\C:\Bar" => "\\?\D:\Foo" - combinedPath = PathInternal.IsDevice(basePath) ? CombineNoChecksInternal(basePath.AsSpan().Slice(0, 4), path.AsSpan().Slice(0, 2), @"\", path.AsSpan().Slice(2)) : path.Insert(2, "\\"); + combinedPath = !PathInternal.IsDevice(basePath) + ? path.Insert(2, @"\") + : length == 2 + ? JoinInternal(basePath.AsSpan().Slice(0, 4), path, @"\") + : JoinInternal(basePath.AsSpan().Slice(0, 4), path.AsSpan().Slice(0, 2), @"\", path.AsSpan().Slice(2)); } } else @@ -107,7 +111,7 @@ public static string GetFullPath(string path, string basePath) // "Simple" relative path // "Foo" and "C:\Bar" => "C:\Bar\Foo" // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo" - combinedPath = CombineNoChecks(basePath, path); + combinedPath = JoinInternal(basePath, path); } // Device paths are normalized by definition, so passing something of this format @@ -215,20 +219,12 @@ internal static ReadOnlySpan GetVolumeName(ReadOnlySpan path) return TrimEndingDirectorySeparator(root); // e.g. "C:" } - /// - /// Returns true if the path ends in a directory separator. - /// - internal static bool EndsInDirectorySeparator(ReadOnlySpan path) - { - return path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]); - } - /// /// Trims the ending directory separator if present. /// /// internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => - EndsInDirectorySeparator(path) ? + PathInternal.EndsInDirectorySeparator(path) ? path.Slice(0, path.Length - 1) : path; diff --git a/src/Common/src/CoreLib/System/IO/Path.cs b/src/Common/src/CoreLib/System/IO/Path.cs index 586ddf3a6998..41ae1cd0bef0 100644 --- a/src/Common/src/CoreLib/System/IO/Path.cs +++ b/src/Common/src/CoreLib/System/IO/Path.cs @@ -262,7 +262,6 @@ public static bool IsPathFullyQualified(ReadOnlySpan path) return !PathInternal.IsPartiallyQualified(path); } - /// /// Tests if a path's file name includes a file extension. A trailing period /// is not considered an extension. @@ -296,7 +295,7 @@ public static string Combine(string path1, string path2) if (path1 == null || path2 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2)); - return CombineNoChecks(path1, path2); + return CombineInternal(path1, path2); } public static string Combine(string path1, string path2, string path3) @@ -304,7 +303,7 @@ public static string Combine(string path1, string path2, string path3) if (path1 == null || path2 == null || path3 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3)); - return CombineNoChecks(path1, path2, path3); + return CombineInternal(path1, path2, path3); } public static string Combine(string path1, string path2, string path3, string path4) @@ -312,7 +311,7 @@ public static string Combine(string path1, string path2, string path3, string pa if (path1 == null || path2 == null || path3 == null || path4 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4)); - return CombineNoChecks(path1, path2, path3, path4); + return CombineInternal(path1, path2, path3, path4); } public static string Combine(params string[] paths) @@ -383,11 +382,102 @@ public static string Combine(params string[] paths) return StringBuilderCache.GetStringAndRelease(finalPath); } - /// - /// Combines two paths. Does no validation of paths, only concatenates the paths - /// and places a directory separator between them if needed. - /// - private static string CombineNoChecks(ReadOnlySpan first, ReadOnlySpan second) + // Unlike Combine(), Join() methods do not consider rooting. They simply combine paths, ensuring that there + // is a directory separator between them. + + public static string Join(ReadOnlySpan path1, ReadOnlySpan path2) + { + if (path1.Length == 0) + return new string(path2); + if (path2.Length == 0) + return new string(path1); + + return JoinInternal(path1, path2); + } + + public static string Join(ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3) + { + if (path1.Length == 0) + return Join(path2, path3); + + if (path2.Length == 0) + return Join(path1, path3); + + if (path3.Length == 0) + return Join(path1, path2); + + return JoinInternal(path1, path2, path3); + } + + public static bool TryJoin(ReadOnlySpan path1, ReadOnlySpan path2, Span destination, out int charsWritten) + { + charsWritten = 0; + if (path1.Length == 0 && path2.Length == 0) + return true; + + if (path1.Length == 0 || path2.Length == 0) + { + ref ReadOnlySpan pathToUse = ref path1.Length == 0 ? ref path2 : ref path1; + if (destination.Length < pathToUse.Length) + { + return false; + } + + pathToUse.CopyTo(destination); + charsWritten = pathToUse.Length; + return true; + } + + bool needsSeparator = !(PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2)); + int charsNeeded = path1.Length + path2.Length + (needsSeparator ? 1 : 0); + if (destination.Length < charsNeeded) + return false; + + path1.CopyTo(destination); + if (needsSeparator) + destination[path1.Length] = DirectorySeparatorChar; + + path2.CopyTo(destination.Slice(path1.Length + (needsSeparator ? 1 : 0))); + + charsWritten = charsNeeded; + return true; + } + + public static bool TryJoin(ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3, Span destination, out int charsWritten) + { + charsWritten = 0; + if (path1.Length == 0 && path2.Length == 0 && path3.Length == 0) + return true; + + if (path1.Length == 0) + return TryJoin(path2, path3, destination, out charsWritten); + if (path2.Length == 0) + return TryJoin(path1, path3, destination, out charsWritten); + if (path3.Length == 0) + return TryJoin(path1, path2, destination, out charsWritten); + + int neededSeparators = PathInternal.EndsInDirectorySeparator(path1) || PathInternal.StartsWithDirectorySeparator(path2) ? 0 : 1; + bool needsSecondSeparator = !(PathInternal.EndsInDirectorySeparator(path2) || PathInternal.StartsWithDirectorySeparator(path3)); + if (needsSecondSeparator) + neededSeparators++; + + int charsNeeded = path1.Length + path2.Length + path3.Length + neededSeparators; + if (destination.Length < charsNeeded) + return false; + + bool result = TryJoin(path1, path2, destination, out charsWritten); + Debug.Assert(result, "should never fail joining first two paths"); + + if (needsSecondSeparator) + destination[charsWritten++] = DirectorySeparatorChar; + + path3.CopyTo(destination.Slice(charsWritten)); + charsWritten += path3.Length; + + return true; + } + + private static string CombineInternal(ReadOnlySpan first, ReadOnlySpan second) { if (first.Length == 0) return second.Length == 0 @@ -400,10 +490,10 @@ private static string CombineNoChecks(ReadOnlySpan first, ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third) - { - if (first.Length == 0) - return CombineNoChecks(second, third); - if (second.Length == 0) - return CombineNoChecks(first, third); - if (third.Length == 0) - return CombineNoChecks(first, second); - - if (IsPathRooted(third)) - return new string(third); - if (IsPathRooted(second)) - return CombineNoChecks(second, third); - - return CombineNoChecksInternal(first, second, third); + return JoinInternal(first, second); } - private static string CombineNoChecks(string first, string second, string third) + private static string CombineInternal(string first, string second, string third) { if (string.IsNullOrEmpty(first)) - return CombineNoChecks(second, third); + return CombineInternal(second, third); if (string.IsNullOrEmpty(second)) - return CombineNoChecks(first, third); + return CombineInternal(first, third); if (string.IsNullOrEmpty(third)) - return CombineNoChecks(first, second); + return CombineInternal(first, second); if (IsPathRooted(third.AsSpan())) return third; if (IsPathRooted(second.AsSpan())) - return CombineNoChecks(second, third); - - return CombineNoChecksInternal(first, second, third); - } - - private static string CombineNoChecks(ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third, ReadOnlySpan fourth) - { - if (first.Length == 0) - return CombineNoChecks(second, third, fourth); - if (second.Length == 0) - return CombineNoChecks(first, third, fourth); - if (third.Length == 0) - return CombineNoChecks(first, second, fourth); - if (fourth.Length == 0) - return CombineNoChecks(first, second, third); - - if (IsPathRooted(fourth)) - return new string(fourth); - if (IsPathRooted(third)) - return CombineNoChecks(third, fourth); - if (IsPathRooted(second)) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third); - return CombineNoChecksInternal(first, second, third, fourth); + return JoinInternal(first, second, third); } - private static string CombineNoChecks(string first, string second, string third, string fourth) + private static string CombineInternal(string first, string second, string third, string fourth) { if (string.IsNullOrEmpty(first)) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third, fourth); if (string.IsNullOrEmpty(second)) - return CombineNoChecks(first, third, fourth); + return CombineInternal(first, third, fourth); if (string.IsNullOrEmpty(third)) - return CombineNoChecks(first, second, fourth); + return CombineInternal(first, second, fourth); if (string.IsNullOrEmpty(fourth)) - return CombineNoChecks(first, second, third); + return CombineInternal(first, second, third); if (IsPathRooted(fourth.AsSpan())) return fourth; if (IsPathRooted(third.AsSpan())) - return CombineNoChecks(third, fourth); + return CombineInternal(third, fourth); if (IsPathRooted(second.AsSpan())) - return CombineNoChecks(second, third, fourth); + return CombineInternal(second, third, fourth); - return CombineNoChecksInternal(first, second, third, fourth); + return JoinInternal(first, second, third, fourth); } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan first, ReadOnlySpan second) + private unsafe static string JoinInternal(ReadOnlySpan first, ReadOnlySpan second) { Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths"); @@ -515,7 +567,7 @@ private unsafe static string CombineNoChecksInternal(ReadOnlySpan first, R } } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third) + private unsafe static string JoinInternal(ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third) { Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths"); @@ -543,7 +595,7 @@ private unsafe static string CombineNoChecksInternal(ReadOnlySpan first, R } } - private unsafe static string CombineNoChecksInternal(ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third, ReadOnlySpan fourth) + private unsafe static string JoinInternal(ReadOnlySpan first, ReadOnlySpan second, ReadOnlySpan third, ReadOnlySpan fourth) { Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths"); diff --git a/src/Common/src/CoreLib/System/IO/PathInternal.cs b/src/Common/src/CoreLib/System/IO/PathInternal.cs index 3eac1e74e808..f2f350ddd015 100644 --- a/src/Common/src/CoreLib/System/IO/PathInternal.cs +++ b/src/Common/src/CoreLib/System/IO/PathInternal.cs @@ -10,8 +10,12 @@ internal static partial class PathInternal /// /// Returns true if the path ends in a directory separator. /// - internal static bool EndsInDirectorySeparator(string path) => - !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); + internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + + /// + /// Returns true if the path starts in a directory separator. + /// + internal static bool StartsWithDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && IsDirectorySeparator(path[0]); /// /// Get the common path length from the start of the string.