From fb4f8c7108f43d2341ba299424646c4963b21188 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Thu, 26 May 2022 17:33:15 -0500 Subject: Replace PathIsAbsolute with PathIsRooted and add PathIsFullyQualified. --- src/burn/engine/cache.cpp | 2 +- src/burn/engine/logging.cpp | 2 +- .../NativeAssert.h | 21 +++ .../XunitExtensions/SpecificReturnCodeException.cs | 19 ++ .../XunitExtensions/WixAssert.cs | 8 + src/libs/dutil/WixToolset.DUtil/inc/pathutil.h | 19 +- src/libs/dutil/WixToolset.DUtil/pathutil.cpp | 102 +++++++++-- src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp | 199 +++++++++++++++++++++ 8 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 src/internal/WixBuildTools.TestSupport/XunitExtensions/SpecificReturnCodeException.cs (limited to 'src') diff --git a/src/burn/engine/cache.cpp b/src/burn/engine/cache.cpp index 5d81e1ba..2b7d6ede 100644 --- a/src/burn/engine/cache.cpp +++ b/src/burn/engine/cache.cpp @@ -563,7 +563,7 @@ extern "C" HRESULT CacheGetLocalSourcePaths( fPreferSourcePathLocation = !pCache->fRunningFromCache || FAILED(hr); fTryLastFolder = SUCCEEDED(hr) && sczLastSourceFolder && *sczLastSourceFolder && CSTR_EQUAL != ::CompareStringW(LOCALE_NEUTRAL, NORM_IGNORECASE, pCache->sczSourceProcessFolder, -1, sczLastSourceFolder, -1); fTryRelativePath = CSTR_EQUAL != ::CompareStringW(LOCALE_NEUTRAL, NORM_IGNORECASE, wzSourcePath, -1, wzRelativePath, -1); - fSourceIsAbsolute = PathIsAbsolute(wzSourcePath); + fSourceIsAbsolute = PathIsRooted(wzSourcePath); // If the source path provided is a full path, try that first. if (fSourceIsAbsolute) diff --git a/src/burn/engine/logging.cpp b/src/burn/engine/logging.cpp index 1b2bec1f..8e89957b 100644 --- a/src/burn/engine/logging.cpp +++ b/src/burn/engine/logging.cpp @@ -140,7 +140,7 @@ extern "C" HRESULT LoggingOpen( LPCWSTR wzPrefix = sczPrefixFormatted; // Best effort to open default logging. - if (PathIsAbsolute(sczPrefixFormatted)) + if (PathIsRooted(sczPrefixFormatted)) { hr = PathGetDirectory(sczPrefixFormatted, &sczLoggingBaseFolder); ExitOnFailure(hr, "Failed to get parent directory from '%ls'.", sczPrefixFormatted); diff --git a/src/internal/WixBuildTools.TestSupport.Native/NativeAssert.h b/src/internal/WixBuildTools.TestSupport.Native/NativeAssert.h index 62ace4a9..b0206b14 100644 --- a/src/internal/WixBuildTools.TestSupport.Native/NativeAssert.h +++ b/src/internal/WixBuildTools.TestSupport.Native/NativeAssert.h @@ -64,6 +64,27 @@ namespace TestSupport { WixAssert::Succeeded(hr, gcnew String(zFormat), formatArgs); } + static void SpecificReturnCode(HRESULT hrExpected, HRESULT hr, LPCSTR zFormat, LPCSTR zArg, ... array^ zArgs) + { + array^ formatArgs = gcnew array(zArgs->Length + 1); + formatArgs[0] = NativeAssert::LPSTRToString(zArg); + for (int i = 0; i < zArgs->Length; ++i) + { + formatArgs[i + 1] = NativeAssert::LPSTRToString(zArgs[i]); + } + WixAssert::SpecificReturnCode(hrExpected, hr, gcnew String(zFormat), formatArgs); + } + + static void SpecificReturnCode(HRESULT hrExpected, HRESULT hr, LPCSTR zFormat, ... array^ wzArgs) + { + array^ formatArgs = gcnew array(wzArgs->Length); + for (int i = 0; i < wzArgs->Length; ++i) + { + formatArgs[i] = NativeAssert::LPWSTRToString(wzArgs[i]); + } + WixAssert::SpecificReturnCode(hrExpected, hr, gcnew String(zFormat), formatArgs); + } + static void ValidReturnCode(HRESULT hr, ... array^ validReturnCodes) { Assert::Contains(hr, (IEnumerable^)validReturnCodes); diff --git a/src/internal/WixBuildTools.TestSupport/XunitExtensions/SpecificReturnCodeException.cs b/src/internal/WixBuildTools.TestSupport/XunitExtensions/SpecificReturnCodeException.cs new file mode 100644 index 00000000..c66890f8 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/XunitExtensions/SpecificReturnCodeException.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixBuildTools.TestSupport +{ + using System; + using Xunit.Sdk; + + public class SpecificReturnCodeException : XunitException + { + public SpecificReturnCodeException(int hr, string userMessage) + : base(String.Format("WixAssert.SpecificReturnCode() Failure\r\n" + + "HRESULT: 0x{0:X8}\r\n" + + "Message: {1}", + hr, userMessage)) + { + this.HResult = hr; + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/XunitExtensions/WixAssert.cs b/src/internal/WixBuildTools.TestSupport/XunitExtensions/WixAssert.cs index 10156547..1ede55b3 100644 --- a/src/internal/WixBuildTools.TestSupport/XunitExtensions/WixAssert.cs +++ b/src/internal/WixBuildTools.TestSupport/XunitExtensions/WixAssert.cs @@ -52,6 +52,14 @@ namespace WixBuildTools.TestSupport throw new SkipTestException(message); } + public static void SpecificReturnCode(int hrExpected, int hr, string format, params object[] formatArgs) + { + if (hrExpected != hr) + { + throw new SpecificReturnCodeException(hr, String.Format(format, formatArgs)); + } + } + public static void Succeeded(int hr, string format, params object[] formatArgs) { if (0 > hr) diff --git a/src/libs/dutil/WixToolset.DUtil/inc/pathutil.h b/src/libs/dutil/WixToolset.DUtil/inc/pathutil.h index 44d36568..602b4a80 100644 --- a/src/libs/dutil/WixToolset.DUtil/inc/pathutil.h +++ b/src/libs/dutil/WixToolset.DUtil/inc/pathutil.h @@ -166,10 +166,23 @@ DAPI_(HRESULT) PathGetKnownFolder( ); /******************************************************************* - PathIsAbsolute - returns true if the path is absolute; false - otherwise. + PathIsFullyQualified - returns true if the path is fully qualified; false otherwise. + Note that some rooted paths like C:dir are not fully qualified. + For example, these are all fully qualified: C:\dir, \\server\share, \\?\C:\dir. + For example, these are not fully qualified: C:dir, C:, \dir, dir, dir\subdir. *******************************************************************/ -DAPI_(BOOL) PathIsAbsolute( +DAPI_(BOOL) PathIsFullyQualified( + __in_z LPCWSTR wzPath, + __out_opt BOOL* pfHasLongPathPrefix + ); + +/******************************************************************* + PathIsRooted - returns true if the path is rooted; false otherwise. + Note that some rooted paths like C:dir are not fully qualified. + For example, these are all rooted: C:\dir, C:dir, C:, \dir, \\server\share, \\?\C:\dir. + For example, these are not rooted: dir, dir\subdir. +*******************************************************************/ +DAPI_(BOOL) PathIsRooted( __in_z LPCWSTR wzPath ); diff --git a/src/libs/dutil/WixToolset.DUtil/pathutil.cpp b/src/libs/dutil/WixToolset.DUtil/pathutil.cpp index 9823779b..314eab85 100644 --- a/src/libs/dutil/WixToolset.DUtil/pathutil.cpp +++ b/src/libs/dutil/WixToolset.DUtil/pathutil.cpp @@ -20,6 +20,10 @@ #define PATH_GOOD_ENOUGH 64 +static BOOL IsValidDriveChar( + __in WCHAR wc + ); + DAPI_(LPWSTR) PathFile( __in_z LPCWSTR wzPath @@ -272,29 +276,30 @@ DAPI_(HRESULT) PathPrefix( HRESULT hr = S_OK; LPWSTR wzFullPath = *psczFullPath; + BOOL fFullyQualified = FALSE; + BOOL fHasPrefix = FALSE; SIZE_T cbFullPath = 0; - if (((L'a' <= wzFullPath[0] && L'z' >= wzFullPath[0]) || - (L'A' <= wzFullPath[0] && L'Z' >= wzFullPath[0])) && - L':' == wzFullPath[1] && - L'\\' == wzFullPath[2]) // normal path + fFullyQualified = PathIsFullyQualified(wzFullPath, &fHasPrefix); + if (fHasPrefix) + { + ExitFunction(); + } + + if (fFullyQualified && L':' == wzFullPath[1]) // normal path { hr = StrAllocPrefix(psczFullPath, L"\\\\?\\", 4); PathExitOnFailure(hr, "Failed to add prefix to file path."); } - else if (L'\\' == wzFullPath[0] && L'\\' == wzFullPath[1]) // UNC + else if (fFullyQualified && L'\\' == wzFullPath[1]) // UNC { - // ensure that we're not already prefixed - if (!(L'?' == wzFullPath[2] && L'\\' == wzFullPath[3])) - { - hr = StrSize(*psczFullPath, &cbFullPath); - PathExitOnFailure(hr, "Failed to get size of full path."); + hr = StrSize(*psczFullPath, &cbFullPath); + PathExitOnFailure(hr, "Failed to get size of full path."); - memmove_s(wzFullPath, cbFullPath, wzFullPath + 1, cbFullPath - sizeof(WCHAR)); + memmove_s(wzFullPath, cbFullPath, wzFullPath + 1, cbFullPath - sizeof(WCHAR)); - hr = StrAllocPrefix(psczFullPath, L"\\\\?\\UNC", 7); - PathExitOnFailure(hr, "Failed to add prefix to UNC path."); - } + hr = StrAllocPrefix(psczFullPath, L"\\\\?\\UNC", 7); + PathExitOnFailure(hr, "Failed to add prefix to UNC path."); } else { @@ -814,11 +819,66 @@ LExit: } -DAPI_(BOOL) PathIsAbsolute( +DAPI_(BOOL) PathIsFullyQualified( + __in_z LPCWSTR wzPath, + __out_opt BOOL* pfHasLongPathPrefix + ) +{ + BOOL fFullyQualified = FALSE; + BOOL fHasLongPathPrefix = FALSE; + + if (!wzPath || !wzPath[0] || !wzPath[1]) + { + // There is no way to specify a fully qualified path with one character (or less). + ExitFunction(); + } + + if (L'\\' != wzPath[0]) + { + // The only way to specify a fully qualified path that doesn't begin with a slash + // is the drive, colon, slash format (C:\). + if (IsValidDriveChar(wzPath[0]) && + L':' == wzPath[1] && + L'\\' == wzPath[2]) + { + fFullyQualified = TRUE; + } + + ExitFunction(); + } + + // Non-drive fully qualified paths must start with \\ or \?. + // \??\ is an archaic form of \\?\. + if (L'?' != wzPath[1] && L'\\' != wzPath[1]) + { + ExitFunction(); + } + + fFullyQualified = TRUE; + + if (L'?' == wzPath[2] && L'\\' == wzPath[3]) + { + fHasLongPathPrefix = TRUE; + } + + +LExit: + if (pfHasLongPathPrefix) + { + *pfHasLongPathPrefix = fHasLongPathPrefix; + } + + return fFullyQualified; +} + + +DAPI_(BOOL) PathIsRooted( __in_z LPCWSTR wzPath ) { - return wzPath && wzPath[0] && wzPath[1] && (wzPath[0] == L'\\') || (wzPath[1] == L':'); + return wzPath && + (wzPath[0] == L'\\' || + IsValidDriveChar(wzPath[0]) && wzPath[1] == L':'); } @@ -847,7 +907,7 @@ DAPI_(HRESULT) PathConcatCch( hr = StrAllocString(psczCombined, wzPath1, cchPath1); PathExitOnFailure(hr, "Failed to copy just path1 to output."); } - else if (!wzPath1 || !*wzPath1 || PathIsAbsolute(wzPath2)) + else if (!wzPath1 || !*wzPath1 || PathIsRooted(wzPath2)) { hr = StrAllocString(psczCombined, wzPath2, cchPath2); PathExitOnFailure(hr, "Failed to copy just path2 to output."); @@ -1002,3 +1062,11 @@ LExit: return hr; } + +static BOOL IsValidDriveChar( + __in WCHAR wc + ) +{ + return L'a' <= wc && L'z' >= wc || + L'A' <= wc && L'Z' >= wc; +} diff --git a/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp b/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp index 6a05a45c..65856514 100644 --- a/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp +++ b/src/libs/dutil/test/DUtilUnitTest/PathUtilTest.cpp @@ -109,5 +109,204 @@ namespace DutilTests ReleaseStrArray(rgsczPaths, cPaths); } } + + [Fact] + void PathPrefixTest() + { + HRESULT hr = S_OK; + LPWSTR sczPath = NULL; + LPCWSTR rgwzPaths[12] = + { + 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) + { + hr = StrAllocString(&sczPath, rgwzPaths[i], 0); + NativeAssert::Succeeded(hr, "Failed to copy string"); + + hr = PathPrefix(&sczPath); + NativeAssert::Succeeded(hr, "PathPrefix: {0}", rgwzPaths[i]); + NativeAssert::StringEqual(rgwzPaths[i + 1], sczPath); + } + } + finally + { + ReleaseStr(sczPath); + } + } + + [Fact] + void PathPrefixFailureTest() + { + HRESULT hr = S_OK; + LPWSTR sczPath = NULL; + LPCWSTR rgwzPaths[8] = + { + L"\\", + L"C:", + L"C:foo.txt", + L"", + L"\\?", + L"\\dir", + L"dir", + L"dir\\subdir", + }; + + try + { + for (DWORD i = 0; i < countof(rgwzPaths); ++i) + { + hr = StrAllocString(&sczPath, rgwzPaths[i], 0); + NativeAssert::Succeeded(hr, "Failed to copy string"); + + hr = PathPrefix(&sczPath); + NativeAssert::SpecificReturnCode(E_INVALIDARG, hr, "PathPrefix: {0}, {1}", rgwzPaths[i], sczPath); + } + } + finally + { + ReleaseStr(sczPath); + } + } + + [Fact] + void PathIsRootedAndFullyQualifiedTest() + { + 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", + }; + + for (DWORD i = 0; i < countof(rgwzPaths); ++i) + { + ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, FALSE); + ValidateRootedPath(rgwzPaths[i], TRUE); + } + } + + [Fact] + void PathIsRootedAndFullyQualifiedWithPrefixTest() + { + 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", + }; + + for (DWORD i = 0; i < countof(rgwzPaths); ++i) + { + ValidateFullyQualifiedPath(rgwzPaths[i], TRUE, TRUE); + ValidateRootedPath(rgwzPaths[i], TRUE); + } + } + + [Fact] + void PathIsRootedButNotFullyQualifiedTest() + { + LPCWSTR rgwzPaths[7] = + { + L"\\", + L"a:", + L"A:", + L"z:", + L"Z:", + L"C:foo.txt", + L"\\dir", + }; + + for (DWORD i = 0; i < countof(rgwzPaths); ++i) + { + ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE); + ValidateRootedPath(rgwzPaths[i], TRUE); + } + } + + [Fact] + void PathIsNotRootedAndNotFullyQualifiedTest() + { + 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"[:", + }; + + for (DWORD i = 0; i < countof(rgwzPaths); ++i) + { + ValidateFullyQualifiedPath(rgwzPaths[i], FALSE, FALSE); + ValidateRootedPath(rgwzPaths[i], FALSE); + } + } + + void ValidateFullyQualifiedPath(LPCWSTR wzPath, BOOL fExpected, BOOL fExpectedHasPrefix) + { + BOOL fHasLongPathPrefix = FALSE; + BOOL fRooted = PathIsFullyQualified(wzPath, &fHasLongPathPrefix); + String^ message = String::Format("IsFullyQualified: {0}", gcnew String(wzPath)); + if (fExpected) + { + Assert::True(fRooted, message); + } + else + { + Assert::False(fRooted, message); + } + + message = String::Format("HasLongPathPrefix: {0}", gcnew String(wzPath)); + if (fExpectedHasPrefix) + { + Assert::True(fHasLongPathPrefix, message); + } + else + { + Assert::False(fHasLongPathPrefix, message); + } + } + + void ValidateRootedPath(LPCWSTR wzPath, BOOL fExpected) + { + BOOL fRooted = PathIsRooted(wzPath); + String^ message = String::Format("IsRooted: {0}", gcnew String(wzPath)); + if (fExpected) + { + Assert::True(fRooted, message); + } + else + { + Assert::False(fRooted, message); + } + } }; } -- cgit v1.2.3-55-g6feb