From dea25f58e6119ef1acc5c5cc2a7c98e52cdff519 Mon Sep 17 00:00:00 2001
From: Sean Hall <r.sean.hall@gmail.com>
Date: Thu, 26 May 2022 17:33:41 -0500
Subject: Add PathCanonicalizeForComparison.

---
 .../dutil/test/DUtilUnitTest/DUtilUnitTest.vcxproj |   2 +-
 src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp | 554 +++++++++++++++++++--
 2 files changed, 512 insertions(+), 44 deletions(-)

(limited to 'src/libs/dutil/test/DUtilUnitTest')

diff --git a/src/libs/dutil/test/DUtilUnitTest/DUtilUnitTest.vcxproj b/src/libs/dutil/test/DUtilUnitTest/DUtilUnitTest.vcxproj
index 1c821a7c..c37bdad1 100644
--- a/src/libs/dutil/test/DUtilUnitTest/DUtilUnitTest.vcxproj
+++ b/src/libs/dutil/test/DUtilUnitTest/DUtilUnitTest.vcxproj
@@ -40,7 +40,7 @@
 
   <PropertyGroup>
     <ProjectAdditionalIncludeDirectories>..\..\WixToolset.DUtil\inc</ProjectAdditionalIncludeDirectories>
-    <ProjectAdditionalLinkLibraries>rpcrt4.lib;Mpr.lib;Ws2_32.lib;urlmon.lib;wininet.lib</ProjectAdditionalLinkLibraries>
+    <ProjectAdditionalLinkLibraries>rpcrt4.lib;Mpr.lib;Ws2_32.lib;shlwapi.lib;urlmon.lib;wininet.lib</ProjectAdditionalLinkLibraries>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp b/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp
index 65856514..04d0b447 100644
--- a/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp
+++ b/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp
@@ -11,6 +11,290 @@ namespace DutilTests
     public ref class PathUtil
     {
     public:
+        [Fact]
+        void PathBackslashFixedTerminateTest()
+        {
+            HRESULT hr = S_OK;
+            WCHAR wzEmpty[1] = { L'\0' };
+            WCHAR wzSingleLetter[1] = { L'a' };
+            WCHAR wzSingleBackslash[1] = { L'\\' };
+            WCHAR wzSingleForwardSlash[1] = { L'/' };
+            WCHAR wzSingleLetterNullTerminated[2] = { L'a', L'\0' };
+            WCHAR wzSingleBackslashNullTerminated[2] = { L'\\', L'\0' };
+            WCHAR wzSingleForwardSlashNullTerminated[2] = { L'/', L'\0' };
+            WCHAR wzExtraSpaceLetterNullTerminated[3] = { L'a', L'\0', L'\0' };
+            WCHAR wzExtraSpaceBackslashNullTerminated[3] = { L'\\', L'\0', L'\0' };
+            WCHAR wzExtraSpaceForwardSlashNullTerminated[3] = { L'/', L'\0', L'\0' };
+
+            hr = PathFixedBackslashTerminate(wzEmpty, 0);
+            NativeAssert::SpecificReturnCode(E_INSUFFICIENT_BUFFER, hr, "PathFixedBackslashTerminate: zero-length, {0}", wzEmpty);
+
+            hr = PathFixedBackslashTerminate(wzEmpty, countof(wzEmpty));
+            NativeAssert::SpecificReturnCode(E_INSUFFICIENT_BUFFER, hr, "PathFixedBackslashTerminate: '' (length 1), {0}", wzEmpty);
+
+            hr = PathFixedBackslashTerminate(wzSingleLetter, countof(wzSingleLetter));
+            NativeAssert::SpecificReturnCode(E_INVALIDARG, hr, "PathFixedBackslashTerminate: 'a' (length 1)");
+
+            hr = PathFixedBackslashTerminate(wzSingleBackslash, countof(wzSingleBackslash));
+            NativeAssert::SpecificReturnCode(E_INVALIDARG, hr, "PathFixedBackslashTerminate: '\\' (length 1)");
+
+            hr = PathFixedBackslashTerminate(wzSingleForwardSlash, countof(wzSingleForwardSlash));
+            NativeAssert::SpecificReturnCode(E_INVALIDARG, hr, "PathFixedBackslashTerminate: '/' (length 1)");
+
+            hr = PathFixedBackslashTerminate(wzSingleLetterNullTerminated, countof(wzSingleLetterNullTerminated));
+            NativeAssert::SpecificReturnCode(E_INSUFFICIENT_BUFFER, hr, "PathFixedBackslashTerminate: 'a' (length 2)");
+
+            hr = PathFixedBackslashTerminate(wzSingleBackslashNullTerminated, countof(wzSingleBackslashNullTerminated));
+            NativeAssert::Succeeded(hr, "PathFixedBackslashTerminate: '\\' (length 2)");
+            NativeAssert::StringEqual(L"\\", wzSingleBackslashNullTerminated);
+
+            hr = PathFixedBackslashTerminate(wzSingleForwardSlashNullTerminated, countof(wzSingleForwardSlashNullTerminated));
+            NativeAssert::Succeeded(hr, "PathFixedBackslashTerminate: '/' (length 2)");
+            NativeAssert::StringEqual(L"\\", wzSingleForwardSlashNullTerminated);
+
+            hr = PathFixedBackslashTerminate(wzExtraSpaceLetterNullTerminated, countof(wzExtraSpaceLetterNullTerminated));
+            NativeAssert::Succeeded(hr, "PathFixedBackslashTerminate: 'a' (length 3)");
+            NativeAssert::StringEqual(L"a\\", wzExtraSpaceLetterNullTerminated);
+
+            hr = PathFixedBackslashTerminate(wzExtraSpaceBackslashNullTerminated, countof(wzExtraSpaceBackslashNullTerminated));
+            NativeAssert::Succeeded(hr, "PathFixedBackslashTerminate: '\\' (length 3)");
+            NativeAssert::StringEqual(L"\\", wzExtraSpaceBackslashNullTerminated);
+
+            hr = PathFixedBackslashTerminate(wzExtraSpaceForwardSlashNullTerminated, countof(wzExtraSpaceForwardSlashNullTerminated));
+            NativeAssert::Succeeded(hr, "PathFixedBackslashTerminate: '/' (length 3)");
+            NativeAssert::StringEqual(L"\\", wzExtraSpaceForwardSlashNullTerminated);
+        }
+
+        [Fact]
+        void PathBackslashTerminateTest()
+        {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
+            LPCWSTR rgwzPaths[16] =
+            {
+                L"", L"\\",
+                L"a", L"a\\",
+                L"\\", L"\\",
+                L"a\\", L"a\\",
+                L"/", L"\\",
+                L"a/", L"a\\",
+                L"\\\\", L"\\\\",
+                L"//", L"/\\",
+            };
+
+            try
+            {
+                for (DWORD i = 0; i < countof(rgwzPaths); i += 2)
+                {
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    hr = PathBackslashTerminate(&sczPath);
+                    NativeAssert::Succeeded(hr, "PathBackslashTerminate: {0}", rgwzPaths[i]);
+                    NativeAssert::StringEqual(rgwzPaths[i + 1], sczPath);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
+            }
+        }
+
+        [Fact]
+        void PathCanonicalizeForComparisonTest()
+        {
+            HRESULT hr = S_OK;
+            LPWSTR sczCanonicalized = NULL;
+
+            try
+            {
+                hr = PathCanonicalizeForComparison(L"C:\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789", 0, &sczCanonicalized);
+                Assert::Equal<HRESULT>(HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE), hr);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\C:\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789\\abcdefghijklomnopqrstuvwxyz0123456789", 0, &sczCanonicalized);
+                Assert::Equal<HRESULT>(HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE), hr);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server\\", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server\\", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server\\share", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server\\share", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server\\share\\", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server\\share\\", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\.\\share\\otherdir\\unc.exe", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\.\\share\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\.\\share\\otherdir\\unc.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\share\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server\\share\\..\\..\\otherdir\\unc.exe", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\server\\share\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\server\\share\\..\\..\\otherdir\\unc.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\UNC\\server\\share\\..\\..\\otherdir\\unc.exe", PATH_CANONICALIZE_KEEP_UNC_ROOT, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\?\\UNC\\server\\share\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\UNC\\server\\share\\..\\..\\otherdir\\unc.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\otherdir\\unc.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\dir\\subdir\\..\\..\\..\\otherdir\\pastroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\otherdir\\pastroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\C:\\dir\\subdir\\..\\..\\..\\otherdir\\pastroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\otherdir\\pastroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\C:dir\\subdir\\..\\..\\..\\otherdir\\pastroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\otherdir\\pastroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:dir\\subdir\\..\\..\\..\\otherdir\\pastrelativeroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\otherdir\\pastrelativeroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"A:dir\\subdir\\..\\..\\otherdir\\relativeroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\otherdir\\relativeroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:dir\\subdir\\otherdir\\relativeroot.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:dir\\subdir\\otherdir\\relativeroot.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\dir\\subdir\\..\\..\\otherdir\\backslashes.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\otherdir\\backslashes.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\dir\\subdir\\..\\..\\otherdir\\\\consecutivebackslashes.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\otherdir\\consecutivebackslashes.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:/dir/subdir/../../otherdir/forwardslashes.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\otherdir\\forwardslashes.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\C:\\test\\..\\validlongpath.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\validlongpath.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"\\\\?\\test\\..\\invalidlongpath.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\?\\invalidlongpath.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\.\\invalid:pathchars?.exe", 0, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\invalid:pathchars?.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\addprefix.exe", PATH_CANONICALIZE_APPEND_LONG_PATH_PREFIX, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"\\\\?\\C:\\addprefix.exe", sczCanonicalized);
+
+                hr = PathCanonicalizeForComparison(L"C:\\addbackslash.exe", PATH_CANONICALIZE_BACKSLASH_TERMINATE, &sczCanonicalized);
+                NativeAssert::Succeeded(hr, "Failed to canonicalize path");
+                NativeAssert::StringEqual(L"C:\\addbackslash.exe\\", sczCanonicalized);
+            }
+            finally
+            {
+                ReleaseStr(sczCanonicalized);
+            }
+        }
+
+        [Fact]
+        void PathDirectoryContainsPathTest()
+        {
+            HRESULT hr = S_OK;
+
+            hr = PathDirectoryContainsPath(L"", L"");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"", L"C:\\Directory");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\Directory");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Dir", L"C:\\Directory");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\DirectoryPlus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory\\", L"C:\\DirectoryPlus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory\\", L"C:\\Directory\\../Plus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory\\", L"C:\\Directory/../Plus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"\\\\server\\share\\Directory", L"\\\\server\\share\\DirectoryPlus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"\\\\server\\share\\Directory", L"\\\\discarded\\..\\server\\share\\Directory\\Plus");
+            Assert::Equal<HRESULT>(S_FALSE, hr);
+
+            hr = PathDirectoryContainsPath(L"..\\..", L"..\\..\\plus");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"..\\..", L"\\..\\..\\plus");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"\\..\\..", L"\\..\\..\\plus");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"C:..\\..", L"C:..\\..\\plus");
+            Assert::Equal<HRESULT>(E_INVALIDARG, hr);
+
+            hr = PathDirectoryContainsPath(L"\\\\server\\share\\Directory", L"\\\\server\\share\\Directory\\Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\directory\\plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory\\", L"C:\\Directory\\Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\.\\Directory\\Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory", L"C:\\Directory/Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"C:\\Directory\\", L"C:\\Directory/Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+
+            hr = PathDirectoryContainsPath(L"\\\\?\\C:\\Directory", L"C:\\Directory\\Plus");
+            Assert::Equal<HRESULT>(S_OK, hr);
+        }
+
         [Fact]
         void PathGetDirectoryTest()
         {
@@ -103,6 +387,57 @@ namespace DutilTests
                 NativeAssert::StringEqual(L"Software\\Microsoft\\", rgsczPaths[1]);
                 NativeAssert::StringEqual(L"Software\\Microsoft\\Windows\\", rgsczPaths[2]);
                 ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"c:/foo/bar/bas/a.txt", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for regular file path");
+                Assert::Equal<DWORD>(5, cPaths);
+                NativeAssert::StringEqual(L"c:/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"c:/foo/", rgsczPaths[1]);
+                NativeAssert::StringEqual(L"c:/foo/bar/", rgsczPaths[2]);
+                NativeAssert::StringEqual(L"c:/foo/bar/bas/", rgsczPaths[3]);
+                NativeAssert::StringEqual(L"c:/foo/bar/bas/a.txt", rgsczPaths[4]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"c:/foo/bar/bas/", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for regular directory path");
+                Assert::Equal<DWORD>(4, cPaths);
+                NativeAssert::StringEqual(L"c:/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"c:/foo/", rgsczPaths[1]);
+                NativeAssert::StringEqual(L"c:/foo/bar/", rgsczPaths[2]);
+                NativeAssert::StringEqual(L"c:/foo/bar/bas/", rgsczPaths[3]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"//server/share/subdir/file.txt", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for UNC file path");
+                Assert::Equal<DWORD>(3, cPaths);
+                NativeAssert::StringEqual(L"//server/share/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"//server/share/subdir/", rgsczPaths[1]);
+                NativeAssert::StringEqual(L"//server/share/subdir/file.txt", rgsczPaths[2]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"//server/share/subdir/", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for UNC directory path");
+                Assert::Equal<DWORD>(2, cPaths);
+                NativeAssert::StringEqual(L"//server/share/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"//server/share/subdir/", rgsczPaths[1]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"Software/Microsoft/Windows/ValueName", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for UNC directory path");
+                Assert::Equal<DWORD>(4, cPaths);
+                NativeAssert::StringEqual(L"Software/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"Software/Microsoft/", rgsczPaths[1]);
+                NativeAssert::StringEqual(L"Software/Microsoft/Windows/", rgsczPaths[2]);
+                NativeAssert::StringEqual(L"Software/Microsoft/Windows/ValueName", rgsczPaths[3]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
+
+                hr = PathGetHierarchyArray(L"Software/Microsoft/Windows/", &rgsczPaths, &cPaths);
+                NativeAssert::Succeeded(hr, "Failed to get parent directories array for UNC directory path");
+                Assert::Equal<DWORD>(3, cPaths);
+                NativeAssert::StringEqual(L"Software/", rgsczPaths[0]);
+                NativeAssert::StringEqual(L"Software/Microsoft/", rgsczPaths[1]);
+                NativeAssert::StringEqual(L"Software/Microsoft/Windows/", rgsczPaths[2]);
+                ReleaseNullStrArray(rgsczPaths, cPaths);
             }
             finally
             {
@@ -110,12 +445,66 @@ namespace DutilTests
             }
         }
 
+        [Fact]
+        void PathNormalizeSlashesFixedTest()
+        {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
+            LPCWSTR rgwzPaths[54] =
+            {
+                L"", L"",
+                L"\\", L"\\",
+                L"\\\\", L"\\\\",
+                L"\\\\\\", L"\\\\\\",
+                L"\\\\?\\UNC\\", L"\\\\?\\UNC\\",
+                L"C:\\\\foo2", L"C:\\foo2",
+                L"\\\\?\\C:\\\\foo2", L"\\\\?\\C:\\foo2",
+                L"\\\\a\\b\\", L"\\\\a\\b\\",
+                L"\\\\?\\UNC\\a\\b\\\\c\\", L"\\\\?\\UNC\\a\\b\\c\\",
+                L"\\\\?\\UNC\\a\\b\\\\", L"\\\\?\\UNC\\a\\b\\",
+                L"\\\\?\\UNC\\test\\unc\\path\\to\\\\something", L"\\\\?\\UNC\\test\\unc\\path\\to\\something",
+                L"\\\\?\\C:\\\\foo\\\\bar.txt", L"\\\\?\\C:\\foo\\bar.txt",
+                L"\\??\\C:\\\\foo\\bar.txt", L"\\??\\C:\\foo\\bar.txt",
+                L"\\??\\\\C:\\\\foo\\bar.txt", L"\\??\\\\C:\\foo\\bar.txt",
+                L"/", L"\\",
+                L"//", L"\\\\",
+                L"///", L"\\\\\\",
+                L"//?/UNC/", L"\\\\?\\UNC\\",
+                L"C://foo2", L"C:\\foo2",
+                L"//?/C://foo2", L"\\\\?\\C:\\foo2",
+                L"//a/b/", L"\\\\a\\b\\",
+                L"//?/UNC/a/b//c/", L"\\\\?\\UNC\\a\\b\\c\\",
+                L"//?/UNC/a/b//", L"\\\\?\\UNC\\a\\b\\",
+                L"//?/UNC/test/unc/path/to//something", L"\\\\?\\UNC\\test\\unc\\path\\to\\something",
+                L"//?/C://foo//bar.txt", L"\\\\?\\C:\\foo\\bar.txt",
+                L"/??/C://foo/bar.txt", L"\\??\\C:\\foo\\bar.txt",
+                L"/??//C://foo/bar.txt", L"\\??\\\\C:\\foo\\bar.txt",
+            };
+
+            try
+            {
+                for (DWORD i = 0; i < countof(rgwzPaths); i += 2)
+                {
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    hr = PathFixedNormalizeSlashes(sczPath);
+                    NativeAssert::Succeeded(hr, "PathNormalizeSlashes: {0}", rgwzPaths[i]);
+                    NativeAssert::StringEqual(rgwzPaths[i + 1], sczPath);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
+            }
+        }
+
         [Fact]
         void PathPrefixTest()
         {
             HRESULT hr = S_OK;
             LPWSTR sczPath = NULL;
-            LPCWSTR rgwzPaths[12] =
+            LPCWSTR rgwzPaths[24] =
             {
                 L"\\\\", L"\\\\?\\UNC\\",
                 L"C:\\\\foo2", L"\\\\?\\C:\\\\foo2",
@@ -123,11 +512,17 @@ namespace DutilTests
                 L"\\\\?\\UNC\\test\\unc\\path\\to\\something", L"\\\\?\\UNC\\test\\unc\\path\\to\\something",
                 L"\\\\?\\C:\\foo\\bar.txt", L"\\\\?\\C:\\foo\\bar.txt",
                 L"\\??\\C:\\foo\\bar.txt", L"\\??\\C:\\foo\\bar.txt",
+                L"//", L"\\\\?\\UNC\\",
+                L"C://foo2", L"\\\\?\\C://foo2",
+                L"//a/b/", L"\\\\?\\UNC\\a/b/",
+                L"//?/UNC/test/unc/path/to/something", L"//?/UNC/test/unc/path/to/something",
+                L"//?/C:/foo/bar.txt", L"//?/C:/foo/bar.txt",
+                L"/??/C:/foo/bar.txt", L"/??/C:/foo/bar.txt",
             };
 
             try
             {
-                for (DWORD i = 0; i < countof(rgwzPaths) / 2; i += 2)
+                for (DWORD i = 0; i < countof(rgwzPaths); i += 2)
                 {
                     hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
                     NativeAssert::Succeeded(hr, "Failed to copy string");
@@ -148,16 +543,20 @@ namespace DutilTests
         {
             HRESULT hr = S_OK;
             LPWSTR sczPath = NULL;
-            LPCWSTR rgwzPaths[8] =
+            LPCWSTR rgwzPaths[12] =
             {
                 L"\\",
+                L"/",
                 L"C:",
                 L"C:foo.txt",
                 L"",
                 L"\\?",
+                L"/?",
                 L"\\dir",
+                L"/dir",
                 L"dir",
                 L"dir\\subdir",
+                L"dir/subdir",
             };
 
             try
@@ -180,93 +579,162 @@ namespace DutilTests
         [Fact]
         void PathIsRootedAndFullyQualifiedTest()
         {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
             LPCWSTR rgwzPaths[15] =
             {
-                L"\\\\",
-                L"\\\\\\",
-                L"C:\\",
-                L"C:\\\\",
-                L"C:\\foo1",
-                L"C:\\\\foo2",
-                L"\\\\test\\unc\\path\\to\\something",
-                L"\\\\a\\b\\c\\d\\e",
-                L"\\\\a\\b\\",
-                L"\\\\a\\b",
-                L"\\\\test\\unc",
-                L"\\\\Server",
-                L"\\\\Server\\Foo.txt",
-                L"\\\\Server\\Share\\Foo.txt",
-                L"\\\\Server\\Share\\Test\\Foo.txt",
+                L"//",
+                L"///",
+                L"C:/",
+                L"C://",
+                L"C:/foo1",
+                L"C://foo2",
+                L"//test/unc/path/to/something",
+                L"//a/b/c/d/e",
+                L"//a/b/",
+                L"//a/b",
+                L"//test/unc",
+                L"//Server",
+                L"//Server/Foo.txt",
+                L"//Server/Share/Foo.txt",
+                L"//Server/Share/Test/Foo.txt",
             };
 
-            for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+            try
             {
-                ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, FALSE);
-                ValidateRootedPath(rgwzPaths[i], TRUE);
+                for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+                {
+                    ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, FALSE);
+                    ValidateRootedPath(rgwzPaths[i], TRUE);
+
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    PathFixedReplaceForwardSlashes(sczPath);
+                    ValidateFullyQualifiedPath(sczPath, TRUE, FALSE);
+                    ValidateRootedPath(sczPath, TRUE);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
             }
         }
 
         [Fact]
         void PathIsRootedAndFullyQualifiedWithPrefixTest()
         {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
             LPCWSTR rgwzPaths[6] =
             {
-                L"\\\\?\\UNC\\test\\unc\\path\\to\\something",
-                L"\\\\?\\UNC\\test\\unc",
-                L"\\\\?\\UNC\\a\\b1",
-                L"\\\\?\\UNC\\a\\b2\\",
-                L"\\\\?\\C:\\foo\\bar.txt",
-                L"\\??\\C:\\foo\\bar.txt",
+                L"//?/UNC/test/unc/path/to/something",
+                L"//?/UNC/test/unc",
+                L"//?/UNC/a/b1",
+                L"//?/UNC/a/b2/",
+                L"//?/C:/foo/bar.txt",
+                L"/??/C:/foo/bar.txt",
             };
 
-            for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+            try
             {
-                ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, TRUE);
-                ValidateRootedPath(rgwzPaths[i], TRUE);
+                for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+                {
+                    ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, TRUE);
+                    ValidateRootedPath(rgwzPaths[i], TRUE);
+
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    PathFixedReplaceForwardSlashes(sczPath);
+                    ValidateFullyQualifiedPath(sczPath, TRUE, TRUE);
+                    ValidateRootedPath(sczPath, TRUE);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
             }
         }
 
         [Fact]
         void PathIsRootedButNotFullyQualifiedTest()
         {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
             LPCWSTR rgwzPaths[7] =
             {
-                L"\\",
+                L"/",
                 L"a:",
                 L"A:",
                 L"z:",
                 L"Z:",
                 L"C:foo.txt",
-                L"\\dir",
+                L"/dir",
             };
 
-            for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+            try
             {
-                ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE);
-                ValidateRootedPath(rgwzPaths[i], TRUE);
+                for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+                {
+                    ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE);
+                    ValidateRootedPath(rgwzPaths[i], TRUE);
+
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    PathFixedReplaceForwardSlashes(sczPath);
+                    ValidateFullyQualifiedPath(sczPath, FALSE, FALSE);
+                    ValidateRootedPath(sczPath, TRUE);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
             }
         }
 
         [Fact]
         void PathIsNotRootedAndNotFullyQualifiedTest()
         {
+            HRESULT hr = S_OK;
+            LPWSTR sczPath = NULL;
             LPCWSTR rgwzPaths[9] =
             {
                 NULL,
                 L"",
                 L"dir",
-                L"dir\\subdir",
-                L"@:\\foo",  // 064 = @     065 = A
-                L"[:\\\\",   // 091 = [     090 = Z
-                L"`:\\foo ", // 096 = `     097 = a
-                L"{:\\\\",   // 123 = {     122 = z
+                L"dir/subdir",
+                L"@:/foo",  // 064 = @     065 = A
+                L"[://",   // 091 = [     090 = Z
+                L"`:/foo ", // 096 = `     097 = a
+                L"{://",   // 123 = {     122 = z
                 L"[:",
             };
 
-            for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+            try
             {
-                ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE);
-                ValidateRootedPath(rgwzPaths[i], FALSE);
+                for (DWORD i = 0; i < countof(rgwzPaths); ++i)
+                {
+                    ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE);
+                    ValidateRootedPath(rgwzPaths[i], FALSE);
+
+                    if (!rgwzPaths[i])
+                    {
+                        continue;
+                    }
+
+                    hr = StrAllocString(&sczPath, rgwzPaths[i], 0);
+                    NativeAssert::Succeeded(hr, "Failed to copy string");
+
+                    PathFixedReplaceForwardSlashes(sczPath);
+                    ValidateFullyQualifiedPath(sczPath, FALSE, FALSE);
+                    ValidateRootedPath(sczPath, FALSE);
+                }
+            }
+            finally
+            {
+                ReleaseStr(sczPath);
             }
         }
 
-- 
cgit v1.2.3-55-g6feb