diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index afb1acb118..727c548432 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -533,6 +533,7 @@ quick_lint_js_add_library( quick-lint-js/fe/language-debug.cpp quick-lint-js/fe/lex-debug.cpp quick-lint-js/i18n/po-parser-debug.cpp + quick-lint-js/io/file-path-debug.cpp quick-lint-js/lsp/lsp-location-debug.cpp quick-lint-js/port/char8-debug.cpp ) diff --git a/src/quick-lint-js/io/file-canonical.cpp b/src/quick-lint-js/io/file-canonical.cpp index 9eee6132d9..52134ea3a9 100644 --- a/src/quick-lint-js/io/file-canonical.cpp +++ b/src/quick-lint-js/io/file-canonical.cpp @@ -425,6 +425,9 @@ class Path_Canonicalizer_Base { path_to_process_ = path_to_process_.substr(next_component_index); } + // TODO(strager): Have canonicalize_path accept an allocator. + Monotonic_Allocator allocator_{"Path_Canonicalizer_Base"}; + Canonicalize_Observer *observer_; Path_String_View original_path_; @@ -583,51 +586,15 @@ class Windows_Path_Canonicalizer quick_lint_js::Result process_start_of_path() { - std::wstring temp(path_to_process_); - - // The PathCch functions only support '\' as a directory separator. Convert - // all '/'s into '\'s. - for (wchar_t &c : temp) { - if (c == L'/') { - c = L'\\'; - } - } - - wchar_t *root_end; - HRESULT result = ::PathCchSkipRoot(temp.data(), &root_end); - switch (result) { - case S_OK: - // Path is absolute. - QLJS_ASSERT(root_end != temp.data()); - - path_to_process_ = path_to_process_.substr(root_end - temp.data()); - skip_to_next_component(); - - // Drop '\' from 'C:\' if present. - if (root_end[-1] == L'\\') { - --root_end; - } - canonical_.assign(temp.data(), root_end); - - need_root_slash_ = true; - break; - - case HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER): { - // Path is invalid or is relative. Assume that it is relative. - quick_lint_js::Result r = load_cwd(); - if (!r.ok()) { - return failed_result(Canonicalizing_Path_IO_Error{ - .canonicalizing_path = Path_String(this->path_to_process_), - .io_error = r.error(), - }); - } - break; - } - - default: - QLJS_UNIMPLEMENTED(); - break; - } + // @@@ what about empty path? + + // FIXME(strager): Do we need to copy (std::wstring) to add the null + // terminator? + Simplified_Path simplified_path = simplify_path_and_make_absolute( + &this->allocator_, std::wstring(path_to_process_).c_str()); + this->canonical_ = simplified_path.root; + this->path_to_process_ = simplified_path.relative; + this->need_root_slash_ = true; return {}; } diff --git a/src/quick-lint-js/io/file-path-debug.cpp b/src/quick-lint-js/io/file-path-debug.cpp new file mode 100644 index 0000000000..8a4da6f4a2 --- /dev/null +++ b/src/quick-lint-js/io/file-path-debug.cpp @@ -0,0 +1,42 @@ +// Copyright (C) 2020 Matthew "strager" Glazar +// See end of file for extended copyright information. + +#include +#include +#include +#include + +namespace quick_lint_js { +#if defined(_WIN32) +std::ostream& operator<<(std::ostream& out, Simplified_Path path) { + auto write_field = [&](const char* name, std::wstring_view s) -> void { + out << " ." << name << " = \"" << wstring_to_mbstring(s).value() + << "\",\n"; + }; + out << "Simplified_Path{\n"; + write_field("full_path", path.full_path); + write_field("root", path.root); + write_field("relative", path.relative); + out << "}"; + return out; +} +#endif +} + +// quick-lint-js finds bugs in JavaScript programs. +// Copyright (C) 2020 Matthew "strager" Glazar +// +// This file is part of quick-lint-js. +// +// quick-lint-js is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// quick-lint-js is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with quick-lint-js. If not, see . diff --git a/src/quick-lint-js/io/file-path.cpp b/src/quick-lint-js/io/file-path.cpp index 0340799cda..391a1e866f 100644 --- a/src/quick-lint-js/io/file-path.cpp +++ b/src/quick-lint-js/io/file-path.cpp @@ -2,6 +2,7 @@ // See end of file for extended copyright information. #include +#include #include #include #include @@ -160,6 +161,77 @@ std::string_view path_file_name(std::string_view path) { } return path.substr(last_slash_index + 1); } + +#if defined(_WIN32) +Simplified_Path simplify_path_and_make_absolute(Monotonic_Allocator* allocator, + const wchar_t* path) { + Span absolute_path_buffer; + if (path[0] == L'\\' && path[1] == L'\\' && path[2] == L'?' && + path[3] == L'\\') { + // ::GetFullPathNameW mangles \\?\ paths, but we want \\?\ paths to be + // untouched. Also, ::PathCchSkipRoot treats \ and / the same, but they + // differ for \\?\ paths. Handle \\?\ paths specially. + absolute_path_buffer = allocator->new_objects_copy( + Span(path, std::wcslen(path) + 1)); + + const wchar_t* root_end = std::find(absolute_path_buffer.begin() + 4, + absolute_path_buffer.end() - 1, L'\\'); + + const wchar_t* relative_start = + *root_end == L'\\' ? root_end + 1 : root_end; + + return Simplified_Path{ + .full_path = absolute_path_buffer.data(), + .root = make_string_view(absolute_path_buffer.data(), root_end), + .relative = + make_string_view(relative_start, absolute_path_buffer.end() - 1), + }; + } + + if (path[0] == L'\0') { + // ::GetFullPathNameW returns 0 if path is empty, causing us to + // underallocate. ::PathCchSkipRoot also fails if path is empty. Avoid + // problems by special-casing empty inputs. + Span full_path = allocator->new_objects_copy(Span({L'\0'})); + return Simplified_Path{ + .full_path = full_path.data(), + .root = std::wstring_view(), + .relative = std::wstring_view(), + }; + } + + ::DWORD absolute_path_buffer_size = + ::GetFullPathNameW(path, 0, nullptr, nullptr); + QLJS_ALWAYS_ASSERT(absolute_path_buffer_size > 0); + absolute_path_buffer = allocator->allocate_uninitialized_span( + absolute_path_buffer_size); + ::DWORD absolute_path_length = ::GetFullPathNameW( + path, absolute_path_buffer_size, absolute_path_buffer.data(), nullptr); + QLJS_ALWAYS_ASSERT(absolute_path_length < absolute_path_buffer_size); + QLJS_ALWAYS_ASSERT(absolute_path_buffer[absolute_path_length] == L'\0'); + absolute_path_buffer = + absolute_path_buffer.subspan(0, absolute_path_length + 1); + + const wchar_t* relative_start; + ::HRESULT result = + ::PathCchSkipRoot(absolute_path_buffer.data(), &relative_start); + if (result != S_OK) { + QLJS_UNIMPLEMENTED(); + } + const wchar_t* root_end = relative_start; + if (root_end != path && root_end[-1] == L'\\') { + // Don't include the trailing '\'. + root_end -= 1; + } + + return Simplified_Path{ + .full_path = absolute_path_buffer.data(), + .root = make_string_view(absolute_path_buffer.data(), root_end), + .relative = + make_string_view(relative_start, absolute_path_buffer.end() - 1), + }; +} +#endif } // quick-lint-js finds bugs in JavaScript programs. diff --git a/src/quick-lint-js/io/file-path.h b/src/quick-lint-js/io/file-path.h index 9747fd716a..4accc479f9 100644 --- a/src/quick-lint-js/io/file-path.h +++ b/src/quick-lint-js/io/file-path.h @@ -3,9 +3,12 @@ #pragma once +#include +#include #include #include #include +#include #if defined(_WIN32) #define QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "\\" @@ -22,6 +25,42 @@ namespace quick_lint_js { std::string parent_path(std::string&&); std::string_view path_file_name(std::string_view); + +#if defined(_WIN32) +struct Simplified_Path { + // Null-terminated absolute path. + wchar_t* full_path; + + // Root portion of the path. Substring of full_path. Does not contain a + // trailing '\'. + std::wstring_view root; + + // Relative portion of the path. Substring of full_path. Does not start with a + // leading '\'. + std::wstring_view relative; + + friend std::ostream& operator<<(std::ostream&, Simplified_Path); +}; + +// Simplify (resolve '.' and '..') and make the path absolute (based on the +// current working directories). +// +// This function should not change the path according to ::CreateFileW and other +// Win32 APIs. +// +// * Preserves at most one trailing '\'. +// * Combines redundant '\' characters. +// * Expands relative paths into absolute paths using the process's current +// working directory and the process's per-drive working directories. +// * Does not resolve symlinks, junctions, shortcuts, etc. +// * Does not check validity of the path. +// * Does not check for existence of directories and files in the path. +// * Does not convert 8.3 names into long names. +// +// Returns pointers into memory allocated by 'allocator'. +Simplified_Path simplify_path_and_make_absolute(Monotonic_Allocator* allocator, + const wchar_t* path); +#endif } // quick-lint-js finds bugs in JavaScript programs. diff --git a/src/quick-lint-js/port/span.h b/src/quick-lint-js/port/span.h index 2000b5bef9..8dcbedf213 100644 --- a/src/quick-lint-js/port/span.h +++ b/src/quick-lint-js/port/span.h @@ -76,6 +76,15 @@ class Span { bool empty() const { return this->size() == 0; } + Span subspan(Span_Size offset, Span_Size count) { + // TODO(strager): Be lax with offset and count. + QLJS_ASSERT(offset >= 0); + QLJS_ASSERT(offset <= this->size()); + QLJS_ASSERT(count >= 0); + QLJS_ASSERT(count + offset <= this->size()); + return Span(this->begin() + offset, this->begin() + offset + count); + } + friend bool operator==(Span lhs, Span rhs) { return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); } diff --git a/test/test-file-canonical.cpp b/test/test-file-canonical.cpp index 363ee78b18..2b994425aa 100644 --- a/test/test-file-canonical.cpp +++ b/test/test-file-canonical.cpp @@ -27,10 +27,10 @@ #include #if defined(_WIN32) -// TODO(strager): This should be 1, not 0. Windows allows you to access -// 'c:\file.txt\.', for example. -#define QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS 0 +// Windows allows you to access 'c:\file.txt\.', for example. +#define QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS 1 #else +// POSIX does not allow you to access '/file.txt/.'. #define QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS 0 #endif @@ -262,7 +262,7 @@ TEST_F(Test_File_Canonical, canonical_path_removes_trailing_dot_component) { } TEST_F(Test_File_Canonical, - canonical_path_fails_with_dot_component_after_regular_file) { + canonical_path_fails_with_dot_and_component_after_regular_file) { std::string temp_dir = this->make_temporary_directory(); write_file_or_exit(temp_dir + "/just-a-file", u8""_sv); @@ -281,6 +281,36 @@ TEST_F(Test_File_Canonical, #endif } +#if QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS +TEST_F(Test_File_Canonical, canonical_path_removes_dot_after_regular_file) { + std::string temp_dir = this->make_temporary_directory(); + write_file_or_exit(temp_dir + "/just-a-file", u8""_sv); + + std::string input_path = temp_dir + "/just-a-file/."; + Result canonical = + canonicalize_path(input_path); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("/."))); + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("\\."))); + EXPECT_SAME_FILE(canonical->path(), input_path); +} +#else +TEST_F(Test_File_Canonical, canonical_path_fails_with_dot_after_regular_file) { + std::string temp_dir = this->make_temporary_directory(); + write_file_or_exit(temp_dir + "/just-a-file", u8""_sv); + + std::string input_path = temp_dir + "/just-a-file/."; + Result canonical = + canonicalize_path(input_path); + ASSERT_FALSE(canonical.ok()); + EXPECT_EQ(canonical.error().input_path, input_path); + EXPECT_THAT(canonical.error().canonicalizing_path, + ::testing::EndsWith("just-a-file")); + EXPECT_EQ(canonical.error().io_error.error, ENOTDIR); +} +#endif + TEST_F(Test_File_Canonical, canonical_path_removes_dot_components_after_missing_path) { std::string temp_dir = this->make_temporary_directory(); @@ -301,28 +331,42 @@ TEST_F(Test_File_Canonical, EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\.\\"))); } -// TODO(strager): This test is wrong if -// QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS. +#if QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS +TEST_F(Test_File_Canonical, + canonical_path_simplifies_dot_dot_component_after_regular_file) { + std::string temp_dir = this->make_temporary_directory(); + write_file_or_exit(temp_dir + "/just-a-file", u8""_sv); + write_file_or_exit(temp_dir + "/other.txt", u8""_sv); + + std::string input_path = temp_dir + "/just-a-file/../other.txt"; + Result canonical = + canonicalize_path(input_path); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/../"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\..\\"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/just-a-file/"))); + EXPECT_THAT(std::string(canonical->path()), + Not(HasSubstr("\\just-a-file\\"))); + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/other.txt"); +} +#else TEST_F(Test_File_Canonical, canonical_path_fails_with_dot_dot_component_after_regular_file) { std::string temp_dir = this->make_temporary_directory(); write_file_or_exit(temp_dir + "/just-a-file", u8""_sv); write_file_or_exit(temp_dir + "/other.txt", u8""_sv); - std::string input_path = temp_dir + "/just-a-file/../other.text"; + std::string input_path = temp_dir + "/just-a-file/../other.txt"; Result canonical = canonicalize_path(input_path); ASSERT_FALSE(canonical.ok()); EXPECT_EQ(canonical.error().input_path, input_path); EXPECT_THAT(canonical.error().canonicalizing_path, ::testing::EndsWith("just-a-file")); -#if QLJS_HAVE_WINDOWS_H - EXPECT_EQ(canonical.error().io_error.error, ERROR_DIRECTORY); -#endif -#if QLJS_HAVE_UNISTD_H EXPECT_EQ(canonical.error().io_error.error, ENOTDIR); -#endif } +#endif #if QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS TEST_F(Test_File_Canonical, @@ -335,8 +379,8 @@ TEST_F(Test_File_Canonical, canonicalize_path(input_path); ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); - EXPECT_FALSE(ends_with(canonical->path(), "/..")) << canonical.path(); - EXPECT_FALSE(ends_with(canonical->path(), "\\..")) << canonical.path(); + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("/."))); + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("\\."))); EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("fake-subdir"))); EXPECT_SAME_FILE(canonical->path(), temp_dir + "/just-a-file"); } @@ -353,12 +397,7 @@ TEST_F(Test_File_Canonical, EXPECT_EQ(canonical.error().input_path, input_path); EXPECT_THAT(canonical.error().canonicalizing_path, ::testing::EndsWith("just-a-file")); -#if QLJS_HAVE_WINDOWS_H - EXPECT_EQ(canonical.error().io_error.error, ERROR_DIRECTORY); -#endif -#if QLJS_HAVE_UNISTD_H EXPECT_EQ(canonical.error().io_error.error, ENOTDIR); -#endif } #endif @@ -372,8 +411,8 @@ TEST_F(Test_File_Canonical, canonical_path_with_dot_after_regular_file) { canonicalize_path(input_path); ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); - EXPECT_FALSE(ends_with(canonical->path(), "/.")) << canonical.path(); - EXPECT_FALSE(ends_with(canonical->path(), "\\.")) << canonical.path(); + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("/."))); + EXPECT_THAT(std::string(canonical->path()), Not(::testing::EndsWith("\\."))); EXPECT_SAME_FILE(canonical->path(), temp_dir + "/just-a-file"); } #else @@ -389,12 +428,7 @@ TEST_F(Test_File_Canonical, EXPECT_EQ(canonical.error().input_path, input_path); EXPECT_THAT(canonical.error().canonicalizing_path, ::testing::EndsWith("just-a-file")); -#if QLJS_HAVE_WINDOWS_H - EXPECT_EQ(canonical.error().io_error.error, ERROR_DIRECTORY); -#endif -#if QLJS_HAVE_UNISTD_H EXPECT_EQ(canonical.error().io_error.error, ENOTDIR); -#endif } #endif @@ -518,6 +552,23 @@ TEST_F(Test_File_Canonical, } } +#if QLJS_FILE_PATH_ALLOWS_FOLLOWING_COMPONENTS +TEST_F(Test_File_Canonical, + canonical_path_folds_dot_dot_components_after_missing_path) { + std::string temp_dir = this->make_temporary_directory(); + Result temp_dir_canonical = + canonicalize_path(temp_dir); + ASSERT_TRUE(temp_dir_canonical.ok()) + << temp_dir_canonical.error().to_string(); + write_file_or_exit(temp_dir + "/real.js", u8""_sv); + + Result canonical = + canonicalize_path(temp_dir + "/does-not-exist/../real.js"); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/real.js"); +} +#else TEST_F(Test_File_Canonical, canonical_path_keeps_dot_dot_components_after_missing_path) { std::string temp_dir = this->make_temporary_directory(); @@ -538,6 +589,7 @@ TEST_F(Test_File_Canonical, ".." QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR + "real.js"); EXPECT_TRUE(canonical->have_missing_components()); } +#endif TEST_F(Test_File_Canonical, canonical_path_makes_relative_paths_absolute) { std::string temp_dir = this->make_temporary_directory(); diff --git a/test/test-file-path.cpp b/test/test-file-path.cpp index dbe03558d3..903636153c 100644 --- a/test/test-file-path.cpp +++ b/test/test-file-path.cpp @@ -3,8 +3,12 @@ #include #include +#include +#include #include +#include #include +#include #if QLJS_HAVE_UNISTD_H #include @@ -14,9 +18,14 @@ using ::testing::AnyOf; namespace quick_lint_js { namespace { +class Test_File_Path : public ::testing::Test, public Filesystem_Test { + public: + Monotonic_Allocator allocator_{"test"}; +}; + #if defined(QLJS_HAVE_UNISTD_H) && defined(_POSIX_VERSION) && \ _POSIX_VERSION >= 200112L -TEST(Test_File_Path, parent_path_posix) { +TEST_F(Test_File_Path, parent_path_posix) { EXPECT_EQ(parent_path("x/y"), "x"); EXPECT_EQ(parent_path("x/y/z"), "x/y"); @@ -47,7 +56,7 @@ TEST(Test_File_Path, parent_path_posix) { << "// is implementation-defined"; } -TEST(Test_File_Path, path_file_name_posix) { +TEST_F(Test_File_Path, path_file_name_posix) { EXPECT_EQ(path_file_name(""), ""); EXPECT_EQ(path_file_name("x"), "x"); @@ -79,7 +88,7 @@ TEST(Test_File_Path, path_file_name_posix) { #endif #if defined(_WIN32) -TEST(Test_File_Path, parent_path_windows) { +TEST_F(Test_File_Path, parent_path_windows) { EXPECT_EQ(parent_path(R"(x/y)"), R"(x)"); EXPECT_EQ(parent_path(R"(x/y/z)"), R"(x/y)"); EXPECT_EQ(parent_path(R"(x\y)"), R"(x)"); @@ -160,7 +169,7 @@ TEST(Test_File_Path, parent_path_windows) { // TODO(strager): Test \\?\UNC\host\share paths. } -TEST(Test_File_Path, path_file_name_windows) { +TEST_F(Test_File_Path, path_file_name_windows) { EXPECT_EQ(path_file_name(""), ""); EXPECT_EQ(path_file_name(R"(x)"), "x"); @@ -239,6 +248,279 @@ TEST(Test_File_Path, path_file_name_windows) { // TODO(strager): Test \\?\UNC\host\share paths. } #endif + +#if defined(_WIN32) +struct Simplified_Path_Assertion { + const wchar_t* full_path; + const wchar_t* root; + const wchar_t* relative; + + friend bool operator==(Simplified_Path lhs, Simplified_Path_Assertion rhs) { + if (rhs.full_path != nullptr && + std::wstring_view(lhs.full_path) != std::wstring_view(rhs.full_path)) { + return false; + } + if (rhs.root != nullptr && lhs.root != rhs.root) { + return false; + } + if (rhs.relative != nullptr && lhs.relative != rhs.relative) { + return false; + } + return true; + } + + friend std::ostream& operator<<(std::ostream& out, + Simplified_Path_Assertion assertion) { + auto maybe_write_field = [&](const char* name, const wchar_t* s) -> void { + if (s != nullptr) { + out << " ." << name << " = \"" << wstring_to_mbstring(s).value() + << "\",\n"; + } + }; + out << "Simplified_Path{\n"; + maybe_write_field("full_path", assertion.full_path); + maybe_write_field("root", assertion.root); + maybe_write_field("relative", assertion.relative); + out << "}"; + return out; + } +}; +#endif + +#if defined(_WIN32) +#define CHECK_SIMPLIFY(input_path, expected_full_path, expected_root, \ + expected_relative) \ + EXPECT_EQ(simplify_path_and_make_absolute(&this->allocator_, input_path), \ + (Simplified_Path_Assertion{ \ + .full_path = expected_full_path, \ + .root = expected_root, \ + .relative = expected_relative, \ + })) + +TEST_F(Test_File_Path, simplify_path_and_make_absolute_empty_path) { + CHECK_SIMPLIFY(L"", L"", L"", L""); +} + +TEST_F(Test_File_Path, simplify_path_and_make_absolute_already_absolute) { + // clang-format off + // Path splitting with no modifications: + CHECK_SIMPLIFY(LR"(C:\)", LR"(C:\)", L"C:", L""); + CHECK_SIMPLIFY(LR"(C:\a\b)", LR"(C:\a\b)", L"C:", LR"(a\b)"); + CHECK_SIMPLIFY(LR"(C:\a\b\)", LR"(C:\a\b\)", L"C:", LR"(a\b\)"); + + CHECK_SIMPLIFY(LR"(\\?\C:\)", LR"(\\?\C:\)", LR"(\\?\C:)", L""); + CHECK_SIMPLIFY(LR"(\\?\C:\a\b)", LR"(\\?\C:\a\b)", LR"(\\?\C:)", LR"(a\b)"); + CHECK_SIMPLIFY(LR"(\\?\C:\a\b\)", LR"(\\?\C:\a\b\)", LR"(\\?\C:)", LR"(a\b\)"); + + CHECK_SIMPLIFY(LR"(\\server\share)", LR"(\\server\share)", LR"(\\server\share)", L""); + CHECK_SIMPLIFY(LR"(\\server\share\)", LR"(\\server\share\)", LR"(\\server\share)", L""); + CHECK_SIMPLIFY(LR"(\\server\share\file.txt)", LR"(\\server\share\file.txt)", LR"(\\server\share)", L"file.txt"); + + // TODO(strager): Handle \\?\UNC\ paths correctly. + // CHECK_SIMPLIFY(LR"(\\?\UNC\server\share)", LR"(\\?\UNC\server\share)", LR"(\\?\UNC\server\share)", L""); + // CHECK_SIMPLIFY(LR"(\\?\UNC\server\share\)", LR"(\\?\UNC\server\share\)", LR"(\\?\UNC\server\share)", L""); + // CHECK_SIMPLIFY(LR"(\\?\UNC\server\share\file.txt)", LR"(\\?\UNC\server\share\file.txt)", LR"(\\?\UNC\server\share)", L"file.txt"); + + CHECK_SIMPLIFY(LR"(\\.\device)", LR"(\\.\device)", LR"(\\.\device)", L""); + CHECK_SIMPLIFY(LR"(\\.\device\)", LR"(\\.\device\)", LR"(\\.\device)", L""); + + // '.' components are dropped: + CHECK_SIMPLIFY(LR"(C:\a\.\b)", LR"(C:\a\b)", L"C:", LR"(a\b)"); + CHECK_SIMPLIFY(LR"(C:\a\b\.)", LR"(C:\a\b)", L"C:", LR"(a\b)"); + CHECK_SIMPLIFY(LR"(C:\a\b\.\)", LR"(C:\a\b\)", L"C:", LR"(a\b\)"); + + CHECK_SIMPLIFY(LR"(\\?/C:\a\.\b)", LR"(\\?\C:\a\b)", LR"(\\?\C:)", LR"(a\b)"); + + // '..' components are resolved: + CHECK_SIMPLIFY(LR"(C:\a\..\b)", LR"(C:\b)", L"C:", L"b"); + CHECK_SIMPLIFY(LR"(C:\a\b\..)", LR"(C:\a)", L"C:", L"a"); + CHECK_SIMPLIFY(LR"(C:\a\b\..\)", LR"(C:\a\)", L"C:", LR"(a\)"); + CHECK_SIMPLIFY(LR"(C:\a\b\..\..)", LR"(C:\)", L"C:", L""); + + CHECK_SIMPLIFY(LR"(\\?/C:\a\..\b)", LR"(\\?\C:\b)", LR"(\\?\C:)", L"b"); + + // Redundant '\'s are merged: + CHECK_SIMPLIFY(LR"(C:\\a\\b\\)", LR"(C:\a\b\)", L"C:", LR"(a\b\)"); + CHECK_SIMPLIFY(LR"(C:\a\\\\\\b)", LR"(C:\a\b)", L"C:", LR"(a\b)"); + + CHECK_SIMPLIFY(LR"(\\server\\share)", LR"(\\server\share)", LR"(\\server\share)", L""); + + CHECK_SIMPLIFY(LR"(\\?/C:\\a\\b)", LR"(\\?\C:\a\b)", LR"(\\?\C:)", LR"(a\b)"); + + // '/' is converted to '\': + CHECK_SIMPLIFY(LR"(C:/)", LR"(C:\)", L"C:", L""); + CHECK_SIMPLIFY(LR"(C:/a/b)", LR"(C:\a\b)", L"C:", LR"(a\b)"); + CHECK_SIMPLIFY(LR"(C:/a/b/)", LR"(C:\a\b\)", L"C:", LR"(a\b\)"); + + CHECK_SIMPLIFY(LR"(\\server\share)", LR"(\\server\share)", LR"(\\server\share)", L""); + CHECK_SIMPLIFY(LR"(\\server\share/)", LR"(\\server\share\)", LR"(\\server\share)", L""); + CHECK_SIMPLIFY(LR"(\\server\share/a/b)", LR"(\\server\share\a\b)", LR"(\\server\share)", LR"(a\b)"); + + CHECK_SIMPLIFY(LR"(\\?/C:/a/b)", LR"(\\?\C:\a\b)", LR"(\\?\C:)", LR"(a\b)"); + + // '.' is not resolved for \\?\ paths: + CHECK_SIMPLIFY(LR"(\\?\C:\a\.\b)", LR"(\\?\C:\a\.\b)", LR"(\\?\C:)", LR"(a\.\b)"); + + // '..' is not resolved for \\?\ paths: + CHECK_SIMPLIFY(LR"(\\?\C:\a\b\..)", LR"(\\?\C:\a\b\..)", LR"(\\?\C:)", LR"(a\b\..)"); + + // '/' is not converted to '\' for \\?\ paths: + CHECK_SIMPLIFY(LR"(\\?\C:/a/b\c/d/)", LR"(\\?\C:/a/b\c/d/)", LR"(\\?\C:/a/b)", LR"(c/d/)"); + + // clang-format on +} + +struct CWD_Parts { + // Example: + // cwd: L"C:\\projects\\dir" + // root: L"C:" + // relative: L"projects\\dir" + std::wstring cwd; + std::wstring root; + std::wstring relative; +}; + +CWD_Parts get_cwd_parts() { + std::wstring cwd; + if (!get_current_working_directory(cwd).ok()) { + QLJS_UNIMPLEMENTED(); + } + SCOPED_TRACE(cwd); + + // Assumption: cwd has the form 'C:\' or 'C:\dir\subdir'. + EXPECT_GE(cwd.size(), 3); + EXPECT_EQ(cwd[1], L':'); + EXPECT_EQ(cwd[2], L'\\'); + if (cwd.size() > 3) { + EXPECT_NE(cwd.back(), L'\\'); + } + + return CWD_Parts{ + .cwd = cwd, + .root = cwd.substr(0, 2), // "C:" + .relative = cwd.substr(3), // Skip "C:\". + }; +} + +TEST_F(Test_File_Path, simplify_path_and_make_absolute_cwd_relative) { + CWD_Parts cwd = get_cwd_parts(); + + // clang-format off + // Basic path is appended to cwd: + CHECK_SIMPLIFY(L"hello.txt", (cwd.cwd + LR"(\hello.txt)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\hello.txt)").c_str()); + CHECK_SIMPLIFY(LR"(dir\file)", (cwd.cwd + LR"(\dir\file)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\dir\file)").c_str()); + CHECK_SIMPLIFY(LR"(dir\file\)", (cwd.cwd + LR"(\dir\file\)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\dir\file\)").c_str()); + + // '.' components are dropped: + CHECK_SIMPLIFY(L".", cwd.cwd.c_str(), cwd.root.c_str(), cwd.relative.c_str()); + CHECK_SIMPLIFY(LR"(.\)", (cwd.cwd + LR"(\)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\)").c_str()); + CHECK_SIMPLIFY(LR"(a\.\b)", (cwd.cwd + LR"(\a\b)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\a\b)").c_str()); + + // '..' components are resolved: + CHECK_SIMPLIFY(LR"(a\..)", cwd.cwd.c_str(), cwd.root.c_str(), cwd.relative.c_str()); + CHECK_SIMPLIFY(LR"(a\..\)", (cwd.cwd + LR"(\)").c_str(), cwd.root.c_str(), (cwd.relative + LR"(\)").c_str()); + // clang-format on +} + +TEST_F(Test_File_Path, + simplify_path_and_make_absolute_path_starting_with_dot_dot) { + { + this->set_current_working_directory(this->make_temporary_directory()); + CWD_Parts cwd = get_cwd_parts(); + ASSERT_NE(cwd.relative, L"") + << "temporary directory shouldn't be a root path"; + std::size_t relative_last_slash_index = cwd.relative.rfind(L'\\'); + std::wstring parent_relative = + relative_last_slash_index == cwd.relative.npos + ? L"" + : cwd.relative.substr(0, relative_last_slash_index); + std::wstring expected_full_path = cwd.root + L'\\' + parent_relative; + CHECK_SIMPLIFY(L"..", + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/parent_relative.c_str()); + CHECK_SIMPLIFY(LR"(..\.)", + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/parent_relative.c_str()); + CHECK_SIMPLIFY(LR"(.\..)", + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/parent_relative.c_str()); + } + + { + // Change the current directory to e.g. "C:\". + this->set_current_working_directory( + wstring_to_mbstring(get_cwd_parts().root + L"\\").value()); + CWD_Parts cwd = get_cwd_parts(); + ASSERT_EQ(cwd.relative, L"") << "current directory should be root path"; + std::wstring expected_full_path = cwd.root + L'\\'; + CHECK_SIMPLIFY(L"..", + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/L""); + CHECK_SIMPLIFY(LR"(..\..)", + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/L""); + } +} + +TEST_F(Test_File_Path, + simplify_path_and_make_absolute_drive_cwd_relative_path) { + { + CWD_Parts cwd = get_cwd_parts(); + // L"C:dir" for example. + CHECK_SIMPLIFY((cwd.root + L"dir").c_str(), + /*full_path=*/(cwd.cwd + LR"(\dir)").c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/(cwd.relative + LR"(\dir)").c_str()); + // L"C:dir\\subdir" for example. + CHECK_SIMPLIFY((cwd.root + LR"(dir\subdir)").c_str(), + /*full_path=*/(cwd.cwd + LR"(\dir\subdir)").c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/(cwd.relative + LR"(\dir\subdir)").c_str()); + } +} + +// TODO(strager): Test mixing drives (e.g. cwd is on C: but path is "D:foo"). + +TEST_F( + Test_File_Path, + simplify_path_and_make_absolute_drive_cwd_relative_path_starting_with_dot_dot) { + { + this->set_current_working_directory(this->make_temporary_directory()); + CWD_Parts cwd = get_cwd_parts(); + ASSERT_NE(cwd.relative, L"") + << "temporary directory shouldn't be a root path"; + std::size_t relative_last_slash_index = cwd.relative.rfind(L'\\'); + std::wstring parent_relative = + relative_last_slash_index == cwd.relative.npos + ? L"" + : cwd.relative.substr(0, relative_last_slash_index); + std::wstring expected_full_path = cwd.root + L'\\' + parent_relative; + // L"C:.." for example. + CHECK_SIMPLIFY((cwd.root + L"..").c_str(), + /*full_path=*/expected_full_path.c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/parent_relative.c_str()); + } +} + +TEST_F(Test_File_Path, + simplify_path_and_make_absolute_current_drive_relative_path) { + CWD_Parts cwd = get_cwd_parts(); + CHECK_SIMPLIFY(LR"(\somedir)", + /*full_path=*/(cwd.root + LR"(\somedir)").c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/L"somedir"); + CHECK_SIMPLIFY(L"/somedir", + /*full_path=*/(cwd.root + LR"(\somedir)").c_str(), + /*root=*/cwd.root.c_str(), + /*relative=*/L"somedir"); +} +#endif } }