Ограничения на аргументы PathRelativePathTo в среде с поддержкой длинного пути - PullRequest
4 голосов
/ 09 ноября 2019

Для процесса с поддержкой длинного пути в Windows 10 я пытаюсь понять, каковы ограничения аргументов при использовании метода оболочки Windows PathRelativePathTo .

В моем примере ниже я использую C # через pinvoke для вызова метода.
Я привел несколько примеров ниже и их вывод. Примечание:

  • Во всех примерах указаны пути к каталогам для «from» и пути к файлам для «to» (на самом деле ни один из этих путей не существует на диске)
  • По моим наблюдениям
    • Пути под «короткой» длиной MAX_PATH (260) возвращают успех с ожидаемым результатом.
    • Некоторые пути сверх «короткий» MAX_PATH возвращают успех с правильным результатом.
    • Некоторые пути через «короткий» MAX_PATH возвращают успех с неправильный ответ (yikes!)
    • Некоторые более длинные пути возвращают ошибку. Однако он не имеет фиксированной максимальной длины.

Источник:

    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}");
            }
        }
    }

Выход:

#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 Framework, которое я могуиспользуйте вместо этого (Примечание: я вижу, что .NET Core имеет Path.GetRelativePath , но я не могу (пока) использовать это)?

Ответы [ 2 ]

1 голос
/ 13 ноября 2019

at Как можно получить абсолютный или нормализованный путь к файлу в .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 первых частей, которые являются общими.

тогда я бы вычел N из числа частей первого пути C, то есть CN, чтобы узнать, сколько ... \ Мне нужно добавить к первому пути, чтобы вернуться к общему пути.

наконец, я бы добавил оставшуюся часть toPath после удаления из него первых N элементов и вернул бы полученный путь

Думаю, вы также можете сделать это (чтобы избежать дополнительного хранения) со строкойРазбор (без разделения на списки), как только вы нашли нормализованные пути. Идея состоит в том, что вы найдете общий префикс строки и затем обрежете последнюю его часть, если общая часть не заканчивается разделителем пути (поскольку это будет случайная дополнительная общая часть, например, c: \ a\ test1 и c: \ a \ test2 имеют общий путь c: \ a \, а не c: \ a \ test, как вы могли бы получить с простым извлечением строки общего префикса).

В качестве альтернативы вы можете использоватьалгоритм, который возвращает индексы символов для каждого \ обработки двух нормализованных путей одновременно в цикле (по одному шагу на каждом), так что вам не нужно хранить что-то дополнительное. Логика будет аналогична описанной выше.

0 голосов
/ 15 ноября 2019

Похоже, что API-интерфейс PathRelativePathTo безопасен только для путей до MAX_LENGTH. По крайней мере из документации Wine мы видим, что API был проблематичным в реализации Win32.

Версия этой функции для Win32 содержит ошибку, из-за которой на строку lpszTo можно ссылаться на 1 байт за концомстрока. В результате случайный мусор может быть записан в выходной путь, в зависимости от того, что находится за пределами последнего байта строки. Эта ошибка возникает из-за поведения PathCommonPrefix () (см. Примечания к этой функции), и обходной путь с Win32 не представляется возможным. Эта ошибка была исправлена ​​здесь, поэтому, например, относительный путь от "\" до "\" правильно определен как "."в этой реализации.

А из документации PathCommonPrefix

Общий префикс 2 всегда возвращается как 3. Таким образом, возможно, что возвращаемая длина будет недействительной(т.е. длиннее одной или обеих строк, заданных в качестве параметров). Это поведение Win32 было реализовано здесь и не может быть изменено (исправлено?) Без прерывания других вызовов SHLWAPI. Чтобы обойти это при использовании этой функции, всегда проверяйте, что байт в [common_prefix_len-1] не является NUL. Если это так, вычтите 1 из префикса.

Эта информация и предполагается, что реализация shlwapi работает с буферами длины MAX_SIZE и аналогична той, что используется в Wine или ReactOS (https://doxygen.reactos.org/de/dff/dll_2win32_2shlwapi_2path_8c_source.html)похоже, объясняет неопределенное поведение, которое вы видите при тестировании.

Что касается решения .NET, самый простой способ (может быть, не самый лучший), который я могу придумать, - это использовать System.Uri* 1018. *

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

...