Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Add Path.Join() methods. (dotnet/coreclr#16561)
Browse files Browse the repository at this point in the history
* 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

Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com>
  • Loading branch information
JeremyKuhne authored and ahsonkhan committed Mar 1, 2018
1 parent 4d06437 commit 7c5d4bd
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/Common/src/CoreLib/System/IO/Path.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 10 additions & 14 deletions src/Common/src/CoreLib/System/IO/Path.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,34 +80,38 @@ 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
{
// "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
Expand Down Expand Up @@ -215,20 +219,12 @@ internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path)
return TrimEndingDirectorySeparator(root); // e.g. "C:"
}

/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path)
{
return path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]);
}

/// <summary>
/// Trims the ending directory separator if present.
/// </summary>
/// <param name="path"></param>
internal static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<char> path) =>
EndsInDirectorySeparator(path) ?
PathInternal.EndsInDirectorySeparator(path) ?
path.Slice(0, path.Length - 1) :
path;

Expand Down
186 changes: 119 additions & 67 deletions src/Common/src/CoreLib/System/IO/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ public static bool IsPathFullyQualified(ReadOnlySpan<char> path)
return !PathInternal.IsPartiallyQualified(path);
}


/// <summary>
/// Tests if a path's file name includes a file extension. A trailing period
/// is not considered an extension.
Expand Down Expand Up @@ -296,23 +295,23 @@ 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)
{
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)
{
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)
Expand Down Expand Up @@ -383,11 +382,102 @@ public static string Combine(params string[] paths)
return StringBuilderCache.GetStringAndRelease(finalPath);
}

/// <summary>
/// Combines two paths. Does no validation of paths, only concatenates the paths
/// and places a directory separator between them if needed.
/// </summary>
private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> 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<char> path1, ReadOnlySpan<char> 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<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> 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<char> path1, ReadOnlySpan<char> path2, Span<char> destination, out int charsWritten)
{
charsWritten = 0;
if (path1.Length == 0 && path2.Length == 0)
return true;

if (path1.Length == 0 || path2.Length == 0)
{
ref ReadOnlySpan<char> 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<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> 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<char> first, ReadOnlySpan<char> second)
{
if (first.Length == 0)
return second.Length == 0
Expand All @@ -400,10 +490,10 @@ private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<cha
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;
Expand All @@ -414,86 +504,48 @@ private static string CombineNoChecks(string first, string second)
if (IsPathRooted(second.AsSpan()))
return second;

return CombineNoChecksInternal(first, second);
}

private static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> 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<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> 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<char> first, ReadOnlySpan<char> second)
private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");

Expand All @@ -515,7 +567,7 @@ private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, R
}
}

private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
{
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");

Expand Down Expand Up @@ -543,7 +595,7 @@ private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, R
}
}

private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
private unsafe static string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
{
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");

Expand Down
8 changes: 6 additions & 2 deletions src/Common/src/CoreLib/System/IO/PathInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ internal static partial class PathInternal
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(string path) =>
!string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);

/// <summary>
/// Returns true if the path starts in a directory separator.
/// </summary>
internal static bool StartsWithDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[0]);

/// <summary>
/// Get the common path length from the start of the string.
Expand Down

0 comments on commit 7c5d4bd

Please sign in to comment.