在“长路径感知”环境中,对PathRelativePathTo的参数有限制

13
针对 Windows 10 上的 长路径感知 进程,我正在尝试了解在使用 Windows Shell 方法 PathRelativePathTo 时的参数限制。
在下面的示例中,我使用 C# 通过 pinvoke 调用该方法。请注意:
  • 所有示例都为 "from" 提供目录路径,为 "to" 提供文件路径(这些路径实际上不存在于磁盘上)
  • 我的观察结果是:
    • 在 "短" MAX_PATH 长度(260)以下的路径返回预期结果。
    • 一些超过 "短" MAX_PATH 的路径返回正确结果。
    • 一些超过 "短" MAX_PATH 的路径返回错误答案(哎呀!)
    • 一些更长的路径返回错误。但是,它没有固定的最大长度。

来源:

    class Program
    {
        static class Native
        {
            // https://www.pinvoke.net/default.aspx/shlwapi.pathrelativepathto
            // https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathrelativepathtoa
            [DllImport("shlwapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool PathRelativePathTo([Out] StringBuilder pszPath, [In] string pszFrom, [In] int dwAttrFrom, [In] string pszTo, [In] int dwAttrTo);
        }

        static void Main(string[] args)
        {
            string pszFrom, pszTo;
            int i = 0;

            // #1 At "short" max path (259)
            // Succeeds with right answer
            pszFrom = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD123456789";
            pszTo = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD123456789\abcdefghijklmnop.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);

            // #2 One over "short" max path
            // Succeeds with right answer
            pszFrom = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD1234567890";
            pszTo = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD1234567890\abcdefghijklmnop.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);

            // #3 Shortest path (by experiment) that returned the wrong answer
            pszFrom = @"c:\ABCDEFGHIJKLMNOPQRS\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD1234567890";
            pszTo = @"c:\ABCDEFGHIJKLMNOPQRS\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCD1234567890\b.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);

            // #4: Long path that errors out
            // Errors out
            pszFrom = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
            pszTo = @"c:\ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\b.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);

            // #5: Same as previous except one character removed from beginning of first folder
            // Succeeds, but wrong return result
            pszFrom = @"c:\BCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
            pszTo = @"c:\BCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\b.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);

            // #6: Same as previous except 3 characters added to filename. 
            // Succeeds, but wrong return result
            pszFrom = @"c:\BCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
            pszTo = @"c:\BCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\b123.txt";
            TestPathRelativePathTo(++i, pszFrom, pszTo);
        }

        static void TestPathRelativePathTo(int i, string pszFromDir, string pszToFile)
        {
            int maxResult = 10000;
            StringBuilder result = new StringBuilder(maxResult);
            Console.WriteLine($"#{i}: Calling PathRelativePathTo(...): pszFrom.Length: {pszFromDir.Length}; pszTo.Length {pszToFile.Length} ");
            bool bRet = Native.PathRelativePathTo(result, pszFromDir, (int)FileAttributes.Directory, pszToFile, (int)FileAttributes.Normal);
            if (!bRet)
            {
                // *Edit*: As pointed out in the comments, PathRelativePathTo does not set last error, so this part of the code is incorrect, it should really just print out that the method returned false.
                // https://blogs.msdn.microsoft.com/shawnfa/2004/09/10/formatmessage-shortcut-for-win32-error-codes/
                int currentError = Marshal.GetLastWin32Error();
                var errorMessage = new Win32Exception(currentError).Message;
                Console.WriteLine($"  Error: {errorMessage}");
            }
            else
            {
                Console.WriteLine($"  Result: {result}");
            }
        }
    }

输出:

#1: Calling PathRelativePathTo(...): pszFrom.Length: 238; pszTo.Length 259
  Result: .\abcdefghijklmnop.txt
#2: Calling PathRelativePathTo(...): pszFrom.Length: 239; pszTo.Length 260
  Result: .\abcdefghijklmnop.txt
#3: Calling PathRelativePathTo(...): pszFrom.Length: 259; pszTo.Length 265
  Result: ..\ABCD1234567890\b.txt
#4: Calling PathRelativePathTo(...): pszFrom.Length: 481; pszTo.Length 487
  Error: The system cannot find the file specified
#5: Calling PathRelativePathTo(...): pszFrom.Length: 480; pszTo.Length 486
  Result: .\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\b.txt
#6: Calling PathRelativePathTo(...): pszFrom.Length: 480; pszTo.Length 489
  Result: .\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\b123.txt

问题:

  • PathRelativePathTo 的预期行为是什么?
  • 它只能在“短”MAX_PATH限制下的路径上正常工作(其余行为未定义)吗?
  • 是否有其他我可以使用的.NET框架而不是它(注意:我看到.NET Core有Path.GetRelativePath,但我现在不能使用它)?

评论不是用于扩展讨论的; 此对话已被移至聊天室 - Samuel Liew
忘记 PathRelativePathTo 吧,它不适用于长路径。实际上,使用它是不安全的,因为你无法指定目标缓冲区的大小,文档只说它“必须至少有 MAX_PATH 个字符大小”。 - acelent
官方文档对MAX_PATH限制非常清晰。至于替代方案,很容易出错,您可以重用.NET Core源代码或将其用作起点:https://github.com/dotnet/corefx/blob/70150e6750c35c9a67803e9cddfda7b1db2ec487/src/Common/src/CoreLib/System/IO/Path.cs#L805 - Simon Mourier
你目前在使用什么?经典的.NET还是.NET Core,哪个版本? - Pavel Anikhouski
.NET框架。一旦我能够转移到.NET Core 3.0,我就可以使用我提到的内置方法了。 - Matt Smith
4个回答

4
看起来,PathRelativePathTo API只能安全地处理长度最长为MAX_LENGTH的路径。至少从Wine文档中可以看出,在Win32实现中该API存在问题。
在Win32版本的该函数中,存在一个错误,其中lpszTo字符串可能会被引用到字符串末尾1个字节之外。因此,随机垃圾可能会被写入输出路径,具体取决于字符串最后一个字节之后的内容。这个bug发生是因为PathCommonPrefix()的行为(请参阅该函数的说明),并且在Win32中似乎没有解决方法。在此实现中已经修复了这个bug,例如从“\”到“\”的相对路径在本实现中正确地确定为“.”。
从PathCommonPrefix文档中可以得知,2个公共前缀总是返回3。因此,返回的长度可能是无效的(即比其中一个或两个参数给定的字符串还长)。这种Win32行为已经在此实现中实现,不能更改(修复?)而不破坏其他SHLWAPI调用。在使用此函数时,要解决这个问题,请始终检查[common_prefix_len-1]处的字节是否不是NUL。如果是NUL,则将前缀减1。
这些信息以及假设shlwapi实现使用MAX_SIZE长度的缓冲区并且类似于Wine或ReactOS (https://doxygen.reactos.org/de/dff/dll_2win32_2shlwapi_2path_8c_source.html),似乎能够解释您在测试中看到的未定义行为。
至于.NET解决方案,我能想到的最简单的方法(可能不是最好的)是使用System.Uri。
Uri path1 = new Uri(@"c:\lvl1\lvl2\");
Uri path2 = new Uri(@"c:\lvl1\lvl3\file1.txt");
Uri diff = path1.MakeRelativeUri(path2);
// Uri will switch to forward slashes, so to fix that...
string relPath = 
Uri.UnescapeDataString(diff.OriginalString).Replace("/",@"\");

当然,你也可以基于 .NET Core 的源代码实现一些东西,比如 Path.GetRelativePath


3

.NET 4.6.2 解决方案

使用如此处所述的\\?\C:\Verrrrrrrrrrrry long path语法。

关于这个问题也有一篇很棒的博客文章

总的来说,我遇到的最大问题是通过网络共享文件夹。其余的都没问题。

旧版本的.NET

如果你使用较旧的.NET版本,你可以查看此Win32 API函数,你需要使用P/Invoke实现。

Windows API有许多函数也具有Unicode版本,以允许最长总路径长度为32,767个字符

还可以查看这个与您的问题非常相似的SO问题。
如何处理文件名超过259个字符的文件?


4
但这与PathRelativePathTo无关。 - RbMm
2
这怎么回答问题了? - jester
这就是Path所有功能背后的确切思想。 - Gilad
1
所有关于路径的功能都不会影响具体的PathRelativePathTo。这是一个纯粹的词法解析API,硬编码为260个字符限制。同时,即使是\\/之间的差别也会导致它失效。 - RbMm
甚至有一条评论说它不起作用:https://chat.stackoverflow.com/transcript/message/47826723#47826723 - user1781290

3
我决定使用 dotnet/corefx 的端口 Path.GetRelativePath 方法。下面的代码是从以下来源调整而来。请在代码中阅读我的注释,我列出了任何调整或解决方法: 我调整代码的目标是:
  • 尽可能少地进行修改(在代码注释中注明任何所做的修改)
  • 保持类结构与原始源代码相同
  • 仅包括实现方法 GetRelativePath 所需的方法/属性
代码:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using static System.IO.Path;

static class PathExtension
{
    // Port of .net 3.0 Path.GetRelativePath (Windows version)
    // https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=netcore-3.0
    // 
    // Adapted from:
    // https://github.com/dotnet/corefx/blob/b123ba4b9107c73cbc02010dc1ee78eb8ffccb93/src/Common/src/CoreLib/System/IO/Path.cs
    // https://github.com/dotnet/corefx/blob/4a7075f188b5777ccb519f2af9b8a284f4383357/src/Common/src/CoreLib/System/IO/Path.Windows.cs
    //
    // Notes:
    // * I didn't have access to ReadOnlySpan<T> nor .AsSpan(), so I removed them.  I just used regular string instead.
    // * I hard coded some resource strings (from exceptions)
    // * Replaced ValueStringBuild with StringBuilder

    /// <summary>
    /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
    /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
    /// </summary>
    /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
    /// <param name="path">The destination path.</param>
    /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
    public static string GetRelativePath(string relativeTo, string path)
    {
        return GetRelativePath(relativeTo, path, StringComparison);
    }

    private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
    {
        if (relativeTo == null)
            throw new ArgumentNullException(nameof(relativeTo));

        if (PathInternal.IsEffectivelyEmpty(relativeTo.AsSpan()))
            throw new ArgumentException(SR.Arg_PathEmpty, nameof(relativeTo));

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

        if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
            throw new ArgumentException(SR.Arg_PathEmpty, nameof(path));

        Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);

        relativeTo = GetFullPath(relativeTo);
        path = GetFullPath(path);

        // Need to check if the roots are different- if they are we need to return the "to" path.
        if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType))
            return path;

        int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);

        // If there is nothing in common they can't share the same root, return the "to" path as is.
        if (commonLength == 0)
            return path;

        // Trailing separators aren't significant for comparison
        int relativeToLength = relativeTo.Length;
        if (EndsInDirectorySeparator(relativeTo.AsSpan()))
            relativeToLength--;

        bool pathEndsInSeparator = EndsInDirectorySeparator(path.AsSpan());
        int pathLength = path.Length;
        if (pathEndsInSeparator)
            pathLength--;

        // If we have effectively the same path, return "."
        if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";

        // We have the same root, we need to calculate the difference now using the
        // common Length and Segment count past the length.
        //
        // Some examples:
        //
        //  C:\Foo C:\Bar L3, S1 -> ..\Bar
        //  C:\Foo C:\Foo\Bar L6, S0 -> Bar
        //  C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
        //  C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar

        // Original: var sb = new ValueStringBuilder(stackalloc char[260]);
        var sb = new StringBuilder(260);
        sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length));

        // Add parent segments for segments past the common on the "from" path
        if (commonLength < relativeToLength)
        {
            sb.Append("..");

            for (int i = commonLength + 1; i < relativeToLength; i++)
            {
                if (PathInternal.IsDirectorySeparator(relativeTo[i]))
                {
                    sb.Append(DirectorySeparatorChar);
                    sb.Append("..");
                }
            }
        }
        else if (PathInternal.IsDirectorySeparator(path[commonLength]))
        {
            // No parent segments and we need to eat the initial separator
            //  (C:\Foo C:\Foo\Bar case)
            commonLength++;
        }

        // Now add the rest of the "to" path, adding back the trailing separator
        int differenceLength = pathLength - commonLength;
        if (pathEndsInSeparator)
            differenceLength++;

        if (differenceLength > 0)
        {
            if (sb.Length > 0)
            {
                sb.Append(DirectorySeparatorChar);
            }

            sb.Append(path.AsSpan(commonLength, differenceLength));
        }

        return sb.ToString();
    }

    /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
    internal static StringComparison StringComparison =>
        IsCaseSensitive ?
            StringComparison.Ordinal :
            StringComparison.OrdinalIgnoreCase;

    /// <summary>
    /// Returns true if the path ends in a directory separator.
    /// </summary>
    public static bool EndsInDirectorySeparator(string path) // Originally was public static bool EndsInDirectorySeparator(ReadOnlySpan<char> path)
        => path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]);

    #region Resources
    // From https://github.com/dotnet/corefx/blob/c390ce7df50252e11f5d322276e9d19e046d1332/src/Microsoft.IO.Redist/src/Resources/Strings.resx

    static class SR
    {
        public static string Arg_PathEmpty => "The path is empty.";
    }
    #endregion Resources

    #region Path.Windows 
    // Code from 
    // https://github.com/dotnet/corefx/blob/4a7075f188b5777ccb519f2af9b8a284f4383357/src/Common/src/CoreLib/System/IO/Path.Windows.cs

    // https://github.com/dotnet/corefx/blob/4a7075f188b5777ccb519f2af9b8a284f4383357/src/Common/src/CoreLib/System/IO/Path.Windows.cs#L235
    /// <summary>Gets whether the system is case-sensitive.</summary>
    internal static bool IsCaseSensitive => false;

    #endregion Path.Windows

    #region Workarounds

    // Note, this is here just to cause all .AsSpan() calls to return a string since I don't have access to ReadOnlySpan<char>
    // https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.primitives.stringsegment.asspan?view=dotnet-plat-ext-3.0
    static string AsSpan(this string s)
    {
        return s;
    }

    // Note, this is here just to cause all .AsSpan() calls to return a string since I don't have access to ReadOnlySpan<char>
    // https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.asspan?view=netcore-3.0#System_MemoryExtensions_AsSpan_System_String_System_Int32_System_Int32_
    static string AsSpan(this string s, int startIndex, int length)
    {
        return s.Substring(startIndex, length);
    }


    #endregion Workarounds

    // Code from 
    // https://github.com/dotnet/corefx/blob/b123ba4b9107c73cbc02010dc1ee78eb8ffccb93/src/Common/src/CoreLib/System/IO/PathInternal.cs
    // https://github.com/dotnet/corefx/blob/b123ba4b9107c73cbc02010dc1ee78eb8ffccb93/src/Common/src/CoreLib/System/IO/PathInternal.Windows.cs
    static class PathInternal
    {
        /// <summary>
        /// Returns true if the two paths have the same root
        /// </summary>
        internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType)
        {
            int firstRootLength = GetRootLength(first.AsSpan());
            int secondRootLength = GetRootLength(second.AsSpan());

            return firstRootLength == secondRootLength
                && string.Compare(
                    strA: first,
                    indexA: 0,
                    strB: second,
                    indexB: 0,
                    length: firstRootLength,
                    comparisonType: comparisonType) == 0;
        }

        #region PathInternal.Windows
        // Code from https://github.com/dotnet/corefx/blob/b123ba4b9107c73cbc02010dc1ee78eb8ffccb93/src/Common/src/CoreLib/System/IO/PathInternal.Windows.cs

        // \\?\, \\.\, \??\
        internal const int DevicePrefixLength = 4;

        // \\
        internal const int UncPrefixLength = 2;

        // \\?\UNC\, \\.\UNC\
        internal const int UncExtendedPrefixLength = 8;

        /// <summary>
        /// Returns true if the given character is a valid drive letter
        /// </summary>
        internal static bool IsValidDriveChar(char value)
        {
            return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
        }

        /// <summary>
        /// True if the given character is a directory separator.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsDirectorySeparator(char c)
        {
            return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
        }

        /// <summary>
        /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
        /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
        /// and path length checks.
        /// </summary>
        internal static bool IsExtended(string path) // Original was internal static bool IsExtended(ReadOnlySpan<char> path)
        {
            // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
            // Skipping of normalization will *only* occur if back slashes ('\') are used.
            return path.Length >= DevicePrefixLength
                && path[0] == '\\'
                && (path[1] == '\\' || path[1] == '?')
                && path[2] == '?'
                && path[3] == '\\';
        }

        /// <summary>
        /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
        /// </summary>
        internal static bool IsDevice(string path) // Original was: internal static bool IsDevice(ReadOnlySpan<char> path)
        {
            // If the path begins with any two separators is will be recognized and normalized and prepped with
            // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
            return IsExtended(path)
                ||
                (
                    path.Length >= DevicePrefixLength
                    && IsDirectorySeparator(path[0])
                    && IsDirectorySeparator(path[1])
                    && (path[2] == '.' || path[2] == '?')
                    && IsDirectorySeparator(path[3])
                );
        }

        /// <summary>
        /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\)
        /// </summary>
        internal static bool IsDeviceUNC(string path) // Original was: internal static bool IsDeviceUNC(ReadOnlySpan<char> path) 
        {
            return path.Length >= UncExtendedPrefixLength
                && IsDevice(path)
                && IsDirectorySeparator(path[7])
                && path[4] == 'U'
                && path[5] == 'N'
                && path[6] == 'C';
        }

        /// <summary>
        /// Gets the length of the root of the path (drive, share, etc.).
        /// </summary>
        internal static int GetRootLength(string path) // Note: original was internal static int GetRootLength(ReadOnlySpan<char> path)

        {
            int pathLength = path.Length;
            int i = 0;

            bool deviceSyntax = IsDevice(path);
            bool deviceUnc = deviceSyntax && IsDeviceUNC(path);

            if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0]))
            {
                // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
                if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1])))
                {
                    // UNC (\\?\UNC\ or \\), scan past server\share

                    // Start past the prefix ("\\" or "\\?\UNC\")
                    i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength;

                    // Skip two separators at most
                    int n = 2;
                    while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0))
                        i++;
                }
                else
                {
                    // Current drive rooted (e.g. "\foo")
                    i = 1;
                }
            }
            else if (deviceSyntax)
            {
                // Device path (e.g. "\\?\.", "\\.\")
                // Skip any characters following the prefix that aren't a separator
                i = DevicePrefixLength;
                while (i < pathLength && !IsDirectorySeparator(path[i]))
                    i++;

                // If there is another separator take it, as long as we have had at least one
                // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\")
                if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i]))
                    i++;
            }
            else if (pathLength >= 2
                && path[1] == VolumeSeparatorChar
                && IsValidDriveChar(path[0]))
            {
                // Valid drive specified path ("C:", "D:", etc.)
                i = 2;

                // If the colon is followed by a directory separator, move past it (e.g "C:\")
                if (pathLength > 2 && IsDirectorySeparator(path[2]))
                    i++;
            }

            return i;
        }

        /// <summary>
        /// Gets the count of common characters from the left optionally ignoring case
        /// </summary>
        internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase)
        {
            if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;

            int commonChars = 0;

            fixed (char* f = first)
            fixed (char* s = second)
            {
                char* l = f;
                char* r = s;
                char* leftEnd = l + first.Length;
                char* rightEnd = r + second.Length;

                while (l != leftEnd && r != rightEnd
                    && (*l == *r || (ignoreCase && char.ToUpperInvariant(*l) == char.ToUpperInvariant(*r))))
                {
                    commonChars++;
                    l++;
                    r++;
                }
            }

            return commonChars;
        }

        /// <summary>
        /// Get the common path length from the start of the string.
        /// </summary>
        internal static int GetCommonPathLength(string first, string second, bool ignoreCase)
        {
            int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);

            // If nothing matches
            if (commonChars == 0)
                return commonChars;

            // Or we're a full string and equal length or match to a separator
            if (commonChars == first.Length
                && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
                return commonChars;

            if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
                return commonChars;

            // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
            while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
                commonChars--;

            return commonChars;
        }

        /// <summary>
        /// Returns true if the path is effectively empty for the current OS.
        /// For unix, this is empty or null. For Windows, this is empty, null, or
        /// just spaces ((char)32).
        /// </summary>
        /// 
        internal static bool IsEffectivelyEmpty(string path)
        {
            // Note, see the original version below
            return string.IsNullOrWhiteSpace(path);
        }

        // Note: here's the original version.  I've replaced it with the version above that just uses string
        // 
        //internal static bool IsEffectivelyEmpty(ReadOnlySpan<char> path)
        //{
        //    if (path.IsEmpty)
        //        return true;

        //    foreach (char c in path)
        //    {
        //        if (c != ' ')
        //            return false;
        //    }
        //    return true;
        //}

        #endregion PathInternal.Windows
    }
}

2

我看到了这个问题:如何在.NET中获取绝对路径或规范化的文件路径?

public static string NormalizePath(string path)
{
    return Path.GetFullPath(new Uri(path).LocalPath)
           .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
           .ToUpperInvariant();
}

所以我会从这个方面开始,将两条路径规范化(也可以参见https://blogs.msdn.microsoft.com/jeremykuhne/2016/04/21/path-normalization/,以防涵盖更多情况)

然后,我会将它们分成子路径的数组/列表(例如使用如何从路径中提取每个文件夹名称?中的一种方法)

从那里开始,我会找到前N个共同部分的最大值。

然后,我会从第一个路径的部件计数C中减去N,即C-N,以获取需要添加到第一个路径中的..\\的数量,以便返回公共路径。

最后,在删除了其前N项后,我会添加toPath的其余部分,并返回结果路径。

如果你找到了规范化路径,你还可以使用字符串解析(而不是拆分为列表)来执行此操作(以避免额外的存储)。 这个想法是找到共同的字符串前缀,然后如果共同的部分没有以路径分隔符结尾,则修剪它的最后一部分(因为这将是一个巧合的额外共同部分,例如c:\ a \ test1和c:\ a \ test2具有共同路径c:\ a \,而不是像简单的公共前缀字符串提取中得到的c:\ a \ test)。

或者,您可以使用一种算法,在循环中同时处理两条规范化路径,为每个\返回字符索引(每次移动一步),以便您无需存储任何额外的信息。 逻辑与上述描述的逻辑类似。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接