#include #include // For wil::unique_hstring #include #ifdef WIL_ENABLE_EXCEPTIONS #include #endif // TODO: str_raw_ptr is not two-phase name lookup clean (https://github.com/Microsoft/wil/issues/8) namespace wil { PCWSTR str_raw_ptr(HSTRING); #ifdef WIL_ENABLE_EXCEPTIONS PCWSTR str_raw_ptr(const std::wstring&); #endif } #include #ifdef WIL_ENABLE_EXCEPTIONS #include // For std::wstring string_maker #endif #include "common.h" #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) bool DirectoryExists(_In_ PCWSTR path) { DWORD dwAttrib = GetFileAttributesW(path); return (dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)); } TEST_CASE("FileSystemTests::CreateDirectory", "[filesystem]") { wchar_t basePath[MAX_PATH]; REQUIRE(GetTempPathW(ARRAYSIZE(basePath), basePath)); REQUIRE_SUCCEEDED(PathCchAppend(basePath, ARRAYSIZE(basePath), L"FileSystemTests")); REQUIRE_FALSE(DirectoryExists(basePath)); REQUIRE(SUCCEEDED(wil::CreateDirectoryDeepNoThrow(basePath))); REQUIRE(DirectoryExists(basePath)); auto scopeGuard = wil::scope_exit([&] { REQUIRE_SUCCEEDED(wil::RemoveDirectoryRecursiveNoThrow(basePath)); }); PCWSTR relativeTestPath = L"folder1\\folder2\\folder3\\folder4\\folder5\\folder6\\folder7\\folder8"; wchar_t absoluteTestPath[MAX_PATH]; REQUIRE_SUCCEEDED(StringCchCopyW(absoluteTestPath, ARRAYSIZE(absoluteTestPath), basePath)); REQUIRE_SUCCEEDED(PathCchAppend(absoluteTestPath, ARRAYSIZE(absoluteTestPath), relativeTestPath)); REQUIRE_FALSE(DirectoryExists(absoluteTestPath)); REQUIRE_SUCCEEDED(wil::CreateDirectoryDeepNoThrow(absoluteTestPath)); PCWSTR invalidCharsPath = L"Bad?Char|"; wchar_t absoluteInvalidPath[MAX_PATH]; REQUIRE_SUCCEEDED(StringCchCopyW(absoluteInvalidPath, ARRAYSIZE(absoluteInvalidPath), basePath)); REQUIRE_SUCCEEDED(PathCchAppend(absoluteInvalidPath, ARRAYSIZE(absoluteInvalidPath), invalidCharsPath)); REQUIRE_FALSE(DirectoryExists(absoluteInvalidPath)); REQUIRE_FALSE(SUCCEEDED(wil::CreateDirectoryDeepNoThrow(absoluteInvalidPath))); PCWSTR testPath3 = L"folder1\\folder2\\folder3"; wchar_t absoluteTestPath3[MAX_PATH]; REQUIRE_SUCCEEDED(StringCchCopyW(absoluteTestPath3, ARRAYSIZE(absoluteTestPath3), basePath)); REQUIRE_SUCCEEDED(PathCchAppend(absoluteTestPath3, ARRAYSIZE(absoluteTestPath3), testPath3)); REQUIRE(DirectoryExists(absoluteTestPath3)); PCWSTR testPath4 = L"folder1\\folder2\\folder3\\folder4"; wchar_t absoluteTestPath4[MAX_PATH]; REQUIRE_SUCCEEDED(StringCchCopyW(absoluteTestPath4, ARRAYSIZE(absoluteTestPath4), basePath)); REQUIRE_SUCCEEDED(PathCchAppend(absoluteTestPath4, ARRAYSIZE(absoluteTestPath4), testPath4)); REQUIRE(DirectoryExists(absoluteTestPath4)); REQUIRE_SUCCEEDED(wil::RemoveDirectoryRecursiveNoThrow(absoluteTestPath3, wil::RemoveDirectoryOptions::KeepRootDirectory)); REQUIRE(DirectoryExists(absoluteTestPath3)); REQUIRE_FALSE(DirectoryExists(absoluteTestPath4)); } #ifdef WIL_ENABLE_EXCEPTIONS // Learn about the Win32 API normalization here: https://blogs.msdn.microsoft.com/jeremykuhne/2016/04/21/path-normalization/ // This test verifies the ability of RemoveDirectoryRecursive to be able to delete files // that are in the non-normalized form. TEST_CASE("FileSystemTests::VerifyRemoveDirectoryRecursiveCanDeleteFoldersWithNonNormalizedNames", "[filesystem]") { // Extended length paths can access files with non-normalized names. // This function creates a path with that ability. auto CreatePathThatCanAccessNonNormalizedNames = [](PCWSTR root, PCWSTR name) { wil::unique_hlocal_string path; THROW_IF_FAILED(PathAllocCombine(root, name, PATHCCH_DO_NOT_NORMALIZE_SEGMENTS | PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, &path)); REQUIRE(wil::is_extended_length_path(path.get())); return path; }; // Regular paths are normalized in the Win32 APIs thus can't address files in the non-normalized form. // This function creates a regular path form but preserves the non-normalized parts of the input (for testing) auto CreateRegularPath = [](PCWSTR root, PCWSTR name) { wil::unique_hlocal_string path; THROW_IF_FAILED(PathAllocCombine(root, name, PATHCCH_DO_NOT_NORMALIZE_SEGMENTS, &path)); REQUIRE_FALSE(wil::is_extended_length_path(path.get())); return path; }; struct TestCases { PCWSTR CreateWithName; PCWSTR DeleteWithName; wil::unique_hlocal_string (*CreatePathFunction)(PCWSTR root, PCWSTR name); HRESULT ExpectedResult; }; PCWSTR NormalizedName = L"Foo"; PCWSTR NonNormalizedName = L"Foo."; // The dot at the end is what makes this non-normalized. const auto PathNotFoundError = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); TestCases tests[] = { { NormalizedName, NormalizedName, CreateRegularPath, S_OK }, { NonNormalizedName, NormalizedName, CreateRegularPath, PathNotFoundError }, { NormalizedName, NonNormalizedName, CreateRegularPath, S_OK }, { NonNormalizedName, NonNormalizedName, CreateRegularPath, PathNotFoundError }, { NormalizedName, NormalizedName, CreatePathThatCanAccessNonNormalizedNames, S_OK }, { NonNormalizedName, NormalizedName, CreatePathThatCanAccessNonNormalizedNames, PathNotFoundError }, { NormalizedName, NonNormalizedName, CreatePathThatCanAccessNonNormalizedNames, PathNotFoundError }, { NonNormalizedName, NonNormalizedName, CreatePathThatCanAccessNonNormalizedNames, S_OK }, }; auto folderRoot = wil::ExpandEnvironmentStringsW(LR"(%TEMP%)"); REQUIRE_FALSE(wil::is_extended_length_path(folderRoot.get())); auto EnsureFolderWithNonCanonicalNameAndContentsExists = [&](const TestCases& test) { const auto enableNonNormalized = PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH | PATHCCH_DO_NOT_NORMALIZE_SEGMENTS; wil::unique_hlocal_string targetFolder; // Create a folder for testing using the extended length form to enable // access to non-normalized forms of the path THROW_IF_FAILED(PathAllocCombine(folderRoot.get(), test.CreateWithName, enableNonNormalized, &targetFolder)); // This ensures the folder is there and won't fail if it already exists (common when testing). wil::CreateDirectoryDeep(targetFolder.get()); // Create a file in that folder with a non-normalized name (with the dot at the end). wil::unique_hlocal_string extendedFilePath; THROW_IF_FAILED(PathAllocCombine(targetFolder.get(), L"NonNormalized.", enableNonNormalized, &extendedFilePath)); wil::unique_hfile fileHandle(CreateFileW(extendedFilePath.get(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); THROW_LAST_ERROR_IF(!fileHandle); }; for (auto const& test : tests) { // remove remnants from previous test that will cause failures wil::RemoveDirectoryRecursiveNoThrow(CreatePathThatCanAccessNonNormalizedNames(folderRoot.get(), NormalizedName).get()); wil::RemoveDirectoryRecursiveNoThrow(CreatePathThatCanAccessNonNormalizedNames(folderRoot.get(), NonNormalizedName).get()); EnsureFolderWithNonCanonicalNameAndContentsExists(test); auto deleteWithPath = test.CreatePathFunction(folderRoot.get(), test.DeleteWithName); const auto hr = wil::RemoveDirectoryRecursiveNoThrow(deleteWithPath.get()); REQUIRE(test.ExpectedResult == hr); } } #endif // real paths to test const wchar_t c_variablePath[] = L"%systemdrive%\\Windows\\System32\\Windows.Storage.dll"; const wchar_t c_expandedPath[] = L"c:\\Windows\\System32\\Windows.Storage.dll"; // // paths that should not exist on the system const wchar_t c_missingVariable[] = L"%doesnotexist%\\doesnotexist.dll"; const wchar_t c_missingPath[] = L"c:\\Windows\\System32\\doesnotexist.dll"; const int c_stackBufferLimitTest = 5; #ifdef WIL_ENABLE_EXCEPTIONS TEST_CASE("FileSystemTests::VerifyGetCurrentDirectory", "[filesystem]") { auto pwd = wil::GetCurrentDirectoryW(); REQUIRE(*pwd.get() != L'\0'); } TEST_CASE("FileSystemTests::VerifyGetFullPathName", "[filesystem]") { PCWSTR fileName = L"ReadMe.txt"; auto result = wil::GetFullPathNameW(fileName, nullptr); PCWSTR fileNameResult; result = wil::GetFullPathNameW(fileName, &fileNameResult); REQUIRE(wcscmp(fileName, fileNameResult) == 0); auto result2 = wil::GetFullPathNameW(fileName, &fileNameResult); REQUIRE(wcscmp(fileName, fileNameResult) == 0); REQUIRE(wcscmp(result.get(), result2.get()) == 0); // The only negative test case I've found is a path > 32k. std::wstring big(1024 * 32, L'a'); wil::unique_hstring output; auto hr = wil::GetFullPathNameW(big.c_str(), output, nullptr); REQUIRE(hr == HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE)); } TEST_CASE("FileSystemTests::VerifyGetFinalPathNameByHandle", "[filesystem]") { wil::unique_hfile fileHandle(CreateFileW(c_expandedPath, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); THROW_LAST_ERROR_IF(!fileHandle); auto name = wil::GetFinalPathNameByHandleW(fileHandle.get()); auto name2 = wil::GetFinalPathNameByHandleW(fileHandle.get()); REQUIRE(wcscmp(name.get(), name2.get()) == 0); std::wstring path; auto hr = wil::GetFinalPathNameByHandleW(nullptr, path); REQUIRE(hr == E_HANDLE); // should be a usage error so be a fail fast. // A more legitimate case is a non file handler like a drive volume. wil::unique_hfile volumeHandle(CreateFileW(LR"(\\?\C:)", FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); THROW_LAST_ERROR_IF(!volumeHandle); const auto hr2 = wil::GetFinalPathNameByHandleW(volumeHandle.get(), path); REQUIRE(hr2 == HRESULT_FROM_WIN32(ERROR_INVALID_FUNCTION)); } TEST_CASE("FileSystemTests::VerifyTrySearchPathW", "[filesystem]") { auto pathToTest = wil::TrySearchPathW(nullptr, c_expandedPath, nullptr); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); pathToTest = wil::TrySearchPathW(nullptr, c_missingPath, nullptr); REQUIRE(wil::string_get_not_null(pathToTest)[0] == L'\0'); } #endif // Simple test to expand an environmental string TEST_CASE("FileSystemTests::VerifyExpandEnvironmentStringsW", "[filesystem]") { wil::unique_cotaskmem_string pathToTest; REQUIRE_SUCCEEDED(wil::ExpandEnvironmentStringsW(c_variablePath, pathToTest)); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); // This should effectively be a no-op REQUIRE_SUCCEEDED(wil::ExpandEnvironmentStringsW(c_expandedPath, pathToTest)); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); // Environment variable does not exist, but the call should still succeed REQUIRE_SUCCEEDED(wil::ExpandEnvironmentStringsW(c_missingVariable, pathToTest)); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_missingVariable, -1, TRUE) == CSTR_EQUAL); } TEST_CASE("FileSystemTests::VerifySearchPathW", "[filesystem]") { wil::unique_cotaskmem_string pathToTest; REQUIRE_SUCCEEDED(wil::SearchPathW(nullptr, c_expandedPath, nullptr, pathToTest)); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); REQUIRE(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == wil::SearchPathW(nullptr, c_missingPath, nullptr, pathToTest)); } TEST_CASE("FileSystemTests::VerifyExpandEnvAndSearchPath", "[filesystem]") { wil::unique_cotaskmem_string pathToTest; REQUIRE_SUCCEEDED(wil::ExpandEnvAndSearchPath(c_variablePath, pathToTest)); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); // This test will exercise the case where AdaptFixedSizeToAllocatedResult will need to // reallocate the initial buffer to fit the final string. // This test is sufficient to test both wil::ExpandEnvironmentStringsW and wil::SeachPathW REQUIRE_SUCCEEDED((wil::ExpandEnvAndSearchPath(c_variablePath, pathToTest))); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, c_expandedPath, -1, TRUE) == CSTR_EQUAL); pathToTest.reset(); REQUIRE(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == wil::ExpandEnvAndSearchPath(c_missingVariable, pathToTest)); REQUIRE(pathToTest.get() == nullptr); } TEST_CASE("FileSystemTests::VerifyGetSystemDirectoryW", "[filesystem]") { wil::unique_cotaskmem_string pathToTest; REQUIRE_SUCCEEDED(wil::GetSystemDirectoryW(pathToTest)); // allocate based on the string that wil::GetSystemDirectoryW returned size_t length = wcslen(pathToTest.get()) + 1; auto trueSystemDir = wil::make_cotaskmem_string_nothrow(nullptr, length); REQUIRE(GetSystemDirectoryW(trueSystemDir.get(), static_cast(length)) > 0); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, trueSystemDir.get(), -1, TRUE) == CSTR_EQUAL); // Force AdaptFixed* to realloc. Test stack boundary with small initial buffer limit, c_stackBufferLimitTest REQUIRE_SUCCEEDED((wil::GetSystemDirectoryW(pathToTest))); // allocate based on the string that wil::GetSystemDirectoryW returned length = wcslen(pathToTest.get()) + 1; trueSystemDir = wil::make_cotaskmem_string_nothrow(nullptr, length); REQUIRE(GetSystemDirectoryW(trueSystemDir.get(), static_cast(length)) > 0); REQUIRE(CompareStringOrdinal(pathToTest.get(), -1, trueSystemDir.get(), -1, TRUE) == CSTR_EQUAL); } struct has_operator_pcwstr { PCWSTR value; operator PCWSTR() const { return value; } }; struct has_operator_pwstr { PWSTR value; operator PWSTR() const { return value; } }; #ifdef WIL_ENABLE_EXCEPTIONS struct has_operator_wstr_ref { std::wstring value; operator const std::wstring&() const { return value; } }; // E.g. mimics something like std::filesystem::path struct has_operator_wstr { std::wstring value; operator std::wstring() const { return value; } }; #endif TEST_CASE("FileSystemTests::VerifyStrConcat", "[filesystem]") { SECTION("Concat with multiple strings") { PCWSTR test1 = L"Test1"; #ifdef WIL_ENABLE_EXCEPTIONS std::wstring test2 = L"Test2"; #else PCWSTR test2 = L"Test2"; #endif WCHAR test3[6] = L"Test3"; wil::unique_cotaskmem_string test4 = wil::make_unique_string_nothrow(L"test4"); wil::unique_hstring test5 = wil::make_unique_string_nothrow(L"test5"); has_operator_pcwstr test6{ L"Test6" }; WCHAR test7Buffer[] = L"Test7"; has_operator_pwstr test7{ test7Buffer }; #ifdef WIL_ENABLE_EXCEPTIONS has_operator_wstr_ref test8{ L"Test8" }; has_operator_wstr test9{ L"Test9" }; #else PCWSTR test8 = L"Test8"; PCWSTR test9 = L"Test9"; #endif PCWSTR expectedStr = L"Test1Test2Test3Test4Test5Test6Test7Test8Test9"; #ifdef WIL_ENABLE_EXCEPTIONS auto combinedString = wil::str_concat(test1, test2, test3, test4, test5, test6, test7, test8, test9); REQUIRE(CompareStringOrdinal(combinedString.get(), -1, expectedStr, -1, TRUE) == CSTR_EQUAL); #endif wil::unique_cotaskmem_string combinedStringNT; REQUIRE_SUCCEEDED(wil::str_concat_nothrow(combinedStringNT, test1, test2, test3, test4, test5, test6, test7, test8, test9)); REQUIRE(CompareStringOrdinal(combinedStringNT.get(), -1, expectedStr, -1, TRUE) == CSTR_EQUAL); auto combinedStringFF = wil::str_concat_failfast(test1, test2, test3, test4, test5, test6, test7, test8, test9); REQUIRE(CompareStringOrdinal(combinedStringFF.get(), -1, expectedStr, -1, TRUE) == CSTR_EQUAL); } SECTION("Concat with single string") { PCWSTR test1 = L"Test1"; #ifdef WIL_ENABLE_EXCEPTIONS auto combinedString = wil::str_concat(test1); REQUIRE(CompareStringOrdinal(combinedString.get(), -1, test1, -1, TRUE) == CSTR_EQUAL); #endif wil::unique_cotaskmem_string combinedStringNT; REQUIRE_SUCCEEDED(wil::str_concat_nothrow(combinedStringNT, test1)); REQUIRE(CompareStringOrdinal(combinedStringNT.get(), -1, test1, -1, TRUE) == CSTR_EQUAL); auto combinedStringFF = wil::str_concat_failfast(test1); REQUIRE(CompareStringOrdinal(combinedStringFF.get(), -1, test1, -1, TRUE) == CSTR_EQUAL); } SECTION("Concat with existing string") { std::wstring test2 = L"Test2"; WCHAR test3[6] = L"Test3"; PCWSTR expectedStr = L"Test1Test2Test3"; wil::unique_cotaskmem_string combinedStringNT = wil::make_unique_string_nothrow(L"Test1"); REQUIRE_SUCCEEDED(wil::str_concat_nothrow(combinedStringNT, test2.c_str(), test3)); REQUIRE(CompareStringOrdinal(combinedStringNT.get(), -1, expectedStr, -1, TRUE) == CSTR_EQUAL); } } TEST_CASE("FileSystemTests::VerifyStrPrintf", "[filesystem]") { #ifdef WIL_ENABLE_EXCEPTIONS auto formattedString = wil::str_printf(L"Test %s %c %d %4.2f", L"String", L'c', 42, 6.28); REQUIRE(CompareStringOrdinal(formattedString.get(), -1, L"Test String c 42 6.28", -1, TRUE) == CSTR_EQUAL); #endif wil::unique_cotaskmem_string formattedStringNT; REQUIRE_SUCCEEDED(wil::str_printf_nothrow(formattedStringNT, L"Test %s %c %d %4.2f", L"String", L'c', 42, 6.28)); REQUIRE(CompareStringOrdinal(formattedStringNT.get(), -1, L"Test String c 42 6.28", -1, TRUE) == CSTR_EQUAL); auto formattedStringFF = wil::str_printf_failfast(L"Test %s %c %d %4.2f", L"String", L'c', 42, 6.28); REQUIRE(CompareStringOrdinal(formattedStringFF.get(), -1, L"Test String c 42 6.28", -1, TRUE) == CSTR_EQUAL); } TEST_CASE("FileSystemTests::VerifyGetModuleFileNameW", "[filesystem]") { wil::unique_cotaskmem_string path; REQUIRE_SUCCEEDED(wil::GetModuleFileNameW(nullptr, path)); auto len = wcslen(path.get()); REQUIRE(((len >= 4) && (wcscmp(path.get() + len - 4, L".exe") == 0))); // Call again, but force multiple retries through a small initial buffer wil::unique_cotaskmem_string path2; REQUIRE_SUCCEEDED((wil::GetModuleFileNameW(nullptr, path2))); REQUIRE(wcscmp(path.get(), path2.get()) == 0); REQUIRE_FAILED(wil::GetModuleFileNameW((HMODULE)INVALID_HANDLE_VALUE, path)); } TEST_CASE("FileSystemTests::VerifyGetModuleFileNameExW", "[filesystem]") { wil::unique_cotaskmem_string path; REQUIRE_SUCCEEDED(wil::GetModuleFileNameExW(nullptr, nullptr, path)); auto len = wcslen(path.get()); REQUIRE(((len >= 4) && (wcscmp(path.get() + len - 4, L".exe") == 0))); // Call again, but force multiple retries through a small initial buffer wil::unique_cotaskmem_string path2; REQUIRE_SUCCEEDED((wil::GetModuleFileNameExW(nullptr, nullptr, path2))); REQUIRE(wcscmp(path.get(), path2.get()) == 0); REQUIRE_FAILED(wil::GetModuleFileNameExW(nullptr, (HMODULE)INVALID_HANDLE_VALUE, path)); } #endif // WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)