W przypadku procesu uwzględniającego długą ścieżkę w systemie Windows 10 próbuję zrozumieć, jakie są ograniczenia argumentów podczas korzystania z metody powłoki Windows PathRelativePathTo .
W poniższym przykładzie używam C # za pomocą pinvoke do wywołania metody.
Podałem wiele przykładów poniżej i ich wyniki. Uwaga:
- Wszystkie przykłady podają ścieżki do katalogu „od” i ścieżki do pliku „do” (żadna z tych ścieżek nie istnieje na dysku)
- Moje obserwacje są takie
- Ścieżki poniżej „krótkiej” długości MAX_PATH (260) zwracają sukces z oczekiwanym wynikiem.
- Niektóre ścieżki nad „krótkim” MAX_PATH zwracają sukces z poprawnym wynikiem.
- Niektóre ścieżki ponad „krótkim” MAX_PATH zwracają sukces z błędną odpowiedzią (tak!)
- Niektóre znacznie dłuższe ścieżki zwracają błąd. Jednak nie ma określonej stałej długości.
Źródło:
class Program
{
static class Native
{
// https://www.pinvoke.net/default.aspx/shlwapi.pathrelativepathto
// https://docs.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}");
}
}
}
Wynik:
#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
Pytania:
- Jakie jest oczekiwane zachowanie w
PathRelativePathTo
odniesieniu do powyższego? - Czy oczekuje się, że będzie działać poprawnie tylko ze ścieżkami poniżej „krótkiego” limitu MAX_PATH (a reszta zachowania jest niezdefiniowana)?
- Czy jest coś innego w .NET Framework, którego mogę użyć zamiast tego (Uwaga: Widzę, że .NET Core ma Path.GetRelativePath , ale nie mogę (jeszcze) tego użyć)?
PathRelativePathTo
, nie jest przeznaczony do długich ścieżek. W rzeczywistości nie jest bezpiecznie go używać, ponieważ nie można podać rozmiaru bufora docelowego, dokumentacja mówi tylko, że „musi mieć co najmniej znaki MAX_PATH”.Odpowiedzi:
Z wyglądu wygląda na to, że interfejs API PathRelativePathTo jest bezpieczny tylko dla ścieżek do MAX_LENGTH. Co najmniej z dokumentacji Wine widzimy, że interfejs API był problematyczny w implementacji Win32.
Z dokumentacji PathCommonPrefix
Ta informacja i przy założeniu, że implementacja shlwapi działa z buforami o długości MAX_SIZE i jest podobna do tej w Wine lub ReactOS ( https://doxygen.reactos.org/de/dff/dll_2win32_2shlwapi_2path_8c_source.html ) wydaje się wyjaśniać niezdefiniowane zachowanie, które widzisz podczas testowania.
Jeśli chodzi o rozwiązanie .NET, najłatwiejszym (może nie być najlepszym) rozwiązaniem jest użycie
System.Uri
Lub oczywiście możesz zaimplementować coś w oparciu o źródło .NET Core
Path.GetRelativePath
źródło
Rozwiązanie .NET 4.6.2
Użyj
\\?\C:\Verrrrrrrrrrrry long path
składni, jak opisano tutaj .Jest też świetny post na blogu na ten temat
Zasadniczo największym problemem, jaki mam, jest udostępnianie folderów przez Internet. Reszta jest w porządku.
Starsze wersje .NET
Jeśli używasz starszej wersji .NET, możesz sprawdzić tę funkcję Win32 API , będziesz tego potrzebować
P/Invoke
.Interfejs API systemu Windows ma wiele funkcji, które również mają wersje Unicode, co pozwala na ścieżkę o rozszerzonej długości dla maksymalnej całkowitej długości ścieżki wynoszącej 32 767 znaków
Możesz także sprawdzić to SO pytanie, które jest bardzo podobne do twojego.
Jak postępować z plikami o nazwie dłuższej niż 259 znaków?
źródło
PathRelativePathTo
PathRelativePathTo
nie ma wpływu na żaden prefiks. jest to czysty leksykalny parsujący interfejs API, zakodowany na stałe do limitu 260 znaków. też nawet \\ vs / different - zepsućw Jak można uzyskać bezwzględną lub znormalizowaną ścieżkę do pliku w .NET? Widzę
więc zacznę od tego, aby znormalizować dwie ścieżki (patrz także https://blogs.msdn.microsoft.com/jeremykuhne/2016/04/21/path-normalization/ w przypadku, gdy obejmuje więcej przypadków)
następnie podzieliłbym je na tablice / listy podścieżek (powiedzmy jedną z metod z Jak wyodrębnić nazwę każdego folderu ze ścieżki? )
stamtąd znajdę maks. N pierwszych części, które są wspólne.
następnie odejmę N od liczby części C pierwszej ścieżki, czyli CN, aby uzyskać liczbę .. \ Muszę dodać do pierwszej ścieżki, aby wrócić do wspólnej ścieżki.
w końcu dodam resztę toPath po usunięciu z niej pierwszych N elementów i zwróceniu wynikowej ścieżki
Zgadnij, że możesz to zrobić (aby uniknąć dodatkowej pamięci) podczas analizowania ciągów (bez dzielenia list) po znalezieniu znormalizowanych ścieżek. Chodzi o to, aby znaleźć wspólny przedrostek łańcucha, a następnie przyciąć jego ostatnią część, jeśli wspólna część nie skończyłaby się separatorem ścieżki (ponieważ byłaby to przypadkowa, bardzo częsta część, np. C: \ a \ test1 i c: \ a \ test2 mają wspólną ścieżkę c: \ a \, a nie c: \ a \ test, jak w przypadku zwykłej ekstrakcji ciągów przedrostków).
Alternatywnie możesz użyć algorytmu, który zwraca indeksy znaków dla każdego \ jednocześnie pracując nad dwiema znormalizowanymi ścieżkami w pętli (jeden krok na każdym), abyś nie musiał przechowywać czegoś dodatkowego. Logika byłaby podobna do opisanej powyżej.
źródło
Postanowiłem użyć portu metody.
dotnet/corefx
Path.GetRelativePath
Poniższy kod został zaadaptowany z następujących źródeł. Przeczytaj komentarze w kodzie, w których wymieniono wszelkie zastosowane dostosowania lub obejścia:
Moim celem w adaptacji kodu było
GetRelativePath
Kod
źródło