From f1fee6d7b4d06b17d872fdf587a4f87ca69827c2 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 27 Feb 2018 18:53:54 -0800 Subject: [PATCH] Add Path.Join() methods. (#16561) * Add Path.Join() methods. See #25536. * Address feedback and fix a couple issues now that I've got tests running correctly. * Update per final API approval * Fix Unix, remove redundant helper. * Merge and tweak join methods in GetFullPath(string, string) * Tweak again --- src/mscorlib/shared/System/IO/Path.Unix.cs | 2 +- src/mscorlib/shared/System/IO/Path.Windows.cs | 24 ++-- src/mscorlib/shared/System/IO/Path.cs | 186 ++++++++++++++++---------- src/mscorlib/shared/System/IO/PathInternal.cs | 8 +- 4 files changed, 136 insertions(+), 84 deletions(-) diff --git a/src/mscorlib/shared/System/IO/Path.Unix.cs b/src/mscorlib/shared/System/IO/Path.Unix.cs index 1294930..fd24cc8 100644 --- a/src/mscorlib/shared/System/IO/Path.Unix.cs +++ b/src/mscorlib/shared/System/IO/Path.Unix.cs @@ -61,7 +61,7 @@ namespace System.IO 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/mscorlib/shared/System/IO/Path.Windows.cs b/src/mscorlib/shared/System/IO/Path.Windows.cs index c92211f..b921db9 100644 --- a/src/mscorlib/shared/System/IO/Path.Windows.cs +++ b/src/mscorlib/shared/System/IO/Path.Windows.cs @@ -80,26 +80,30 @@ namespace System.IO // 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 @@ namespace System.IO // "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 @@ -216,19 +220,11 @@ namespace System.IO } /// - /// 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/mscorlib/shared/System/IO/Path.cs b/src/mscorlib/shared/System/IO/Path.cs index 586ddf3..41ae1cd 100644 --- a/src/mscorlib/shared/System/IO/Path.cs +++ b/src/mscorlib/shared/System/IO/Path.cs @@ -262,7 +262,6 @@ namespace System.IO 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 @@ namespace System.IO 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 @@ namespace System.IO 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 @@ namespace System.IO 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 @@ namespace System.IO 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 @@ namespace System.IO if (IsPathRooted(second)) return new string(second); - return CombineNoChecksInternal(first, second); + return JoinInternal(first, second); } - private static string CombineNoChecks(string first, string second) + private static string CombineInternal(string first, string second) { if (string.IsNullOrEmpty(first)) return second; @@ -414,86 +504,48 @@ namespace System.IO if (IsPathRooted(second.AsSpan())) return second; - return CombineNoChecksInternal(first, second); - } - - private static string CombineNoChecks(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 @@ namespace System.IO } } - 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 @@ namespace System.IO } } - 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/mscorlib/shared/System/IO/PathInternal.cs b/src/mscorlib/shared/System/IO/PathInternal.cs index 3eac1e7..f2f350d 100644 --- a/src/mscorlib/shared/System/IO/PathInternal.cs +++ b/src/mscorlib/shared/System/IO/PathInternal.cs @@ -10,8 +10,12 @@ namespace System.IO /// /// 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. -- 2.7.4