ข้อ จำกัด เกี่ยวกับการขัดแย้งกับ PathRelativePathTo ในสภาพแวดล้อม“ รู้เส้นทางที่ยาวนาน”


12

สำหรับเส้นทางการตระหนักถึงกระบวนการใน Windows 10, ฉันพยายามที่จะเข้าใจในสิ่งที่ข้อ จำกัด ข้อโต้แย้งที่มีเมื่อใช้หน้าต่างเปลือกวิธีPathRelativePathTo

ในตัวอย่างด้านล่างฉันใช้ C # ผ่าน pinvoke เพื่อโทรหาวิธี
ฉันได้รับหลายตัวอย่างด้านล่างและผลลัพธ์ของพวกเขา บันทึก:

  • ตัวอย่างทั้งหมดให้เส้นทางไดเรกทอรีสำหรับ "จาก" และเส้นทางไฟล์สำหรับ "ถึง" (ไม่มีเส้นทางเหล่านี้จริง ๆ บนดิสก์)
  • ข้อสังเกตของฉันคือ
    • เส้นทางภายใต้ความยาว 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 ที่ฉันสามารถใช้แทนได้ (หมายเหตุ: ฉันเห็นว่า. NET Core มีPath.GetRelativePathแต่ฉันยังใช้ไม่ได้)

ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

ลืมPathRelativePathToมันไม่ได้มีไว้สำหรับเส้นทางยาว ๆ จริง ๆ แล้วมันไม่ปลอดภัยที่จะใช้เนื่องจากคุณไม่สามารถระบุขนาดของบัฟเฟอร์ปลายทางได้เอกสารระบุว่า "ต้องมีขนาดอย่างน้อย MAX_PATH ตัว"
acelent

เอกสารอย่างเป็นทางการค่อนข้างชัดเจนในขีด จำกัด MAX_PATH ในฐานะที่เป็นสำหรับการทดแทนจึงเป็นเรื่องง่ายที่จะได้รับมันผิดคุณสามารถกลับมาใช้แหล่งที่มาหลัก .NET หรือใช้เป็นจุดเริ่มต้น: github.com/dotnet/corefx/blob/...
ไซมอน Mourier

คุณกำลังใช้อะไรจนถึงตอนนี้ Classic .NET หรือ. NET Core เวอร์ชันใด
Pavel Anikhouski

. net framework เมื่อฉันสามารถย้ายไปยัง. net core 3.0 ได้ฉันจะถูกตั้งค่าทั้งหมดเนื่องจากมีวิธีการที่กล่าวถึงในตัว
Matt Smith

คำตอบ:


4

จากรูปลักษณ์ของมันดูเหมือนว่า PathRelativePathTo API จะมีความปลอดภัยสำหรับเส้นทางที่เกิน MAX_LENGTH เท่านั้น Atleast จากเอกสารของ Wine เราเห็นว่า API มีปัญหาในการใช้งาน Win32

รุ่น Win32 ของฟังก์ชันนี้ประกอบด้วยจุดบกพร่องที่อาจมีการอ้างอิงสายอักขระ lpszTo 1 ไบต์เกินจุดสิ้นสุดของสายอักขระ เป็นผลสุ่มขยะอาจถูกเขียนไปยังเส้นทางออกขึ้นอยู่กับสิ่งที่อยู่เกินไบต์สุดท้ายของสตริง ข้อผิดพลาดนี้เกิดขึ้นเนื่องจากพฤติกรรมของ PathCommonPrefix () (ดูหมายเหตุสำหรับฟังก์ชั่นนั้น) และดูเหมือนไม่มีวิธีแก้ปัญหาด้วย Win32 จุดบกพร่องนี้ได้รับการแก้ไขที่นี่ดังนั้นตัวอย่างเช่นเส้นทางสัมพัทธ์จาก "\" ถึง "\" จะถูกกำหนดอย่างถูกต้องว่า "" ในการดำเนินการนี้

และจากเอกสาร PathCommonPrefix

คำนำหน้าทั่วไปของ 2 ถูกส่งคืนเสมอเป็น 3 ดังนั้นจึงเป็นไปได้ที่ความยาวที่ส่งคืนจะไม่ถูกต้อง (เช่นยาวกว่าหนึ่งหรือทั้งสองของสตริงที่กำหนดเป็นพารามิเตอร์) ลักษณะการทำงาน Win32 นี้ถูกนำมาใช้ที่นี่และไม่สามารถเปลี่ยนแปลง (แก้ไขได้) โดยไม่ทำให้การเรียก SHLWAPI อื่น ๆ เสียหาย เพื่อหลีกเลี่ยงปัญหานี้เมื่อใช้ฟังก์ชั่นนี้ให้ตรวจสอบว่า byte ที่ [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

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("/",@"\");

หรือ ofcourse คุณสามารถใช้สิ่งที่ขึ้นอยู่กับ. 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
สิ่งนี้จะตอบคำถามได้อย่างไร
ตัวตลก

นี่เป็นแนวคิดเดียวกันที่อยู่เบื้องหลังการทำงานของ Path ทั้งหมด
Gilad

1
ไม่มีฟังก์ชั่น Path ทั้งหมดที่เป็นรูปธรรมPathRelativePathToไม่ได้รับผลกระทบจากส่วนนำหน้าใด ๆ นี่คือการแยกคำศัพท์บริสุทธิ์ API, hardcoded ถึง 260 ตัวอักษร จำกัด นอกจากนี้ยัง \\ vs / แตกต่าง - ทำลายมัน
RbMm

แม้จะมีความคิดเห็นที่ระบุว่าใช้งานไม่ได้: chat.stackoverflow.com/transcript/message/47826723#47826723
user1781290

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/ในกรณีที่ครอบคลุมกรณีเพิ่มเติม)

แล้วฉันจะแบ่งพวกเขาเป็นอาร์เรย์ / รายการของ subpaths (พูดด้วยหนึ่งในวิธีการจากวิธีหนึ่งจะแยกแต่ละชื่อโฟลเดอร์จากเส้นทางได้อย่างไร )

จากที่นั่นฉันจะพบส่วนแรกสูงสุด N ที่พบได้บ่อย

จากนั้นฉันก็ลบ N ออกจากส่วนแรกของเส้นทางส่วน C หรือที่รู้จัก CN เพื่อรับจำนวน .. \ ฉันต้องเพิ่มในเส้นทางแรกเพื่อกลับไปสู่เส้นทางทั่วไป

ในที่สุดฉันจะเพิ่มส่วนที่เหลือของ toPath หลังจากลบรายการ N แรกออกจากรายการและส่งคืนเส้นทางผลลัพธ์

คิดว่าคุณสามารถทำเช่นนั้น (เพื่อหลีกเลี่ยงการเก็บข้อมูลเพิ่มเติม) ด้วยการแยกสตริง (โดยไม่แยกในรายการ) เมื่อคุณพบเส้นทางปกติ แนวคิดจะเป็นว่าคุณจะพบคำนำหน้าสตริงทั่วไปแล้วตัดส่วนสุดท้ายของมันถ้าส่วนทั่วไปไม่ได้ลงท้ายด้วยตัวคั่นเส้นทาง (เนื่องจากจะเป็นส่วนทั่วไปที่บังเอิญร่วมกันเช่น c: \ a \ test1 และ c: \ a \ test2 มีเส้นทางทั่วไป c: \ a \ และไม่ใช่ c: \ a \ test ตามที่คุณจะได้รับพร้อมกับการดึงสตริงคำนำหน้าทั่วไปอย่างง่าย)

อีกวิธีหนึ่งคุณสามารถใช้อัลกอริทึมที่ส่งกลับดัชนีของอักขระสำหรับแต่ละ \ ทำงานสองเส้นทางปกติในเวลาเดียวกันในการวนรอบ (ขั้นตอนเดียวในแต่ละ) เพื่อให้คุณไม่จำเป็นต้องเก็บอะไรพิเศษ ตรรกะจะคล้ายกับที่อธิบายไว้ข้างต้น


1

ฉันตัดสินใจที่จะใช้พอร์ตของวิธีการ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://docs.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://docs.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://docs.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
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.