Implementation of GetFullPath(string path, string basePath)#15579
Implementation of GetFullPath(string path, string basePath)#15579Anipik merged 25 commits intodotnet:masterfrom Anipik:FullPath
Conversation
| throw new ArgumentNullException(nameof(basePath)); | ||
|
|
||
| if (!IsPathFullyQualified(basePath)) | ||
| throw new ArgumentException(); |
|
|
||
| public static string GetFullPath(string path, string basePath) | ||
| { | ||
|
|
| /// Try to remove relative segments from the given path (without combining with a root). | ||
| /// </summary> | ||
| /// <param name="skip">Skip the specified number of characters before evaluating.</param> | ||
| private static string RemoveRelativeSegments(string path, int skip = 0) |
There was a problem hiding this comment.
This looks like a exact copy of a method from the Unix-specific. You should move it to Path.cs that is shared between Windows and Unix instead.
| if (basePath == null) | ||
| throw new ArgumentNullException(nameof(basePath)); | ||
|
|
||
| if (!IsPathFullyQualified(basePath)) |
| { | ||
| return RemoveRelativeSegments(CombineNoChecks(GetPathRoot(basePath), path.Substring(1))); | ||
| } | ||
| else if(length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar && !PathInternal.IsDirectorySeparator(path[2])) |
There was a problem hiding this comment.
Spacing also length check is off by one
There was a problem hiding this comment.
You must be missing a test case of length 2?
|
@dotnet-bot test this please |
|
I"ll let @pjanotti review this one |
| } | ||
|
|
||
| // Skip this character and the next if it's referring to the current directory, | ||
| // e.g. "parent/./child" =? "parent/child" |
|
@dotnet-bot test this please |
|
@pjanotti can you please review this PR ? |
| if (IsPathFullyQualified(path)) | ||
| return GetFullPath(path); | ||
|
|
||
| if (basePath == null) |
There was a problem hiding this comment.
Any specific reason to not check for basePath == null prior to the statement above? In general I would prefer a more predictable API, i.e.: always throwing if basePath is null, instead of depending on the context.
There was a problem hiding this comment.
The only reason I did that was that we were not using basePath if path is fullyQualified. should I reverse them ?
There was a problem hiding this comment.
My personal preference is to reverse them and have a consistent behavior regarding the value of basePath not that it is ignored if the path is fully qualified. That said I may be in the minority, @danmosemsft and @jkotas any preference in this regard?
There was a problem hiding this comment.
have a consistent behavior regarding the value of
basePathnot that it is ignored if the path is fully qualified
I agree.
| StringBuilderCache.Release(sb); | ||
| return path; | ||
| } | ||
| return GetFullPath(CombineNoChecks(basePath, path)); |
There was a problem hiding this comment.
CombineNoChecks is going to cause one allocation, if there are relative segments that will trigger more allocations. One possible way to avoid that is to create a version of RemoveRelativeSegments that takes multiple paths at once so the call to CombineNoChecks can be avoided.
That said perhaps it is better to address it in a separated PR.
|
|
||
| sb.Append(c); | ||
| } | ||
| if (!IsPathFullyQualified(basePath)) |
There was a problem hiding this comment.
@Anipik do this check before the IsPathFullyQualified(path) call this way we are ensuring consistent behavior regarding this parameter.
| if (IsPathFullyQualified(path)) | ||
| return GetFullPath(path); | ||
|
|
||
| if (!IsPathFullyQualified(basePath)) |
There was a problem hiding this comment.
Move to be before IsPathFullyQualified(path)
|
It looks good to me. |
|
|
||
| int length = path.Length; | ||
|
|
||
| if (PathInternal.IsDevice(basePath)) |
There was a problem hiding this comment.
if [](start = 12, length = 2)
You need to check the inputs for embedded nulls at this point and throw as above. We don't ever want to give back "c:\Foo\\0bar".
| } | ||
| else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar) | ||
| { | ||
| if (length == 2 || !PathInternal.IsDirectorySeparator(path[2])) |
There was a problem hiding this comment.
This line should just be an assert, it should never not be true.
There was a problem hiding this comment.
what if basepath = "c:\foo" and path = "d:\bar\tmp.." Wont then we will require this pathway ?
There was a problem hiding this comment.
It would be fully qualified and shouldn't have gotten past the earlier check. @"d:bar\tmp" would get through, and that is our special case (drive relative).
|
|
||
| if (PathInternal.IsDevice(basePath)) | ||
| { | ||
| return RemoveRelativeSegments(CombineNoChecks(basePath, path), PathInternal.GetRootLength(basePath)); |
There was a problem hiding this comment.
Add some comment lines to your cases- this stuff is hard to follow without them.
| { | ||
| if (length == 2 || !PathInternal.IsDirectorySeparator(path[2])) | ||
| { | ||
| if (GetPathRoot(path) == GetPathRoot(basePath)) |
There was a problem hiding this comment.
if (GetPathRoot(path) == GetPathRoot(basePath)) [](start = 20, length = 47)
This should use the span overloads to avoid the allocations.
| { | ||
| if (GetPathRoot(path) == GetPathRoot(basePath)) | ||
| { | ||
| return RemoveRelativeSegments(CombineNoChecks(basePath, path.Substring(2)), basePath.Length); |
There was a problem hiding this comment.
CombineNoChecks [](start = 54, length = 15)
We should get rid of the extra allocations in our RemoveRelativeSegments impl (in all places). Use Span to prepare all of the inputs.
There was a problem hiding this comment.
There was a problem hiding this comment.
CombineNoChecks dont have a span implementation
There was a problem hiding this comment.
should I remove the internal string overload for combineNoChecks ?
There was a problem hiding this comment.
The existing ones in Path can be replaced where the implementation is already done. You can create another issue to deal with the others (such as the array version). (And feel free to take the issue if you want.)
|
@JeremyKuhne can you take another look. I have added the span overloads but it wont be working until the other span PR dont get merged. Can you also review the tests PR also |
| throw new ArgumentException(SR.Arg_BasePathNotFullyQualified); | ||
|
|
||
| if (basePath.Contains('\0')) | ||
| throw new ArgumentException(SR.Arg_BasePathNotFullyQualified); |
There was a problem hiding this comment.
Should be InvalidPathChars || it with the other
| } | ||
|
|
||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] |
There was a problem hiding this comment.
I'm not really sure how the JIT will handle the inlining here. I'm a little worried that we end up with a ton of copies. I don't think they're necessary, I'd remove them.
| public static string GetFullPath(string path, string basePath) | ||
| { | ||
| bool flippedSeparator = false; | ||
| if (basePath == null) |
There was a problem hiding this comment.
Sorry, missed that you aren't checking path for null here- should check that as well. Check it before basePath.
There was a problem hiding this comment.
Can you review the tests for both the PR ?
|
@dotnet-bot test this please |
|
@Anipik you'll want to make sure you add tests as needed to cover Jeremy's feedback above. |
| { | ||
| // No matching root, root to specified drive | ||
| // "D:Foo" and "C:\Bar" => "D:Foo" | ||
| // "D:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar" |
There was a problem hiding this comment.
Whoops. I wrote the comments wrong. Should be => "D:\Foo" and => "\\?\D:\Foo"
| if (path1.Length == 0) | ||
| return path2; | ||
| if (second.Length == 0) | ||
| return new string(first); |
There was a problem hiding this comment.
You should create a follow up issue to try and get rid of the new string's that you introduced in these helpers. Better to deal with it separately as they should be corner cases.
There was a problem hiding this comment.
okay I will do that . should I open to change the implementation for the NormalizeDirectorypath to not use stringbuilder ?
There was a problem hiding this comment.
Having issues to track pending work is never a bad thing. :)
JeremyKuhne
left a comment
There was a problem hiding this comment.
As stated, we should create a follow up issue to look for opportunities to remove more of the potential allocations.
This API is supposed to mimick giving you the same results you would get after setting the current directory and then calling GetFullPath(). You can create test data that you run in proc with the new API and then remote after setting the current directory and calling GetFullPath. I wouldn't block on getting those written, but I would definitely write them. Note that device path tests can't be tested via setting the current working directory. That should be the same set of local test data, sticking \?\ on the base path.
| path.Slice(0, path.Length - 1) : | ||
| path; | ||
|
|
||
| internal static bool IsUnc(ReadOnlySpan<char> path) => |
There was a problem hiding this comment.
UNCs are in three formats. \\Server\Share, \\?\UNC\Server\Share, or \\.\UNC\Server\Share. Anything that begins with \\ and is not followed by . or ? is a UNC. If it is followed by . or ? it needs to have at least \UNC\ to be a UNC. Technically a UNC also requires at least two segments (for the server and share) past the prefix to be valid but we don't care about that here.
| /// <summary>Gets whether the system is case-sensitive.</summary> | ||
| internal static bool IsCaseSensitive { get { return false; } } | ||
|
|
||
| internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path) |
There was a problem hiding this comment.
Should add a comment as to what this returns.
| if (!PathInternal.IsDevice(path)) | ||
| return TrimEndingDirectorySeparator(root); // e.g. "C:" | ||
|
|
||
| if (IsUnc(path)) |
There was a problem hiding this comment.
Need if (IsUnc(path)) then a check for IsDevicePath() to handle both UNC cases.
|
|
||
| // 3 cases: UNC ("\\server\share"), Device ("\\?\C:\"), or Dos ("C:\") | ||
| ReadOnlySpan<char> root = GetPathRoot(path); | ||
| if (!PathInternal.IsDevice(path)) |
There was a problem hiding this comment.
You want server\share out of any type of UNC, in device syntax or not. This would return \\server\share.
There was a problem hiding this comment.
You could, in theory, pass back \\server\share for all UNC types. Doing so, however, would require allocating in the device unc format. As directory separators aren't valid inside a name, it makes trimming it on both ends workable.
| /// </summary> | ||
| internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path) | ||
| { | ||
| if (!IsPathFullyQualified(path)) |
There was a problem hiding this comment.
@JeremyKuhne Can you explain why we are doing this check ?
There was a problem hiding this comment.
Volume name has no meaning if you don't know where your path is. That said, there is a corner case that I forgot about, and that is drive relative. You know the volume definitively, even if it isn't qualified.
If the path starts with just \ or doesn't have a drive letter with : results from this would have no meaning. GetPathRoot() might be enough??
| /// </summary> | ||
| internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path) | ||
| { | ||
| if (!IsPathFullyQualified(path)) |
There was a problem hiding this comment.
As mentioned, this probably isn't correct for c:. Just calling GetPathRoot() and bailing if it is empty may give you the desired behavior.
There was a problem hiding this comment.
There was a problem hiding this comment.
No,
var root = GetPathRoot(path);
if (root.Length == 0)
return root;There was a problem hiding this comment.
Remove
if (!IsPathFullyQualified(path))
return getPathroot(path)| if (!isDevice && StringSpanHelpers.Equals(path.Slice(0, 2), @"\\") ) | ||
| return 2; | ||
| else if (isDevice && path.Length >= 8 | ||
| && (StringSpanHelpers.Equals(path.Slice(0, 8), PathInternal.UncExtendedPathPrefix) |
There was a problem hiding this comment.
As you know it is a device, you can just slice and check for UNC\ in one step.
| { | ||
| return TrimEndingDirectorySeparator(root.Slice(4)); // Cut from "\\?\C:\" to "C:" | ||
| } | ||
| { |
…oreclr#15579) GetFullPathAPI Overload Commit migrated from dotnet/coreclr@2ede044
Related Test PR- dotnet/corefx#25994
Issue:- https://github.com/dotnet/corefx/issues/25535