对于Windows 10上的长路径感知进程,我试图了解使用windows方法PathRelativePathTo时参数限制是什么。
在下面的示例中,我通过pinvoke使用C#调用该方法。
下面我给出了多个例子及其输出。注意:
资料来源:
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的预期行为是什么?发布于 2019-11-15 13:50:18
从外观上看,PathRelativePathTo API似乎只对通向MAX_LENGTH的路径是安全的。至少从葡萄酒文档中,我们看到API在Win32实现中一直存在问题。
此函数的Win32版本包含一个bug,在该bug中,lpszTo字符串可能在字符串结束后1字节被引用。因此,可以将随机垃圾写入输出路径,具体取决于字符串的最后一个字节之外的内容。出现此错误是因为PathCommonPrefix()的行为(请参阅该函数的说明),而使用Win32似乎无法解决这个问题。此bug已在这里修复,因此,例如,从"\“到"\”的相对路径被正确地确定为“”。在这个实现中。
从PathCommonPrefix文档来看,
一个公共前缀2总是作为3返回。因此返回的长度可能无效(即超过一个或两个字符串作为参数)。此Win32行为已在此处实现,无法更改(修复?)而不中断其他SHLWAPI调用。若要在使用此函数时处理此问题,请始终检查公共_前缀_len-1中的字节是否为NUL。如果是,则从前缀中减去1。
这些信息和假设shlwapi实现与MAX_SIZE长度的缓冲区一起工作,并且类似于在葡萄酒或ReactOS (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核心源代码Path.GetRelativePath实现一些东西
发布于 2019-11-19 21:55:49
我决定使用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
}
}发布于 2019-11-12 22:52:00
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,也就是C部分的计数中减去N,得到第一条路径需要添加多少..\才能返回到公共路径。
最后,在删除第一个N项并返回结果路径之后,我将添加toPath的其余部分
在找到规范化路径后,您还可以使用字符串解析(而不是在列表中拆分)来完成(以避免额外的存储)。这样做的想法是,如果公共部分没有以路径分隔符结束(因为这是一个巧合的额外公共部分,例如c:\a\test 1和c:\a\ test 2有公共路径c:\a\\,而不是c:\a\\test),那么您可以找到公共字符串前缀,然后修剪它的最后一部分(因为这是一个巧合的额外公共部分)。
或者,您可以使用一种算法,为每个字符索引在一个循环中同时处理两个归一化路径(每个循环一个步骤),这样您就不需要存储额外的内容了。逻辑将类似于上面所述的逻辑。
https://stackoverflow.com/questions/58774168
复制相似问题