Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Implementation of GetFullPath(string path, string basePath)#15579

Merged
Anipik merged 25 commits intodotnet:masterfrom
Anipik:FullPath
Feb 6, 2018
Merged

Implementation of GetFullPath(string path, string basePath)#15579
Anipik merged 25 commits intodotnet:masterfrom
Anipik:FullPath

Conversation

@Anipik
Copy link

@Anipik Anipik commented Dec 19, 2017

throw new ArgumentNullException(nameof(basePath));

if (!IsPathFullyQualified(basePath))
throw new ArgumentException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs error message.


public static string GetFullPath(string path, string basePath)
{

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra new line

/// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the string

{
return RemoveRelativeSegments(CombineNoChecks(GetPathRoot(basePath), path.Substring(1)));
}
else if(length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar && !PathInternal.IsDirectorySeparator(path[2]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing also length check is off by one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must be missing a test case of length 2?

@jkotas jkotas requested a review from pjanotti December 20, 2017 04:30
@Anipik
Copy link
Author

Anipik commented Dec 22, 2017

@dotnet-bot test this please

@danmoseley
Copy link
Member

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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo =? should be =>

@Anipik
Copy link
Author

Anipik commented Jan 2, 2018

@dotnet-bot test this please

@Anipik
Copy link
Author

Anipik commented Jan 3, 2018

@pjanotti can you please review this PR ?

Copy link

@pjanotti pjanotti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, minor thing will be the exceptions when basePath is null or not rooted, even if path is rooted.

If you decide to not do the suggestion related to RemoveRelativeSegments, please, open an issue to track it.

if (IsPathFullyQualified(path))
return GetFullPath(path);

if (basePath == null)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I did that was that we were not using basePath if path is fullyQualified. should I reverse them ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have a consistent behavior regarding the value of basePath not that it is ignored if the path is fully qualified

I agree.

StringBuilderCache.Release(sb);
return path;
}
return GetFullPath(CombineNoChecks(basePath, path));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to be before IsPathFullyQualified(path)

@pjanotti
Copy link

pjanotti commented Jan 9, 2018

It looks good to me.


int length = path.Length;

if (PathInternal.IsDevice(basePath))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should just be an assert, it should never not be true.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if basepath = "c:\foo" and path = "d:\bar\tmp.." Wont then we will require this pathway ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant CombineNoChecks() should take Span.


In reply to: 162213527 [](ancestors = 162213527)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CombineNoChecks dont have a span implementation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does in CoreFX. Need to port it and use it. :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I remove the internal string overload for combineNoChecks ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

@Anipik
Copy link
Author

Anipik commented Jan 19, 2018

@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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be InvalidPathChars || it with the other

}


[MethodImpl(MethodImplOptions.AggressiveInlining)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, missed that you aren't checking path for null here- should check that as well. Check it before basePath.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you review the tests for both the PR ?

@Anipik
Copy link
Author

Anipik commented Jan 31, 2018

@dotnet-bot test this please

@danmoseley
Copy link
Member

@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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay I will do that . should I open to change the implementation for the NormalizeDirectorypath to not use stringbuilder ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having issues to track pending work is never a bad thing. :)

Copy link
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a comment as to what this returns.

if (!PathInternal.IsDevice(path))
return TrimEndingDirectorySeparator(root); // e.g. "C:"

if (IsUnc(path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want server\share out of any type of UNC, in device syntax or not. This would return \\server\share.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Author

@Anipik Anipik Feb 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JeremyKuhne Can you explain why we are doing this check ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay understood

/// </summary>
internal static ReadOnlySpan<char> GetVolumeName(ReadOnlySpan<char> path)
{
if (!IsPathFullyQualified(path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, this probably isn't correct for c:. Just calling GetPathRoot() and bailing if it is empty may give you the desired behavior.

Copy link
Author

@Anipik Anipik Feb 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JeremyKuhne

if (!IsPathFullyQualified(path))
      return getPathroot(path)

do you mean like this ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No,

var root = GetPathRoot(path);
if (root.Length == 0)
    return root;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:"
}
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the block here?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants