Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicated path.Contains('\0') check #54993

Closed
iSazonov opened this issue Jul 1, 2021 · 9 comments · Fixed by #55373 or #56568
Closed

Duplicated path.Contains('\0') check #54993

iSazonov opened this issue Jul 1, 2021 · 9 comments · Fixed by #55373 or #56568
Assignees
Labels
area-System.IO help wanted [up-for-grabs] Good issue for external contributors tenet-performance Performance related issue
Milestone

Comments

@iSazonov
Copy link
Contributor

iSazonov commented Jul 1, 2021

Description

public static string GetFullPath(string path, string basePath)

The method calls public static string GetFullPath(string path) where path.Contains('\0') is called again.

Discovered in #54253

Perhaps there are other places where this expensive check is duplicated but I did not find after a quick search.

@iSazonov iSazonov added the tenet-performance Performance related issue label Jul 1, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Jul 1, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost
Copy link

ghost commented Jul 1, 2021

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

public static string GetFullPath(string path, string basePath)

The method calls public static string GetFullPath(string path) where path.Contains('\0') is called again.

Discovered in #54253

Perhaps there are other places where this expensive check is duplicated but I did not find after a quick search.

Author: iSazonov
Assignees: -
Labels:

area-System.IO, tenet-performance, untriaged

Milestone: -

@jozkee jozkee added this to the Future milestone Jul 2, 2021
@jozkee jozkee removed the untriaged New issue has not been triaged by the area owner label Jul 2, 2021
@adamsitnik adamsitnik added the help wanted [up-for-grabs] Good issue for external contributors label Jul 2, 2021
@SkiFoD
Copy link
Contributor

SkiFoD commented Jul 6, 2021

Hey everyone. I did a quick research and came up with a very straightforward solution like refactoring

public static string GetFullPath(string path, string basePath)
which is to replace using
public static string GetFullPath(string path)

with some parts of the method.
Before doing that, I'd like to ask you if it is ok. I'm concerned because it would lead to too much of copypaste code.

There is another option like to create a private method that would be based on GetFullPath(string path) and use it in

public static string GetFullPath(string path, string basePath)

@adamsitnik @jozkee @jeffschwMSFT What is your opinion on that?

@steveberdy
Copy link
Contributor

steveberdy commented Jul 8, 2021

@iSazonov @SkiFoD I did some simple refactoring, no research needed. I didn't unit test it since it was just refactoring. If it's okay, I'll submit a PR.

16270cd

// Expands the given path to a fully qualified path.
public static string GetFullPath(string path)
{
    if (path == null)
        throw new ArgumentNullException(nameof(path));

    // If the path would normalize to string empty, we'll consider it empty
    if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
        throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));

    // Embedded null characters are the only invalid character case we trully care about.
    // This is because the nulls will signal the end of the string to Win32 and therefore have
    // unpredictable results.
    if (path.Contains('\0'))
        throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path));
    
    return GetFullQualifiedPath(path);
}

public static string GetFullPath(string path, string basePath)
{
    if (path == null)
        throw new ArgumentNullException(nameof(path));

    if (basePath == null)
        throw new ArgumentNullException(nameof(basePath));

    if (!IsPathFullyQualified(basePath))
        throw new ArgumentException(SR.Arg_BasePathNotFullyQualified, nameof(basePath));

    if (basePath.Contains('\0') || path.Contains('\0'))
        throw new ArgumentException(SR.Argument_InvalidPathChars);

    if (IsPathFullyQualified(path))
        return GetFullQualifiedPath(path);

    if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
        return basePath;

    int length = path.Length;
    string combinedPath;
    if (length >= 1 && PathInternal.IsDirectorySeparator(path[0]))
    {
        // Path is current drive rooted i.e. starts with \:
        // "\Foo" and "C:\Bar" => "C:\Foo"
        // "\Foo" and "\\?\C:\Bar" => "\\?\C:\Foo"
        combinedPath = Join(GetPathRoot(basePath.AsSpan()), path.AsSpan(1)); // Cut the separator to ensure we don't end up with two separators when joining with the root.
    }
    else if (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar)
    {
        // Drive relative paths
        Debug.Assert(length == 2 || !PathInternal.IsDirectorySeparator(path[2]));

        if (GetVolumeName(path.AsSpan()).EqualsOrdinal(GetVolumeName(basePath.AsSpan())))
        {
            // Matching root
            // "C:Foo" and "C:\Bar" => "C:\Bar\Foo"
            // "C:Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
            combinedPath = Join(basePath.AsSpan(), path.AsSpan(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.AsSpan())
                ? path.Insert(2, @"\")
                : length == 2
                    ? JoinInternal(basePath.AsSpan(0, 4), path.AsSpan(), @"\".AsSpan())
                    : JoinInternal(basePath.AsSpan(0, 4), path.AsSpan(0, 2), @"\".AsSpan(), path.AsSpan(2));
        }
    }
    else
    {
        // "Simple" relative path
        // "Foo" and "C:\Bar" => "C:\Bar\Foo"
        // "Foo" and "\\?\C:\Bar" => "\\?\C:\Bar\Foo"
        combinedPath = JoinInternal(basePath.AsSpan(), path.AsSpan());
    }

    // Device paths are normalized by definition, so passing something of this format (i.e. \\?\C:\.\tmp, \\.\C:\foo)
    // to Windows APIs won't do anything by design. Additionally, GetFullPathName() in Windows doesn't root
    // them properly. As such we need to manually remove segments and not use GetFullPath().

    return PathInternal.IsDevice(combinedPath.AsSpan())
        ? PathInternal.RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath.AsSpan()))
        : GetFullQualifiedPath(combinedPath);
}

private static string GetFullQualifiedPath(string path)
{
    if (PathInternal.IsExtended(path.AsSpan()))
    {
        // \\?\ paths are considered normalized by definition. Windows doesn't normalize \\?\
        // paths and neither should we. Even if we wanted to GetFullPathName does not work
        // properly with device paths. If one wants to pass a \\?\ path through normalization
        // one can chop off the prefix, pass it to GetFullPath and add it again.
        return path;
    }

    return PathHelper.Normalize(path);
}

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 8, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 24, 2021
@iSazonov
Copy link
Contributor Author

@jeffhandley It seems there are a lot of places where the extra check presents.

@jeffhandley
Copy link
Member

@iSazonov Do you suggest that we reopen this issue for follow-up to find and eliminate the other occurrences? Did you already identify some other places that could be noted?

@steveberdy
Copy link
Contributor

I am aware of some of the other locations of this check. I could submit a PR.

@jeffhandley
Copy link
Member

Great; thanks. I'll reopen this issue then. It would be helpful to capture the identified locations here and we can turn them into a task list in the issue description. Thank you both!

@jeffhandley jeffhandley reopened this Jul 26, 2021
@steveberdy
Copy link
Contributor

@jeffhandley could you assign this to me so I can track it in my todos? Thanks

@ghost ghost added in-pr There is an active PR which will close this issue when it is merged and removed in-pr There is an active PR which will close this issue when it is merged labels Jul 29, 2021
@ghost ghost added in-pr There is an active PR which will close this issue when it is merged and removed in-pr There is an active PR which will close this issue when it is merged labels Aug 5, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Aug 6, 2021
@adamsitnik adamsitnik modified the milestones: Future, 6.0.0 Aug 11, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Sep 10, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.IO help wanted [up-for-grabs] Good issue for external contributors tenet-performance Performance related issue
Projects
None yet
7 participants