diff --git a/toolsrc/include/vcpkg/base/files.h b/toolsrc/include/vcpkg/base/files.h index 08bbe790651463..71e386a5cf2e53 100644 --- a/toolsrc/include/vcpkg/base/files.h +++ b/toolsrc/include/vcpkg/base/files.h @@ -237,4 +237,8 @@ namespace vcpkg::Files /// Performs "lhs / rhs" according to the C++17 Filesystem Library Specification. /// This function exists as a workaround for TS implementations. fs::path combine(const fs::path& lhs, const fs::path& rhs); + +#if defined(_WIN32) + fs::path win32_fix_path_case(const fs::path& source); +#endif // _WIN32 } diff --git a/toolsrc/src/vcpkg-test/files.cpp b/toolsrc/src/vcpkg-test/files.cpp index a8c7c2ba2ff4f0..429a4ba15382e2 100644 --- a/toolsrc/src/vcpkg-test/files.cpp +++ b/toolsrc/src/vcpkg-test/files.cpp @@ -194,6 +194,62 @@ TEST_CASE ("remove all", "[files]") CHECK_EC_ON_FILE(temp_dir, ec); } +#if defined(_WIN32) +TEST_CASE ("win32_fix_path_case", "[files]") +{ + using vcpkg::Files::win32_fix_path_case; + + // This test assumes that the Windows directory is C:\Windows + + CHECK(win32_fix_path_case(L"") == L""); + + CHECK(win32_fix_path_case(L"C:") == L"C:"); + CHECK(win32_fix_path_case(L"c:") == L"C:"); + CHECK(win32_fix_path_case(L"C:/") == L"C:\\"); + CHECK(win32_fix_path_case(L"C:\\") == L"C:\\"); + CHECK(win32_fix_path_case(L"c:\\") == L"C:\\"); + CHECK(win32_fix_path_case(L"C:\\WiNdOws") == L"C:\\Windows"); + CHECK(win32_fix_path_case(L"c:\\WiNdOws\\") == L"C:\\Windows\\"); + CHECK(win32_fix_path_case(L"C://///////WiNdOws") == L"C:\\Windows"); + CHECK(win32_fix_path_case(L"c:\\/\\/WiNdOws\\/") == L"C:\\Windows\\"); + + auto& fs = vcpkg::Files::get_real_filesystem(); + auto original_cwd = fs.current_path(VCPKG_LINE_INFO); + fs.current_path(L"C:\\", VCPKG_LINE_INFO); + CHECK(win32_fix_path_case(L"\\") == L"\\"); + CHECK(win32_fix_path_case(L"\\/\\WiNdOws") == L"\\Windows"); + CHECK(win32_fix_path_case(L"\\WiNdOws") == L"\\Windows"); + CHECK(win32_fix_path_case(L"\\WiNdOws") == L"\\Windows"); + CHECK(win32_fix_path_case(L"c:WiNdOws") == L"C:Windows"); + CHECK(win32_fix_path_case(L"c:WiNdOws/system32") == L"C:Windows\\System32"); + fs.current_path(original_cwd, VCPKG_LINE_INFO); + + fs.create_directories("SuB/Dir/Ectory", VCPKG_LINE_INFO); + CHECK(win32_fix_path_case(L"sub") == L"SuB"); + CHECK(win32_fix_path_case(L"SUB") == L"SuB"); + CHECK(win32_fix_path_case(L"sub/") == L"SuB\\"); + CHECK(win32_fix_path_case(L"sub/dir") == L"SuB\\Dir"); + CHECK(win32_fix_path_case(L"sub/dir/") == L"SuB\\Dir\\"); + CHECK(win32_fix_path_case(L"sub/dir/ectory") == L"SuB\\Dir\\Ectory"); + CHECK(win32_fix_path_case(L"sub/dir/ectory/") == L"SuB\\Dir\\Ectory\\"); + fs.remove_all("SuB", VCPKG_LINE_INFO); + + CHECK(win32_fix_path_case(L"//nonexistent_server\\nonexistent_share\\") == + L"\\\\nonexistent_server\\nonexistent_share\\"); + CHECK(win32_fix_path_case(L"\\\\nonexistent_server\\nonexistent_share\\") == + L"\\\\nonexistent_server\\nonexistent_share\\"); + CHECK(win32_fix_path_case(L"\\\\nonexistent_server\\nonexistent_share") == + L"\\\\nonexistent_server\\nonexistent_share"); + + CHECK(win32_fix_path_case(L"///three_slashes_not_a_server\\subdir\\") == L"\\three_slashes_not_a_server\\subdir\\"); + + CHECK(win32_fix_path_case(L"\\??\\c:\\WiNdOws") == L"\\??\\c:\\WiNdOws"); + CHECK(win32_fix_path_case(L"\\\\?\\c:\\WiNdOws") == L"\\\\?\\c:\\WiNdOws"); + CHECK(win32_fix_path_case(L"\\\\.\\c:\\WiNdOws") == L"\\\\.\\c:\\WiNdOws"); + CHECK(win32_fix_path_case(L"c:\\/\\/Nonexistent\\/path/here") == L"C:\\Nonexistent\\path\\here"); +} +#endif // _WIN32 + #if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) TEST_CASE ("remove all -- benchmarks", "[files][!benchmark]") { diff --git a/toolsrc/src/vcpkg/base/files.cpp b/toolsrc/src/vcpkg/base/files.cpp index 51c6c9c763a7a7..916c26bdaa42a8 100644 --- a/toolsrc/src/vcpkg/base/files.cpp +++ b/toolsrc/src/vcpkg/base/files.cpp @@ -5,12 +5,14 @@ #include #include -#if !defined(_WIN32) +#if defined(_WIN32) +#include +#else // ^^^ _WIN32 // !_WIN32 vvv #include #include #include -#endif +#endif // _WIN32 #if defined(__linux__) #include @@ -18,6 +20,78 @@ #include #endif // ^^^ defined(__APPLE__) +#include +#include + +#if defined(_WIN32) +namespace +{ + struct IsSlash + { + bool operator()(const wchar_t c) const noexcept { return c == L'/' || c == L'\\'; } + }; + + constexpr IsSlash is_slash; + + template + bool wide_starts_with(const std::wstring& haystack, const wchar_t (&needle)[N]) noexcept + { + const size_t without_null = N - 1; + return haystack.size() >= without_null && std::equal(needle, needle + without_null, haystack.begin()); + } + + bool starts_with_drive_letter(std::wstring::const_iterator first, const std::wstring::const_iterator last) noexcept + { + if (last - first < 2) + { + return false; + } + + if (!(first[0] >= L'a' && first[0] <= L'z') && !(first[0] >= L'A' && first[0] <= L'Z')) + { + return false; + } + + if (first[1] != L':') + { + return false; + } + + return true; + } + + struct FindFirstOp + { + HANDLE h_find = INVALID_HANDLE_VALUE; + WIN32_FIND_DATAW find_data; + + unsigned long find_first(const wchar_t* const path) noexcept + { + assert(h_find == INVALID_HANDLE_VALUE); + h_find = FindFirstFileW(path, &find_data); + if (h_find == INVALID_HANDLE_VALUE) + { + return GetLastError(); + } + + return ERROR_SUCCESS; + } + + FindFirstOp() = default; + FindFirstOp(const FindFirstOp&) = delete; + FindFirstOp& operator=(const FindFirstOp&) = delete; + + ~FindFirstOp() + { + if (h_find != INVALID_HANDLE_VALUE) + { + (void)FindClose(h_find); + } + } + }; +} // unnamed namespace +#endif // _WIN32 + fs::path fs::u8path(vcpkg::StringView s) { #if defined(_WIN32) @@ -1197,4 +1271,106 @@ namespace vcpkg::Files #endif // ^^^ windows #endif // ^^^ std::experimental::filesystem } + +#ifdef _WIN32 + fs::path win32_fix_path_case(const fs::path& source) + { + const std::wstring& native = source.native(); + if (native.empty()) + { + return fs::path{}; + } + + if (wide_starts_with(native, L"\\\\?\\") || wide_starts_with(native, L"\\??\\") || + wide_starts_with(native, L"\\\\.\\")) + { + // no support to attempt to fix paths in the NT, \\GLOBAL??, or device namespaces at this time + return source; + } + + const auto last = native.end(); + auto first = native.begin(); + auto is_wildcard = [](wchar_t c) { return c == L'?' || c == L'*'; }; + if (std::any_of(first, last, is_wildcard)) + { + Checks::exit_with_message( + VCPKG_LINE_INFO, "Attempt to fix case of a path containing wildcards: %s", fs::u8string(source)); + } + + std::wstring in_progress; + in_progress.reserve(native.size()); + if (last - first >= 3 && is_slash(first[0]) && is_slash(first[1]) && !is_slash(first[2])) + { + // path with UNC prefix \\server\share; this will be rejected by FindFirstFile so we skip over that + in_progress.push_back(L'\\'); + in_progress.push_back(L'\\'); + first += 2; + auto next_slash = std::find_if(first, last, is_slash); + in_progress.append(first, next_slash); + in_progress.push_back(L'\\'); + first = std::find_if_not(next_slash, last, is_slash); + next_slash = std::find_if(first, last, is_slash); + in_progress.append(first, next_slash); + first = std::find_if_not(next_slash, last, is_slash); + if (first != next_slash) + { + in_progress.push_back(L'\\'); + } + } + else if (last - first >= 1 && is_slash(first[0])) + { + // root relative path + in_progress.push_back(L'\\'); + first = std::find_if_not(first, last, is_slash); + } + else if (starts_with_drive_letter(first, last)) + { + // path with drive letter root + auto letter = first[0]; + if (letter >= L'a' && letter <= L'z') + { + letter = letter - L'a' + L'A'; + } + + in_progress.push_back(letter); + in_progress.push_back(L':'); + first += 2; + if (first != last && is_slash(*first)) + { + // absolute path + in_progress.push_back(L'\\'); + first = std::find_if_not(first, last, is_slash); + } + } + + assert(!fs::path(first, last).has_root_path()); + + while (first != last) + { + auto next_slash = std::find_if(first, last, is_slash); + auto original_size = in_progress.size(); + in_progress.append(first, next_slash); + FindFirstOp this_find; + unsigned long last_error = this_find.find_first(in_progress.c_str()); + if (last_error == ERROR_SUCCESS) + { + in_progress.resize(original_size); + in_progress.append(this_find.find_data.cFileName); + } + else + { + // we might not have access to this intermediate part of the path; + // just guess that the case of that element is correct and move on + } + + first = std::find_if_not(next_slash, last, is_slash); + if (first != next_slash) + { + in_progress.push_back(L'\\'); + } + } + + return fs::path(std::move(in_progress)); + } +#endif // _WIN32 } diff --git a/toolsrc/src/vcpkg/vcpkgpaths.cpp b/toolsrc/src/vcpkg/vcpkgpaths.cpp index 9f4ffce6b0d1bf..60403edbd1dcc2 100644 --- a/toolsrc/src/vcpkg/vcpkgpaths.cpp +++ b/toolsrc/src/vcpkg/vcpkgpaths.cpp @@ -62,24 +62,13 @@ namespace Files::Filesystem& filesystem, const fs::path& root, std::string* option, StringLiteral name, LineInfo li) { auto result = process_output_directory_impl(filesystem, root, option, name, li); +#if defined(_WIN32) + result = vcpkg::Files::win32_fix_path_case(result); +#endif // _WIN32 Debug::print("Using ", name, "-root: ", fs::u8string(result), '\n'); return result; } - void uppercase_win32_drive_letter(fs::path& path) - { -#if defined(_WIN32) - const auto& nativePath = path.native(); - if (nativePath.size() > 2 && (nativePath[0] >= L'a' && nativePath[0] <= L'z') && nativePath[1] == L':') - { - auto uppercaseFirstLetter = std::move(path).native(); - uppercaseFirstLetter[0] = nativePath[0] - L'a' + L'A'; - path = uppercaseFirstLetter; - } -#endif - (void)path; - } - } // unnamed namespace namespace vcpkg @@ -225,6 +214,10 @@ namespace vcpkg : m_pimpl(std::make_unique(filesystem, args.compiler_tracking_enabled())) { original_cwd = filesystem.current_path(VCPKG_LINE_INFO); +#if defined(_WIN32) + original_cwd = vcpkg::Files::win32_fix_path_case(original_cwd); +#endif // _WIN32 + if (args.vcpkg_root_dir) { root = filesystem.canonical(VCPKG_LINE_INFO, fs::u8path(*args.vcpkg_root_dir)); @@ -238,7 +231,7 @@ namespace vcpkg filesystem.canonical(VCPKG_LINE_INFO, System::get_exe_path_of_current_process()), ".vcpkg-root"); } } - uppercase_win32_drive_letter(root); + Checks::check_exit(VCPKG_LINE_INFO, !root.empty(), "Error: Could not detect vcpkg-root."); Debug::print("Using vcpkg-root: ", fs::u8string(root), '\n'); @@ -252,7 +245,6 @@ namespace vcpkg { manifest_root_dir = filesystem.find_file_recursively_up(original_cwd, fs::u8path("vcpkg.json")); } - uppercase_win32_drive_letter(manifest_root_dir); if (!manifest_root_dir.empty() && manifest_mode_on) {