From 64014e51b46bcd872b6cf8c824bb7e940e647518 Mon Sep 17 00:00:00 2001 From: Matthew Glazar Date: Sun, 11 Feb 2024 16:43:12 -0500 Subject: [PATCH] @@@ symlinks --- .../change-detecting-filesystem-win32.cpp | 40 +++++- .../change-detecting-filesystem.h | 9 +- src/quick-lint-js/container/hash.h | 9 ++ src/quick-lint-js/io/file-canonical.cpp | 125 ++++++++++++++++-- src/quick-lint-js/io/file-path.cpp | 57 +++++++- src/quick-lint-js/io/file-path.h | 3 + src/quick-lint-js/io/file.cpp | 65 +++++++++ src/quick-lint-js/io/file.h | 11 ++ src/quick-lint-js/port/windows.h | 30 +++++ test/test-configuration-loader.cpp | 18 +-- test/test-file-canonical.cpp | 97 ++++++++------ 11 files changed, 388 insertions(+), 76 deletions(-) diff --git a/src/quick-lint-js/configuration/change-detecting-filesystem-win32.cpp b/src/quick-lint-js/configuration/change-detecting-filesystem-win32.cpp index 98a54b680c..0b9b8e8df8 100644 --- a/src/quick-lint-js/configuration/change-detecting-filesystem-win32.cpp +++ b/src/quick-lint-js/configuration/change-detecting-filesystem-win32.cpp @@ -101,7 +101,7 @@ Change_Detecting_Filesystem_Win32::~Change_Detecting_Filesystem_Win32() { Result Change_Detecting_Filesystem_Win32::canonicalize_path(const std::string& path) { - return quick_lint_js::canonicalize_path(path); + return quick_lint_js::canonicalize_path(path, this); } Result @@ -122,6 +122,28 @@ Change_Detecting_Filesystem_Win32::read_file(const Canonical_Path& path) { return *std::move(r); } +void Change_Detecting_Filesystem_Win32::on_canonicalize_child_of_directory( + const char*) { + // We don't use char paths on Windows. + QLJS_UNIMPLEMENTED(); +} + +// @@@ we should only do this on symlinks and shit? +void Change_Detecting_Filesystem_Win32::on_canonicalize_child_of_directory( + const wchar_t* path) { + bool ok = this->watch_directory(path); + if (!ok) { + std::optional narrow_path = wstring_to_mbstring(path); + if (!narrow_path.has_value()) { + QLJS_UNIMPLEMENTED(); + } + this->watch_errors_.emplace_back(Watch_IO_Error{ + .path = narrow_path->c_str(), + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } +} + bool Change_Detecting_Filesystem_Win32::handle_event( ::OVERLAPPED* overlapped, ::DWORD number_of_bytes_transferred, ::DWORD error) { @@ -179,9 +201,13 @@ bool Change_Detecting_Filesystem_Win32::watch_directory( if (!wpath.has_value()) { QLJS_UNIMPLEMENTED(); } + return this->watch_directory(wpath->c_str()); +} +bool Change_Detecting_Filesystem_Win32::watch_directory( + const wchar_t* directory) { Windows_Handle_File directory_handle(::CreateFileW( - wpath->c_str(), /*dwDesiredAccess=*/GENERIC_READ, + directory, /*dwDesiredAccess=*/GENERIC_READ, /*dwShareMode=*/FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, /*lpSecurityAttributes=*/nullptr, /*dwCreationDisposition=*/OPEN_EXISTING, @@ -215,9 +241,9 @@ bool Change_Detecting_Filesystem_Win32::watch_directory( } QLJS_DEBUG_LOG( - "note: Directory handle %#llx: %s: Directory identity changed\n", + "note: Directory handle %#llx: %ls: Directory identity changed\n", reinterpret_cast(old_dir->directory_handle.get()), - directory.c_str()); + directory); this->cancel_watch(std::move(watched_directory_it->second)); watched_directory_it->second = std::move(new_dir); } @@ -249,6 +275,10 @@ bool Change_Detecting_Filesystem_Win32::watch_directory( DWORD error = ::GetLastError(); if (error == ERROR_IO_PENDING) { // run_io_thread will handle the oplock breaking. + QLJS_DEBUG_LOG( + "note: Watching directory with handle %#llx: %ls\n", + reinterpret_cast(dir->directory_handle.get()), + directory); } else { // FIXME(strager): Should we close the directory handle? return false; @@ -297,7 +327,7 @@ void Change_Detecting_Filesystem_Win32::handle_oplock_broke_event( // // https://docs.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_request_oplock QLJS_DEBUG_LOG( - "note: Directory handle %#llx: %s: Oplock broke\n", + "note: Directory handle %#llx: %ls: Oplock broke\n", reinterpret_cast(dir->directory_handle.get()), directory_it->first.c_str()); QLJS_ASSERT(number_of_bytes_transferred == sizeof(dir->oplock_response)); diff --git a/src/quick-lint-js/configuration/change-detecting-filesystem.h b/src/quick-lint-js/configuration/change-detecting-filesystem.h index 0e04113d43..99b6516ede 100644 --- a/src/quick-lint-js/configuration/change-detecting-filesystem.h +++ b/src/quick-lint-js/configuration/change-detecting-filesystem.h @@ -153,7 +153,8 @@ extern ::DWORD mock_win32_force_directory_file_id_error; extern ::DWORD mock_win32_force_directory_ioctl_error; // Not thread-safe. -class Change_Detecting_Filesystem_Win32 : public Configuration_Filesystem { +class Change_Detecting_Filesystem_Win32 : public Configuration_Filesystem, + Canonicalize_Observer { public: explicit Change_Detecting_Filesystem_Win32( Windows_Handle_File_Ref io_completion_port, ::ULONG_PTR completion_key); @@ -164,6 +165,9 @@ class Change_Detecting_Filesystem_Win32 : public Configuration_Filesystem { Result read_file( const Canonical_Path&) override; + void on_canonicalize_child_of_directory(const char*) override; + void on_canonicalize_child_of_directory(const wchar_t*) override; + Windows_Handle_File_Ref io_completion_port() const { return this->io_completion_port_; } @@ -200,6 +204,7 @@ class Change_Detecting_Filesystem_Win32 : public Configuration_Filesystem { // Calls SetLastError and returns false on failure. bool watch_directory(const Canonical_Path&); + bool watch_directory(const wchar_t* path); void cancel_watch(std::unique_ptr&&); @@ -210,7 +215,7 @@ class Change_Detecting_Filesystem_Win32 : public Configuration_Filesystem { Windows_Handle_File_Ref io_completion_port_; ::ULONG_PTR completion_key_; - Hash_Map> + Hash_Map> watched_directories_; std::vector> cancelling_watched_directories_; diff --git a/src/quick-lint-js/container/hash.h b/src/quick-lint-js/container/hash.h index 8357a5d3fd..9b1740e61c 100644 --- a/src/quick-lint-js/container/hash.h +++ b/src/quick-lint-js/container/hash.h @@ -57,6 +57,15 @@ template <> struct Hasher : Hasher {}; #endif +template <> +struct Hasher { + std::size_t operator()(std::wstring_view s) const { + return std::hash()(s); + } +}; +template <> +struct Hasher : Hasher {}; + template struct Hasher> { template diff --git a/src/quick-lint-js/io/file-canonical.cpp b/src/quick-lint-js/io/file-canonical.cpp index b59b9ecd03..98a1ccddf8 100644 --- a/src/quick-lint-js/io/file-canonical.cpp +++ b/src/quick-lint-js/io/file-canonical.cpp @@ -11,8 +11,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -31,6 +33,7 @@ #if QLJS_HAVE_WINDOWS_H #include +#include #include #endif @@ -303,7 +306,7 @@ class Path_Canonicalizer_Base { directory, does_not_exist, other, - symlink, + symlink_or_reparse_point, }; quick_lint_js::Result load_cwd() { @@ -387,9 +390,9 @@ class Path_Canonicalizer_Base { } break; - case File_Type::symlink: { + case File_Type::symlink_or_reparse_point: { quick_lint_js::Result r = - this->derived().resolve_symlink(); + this->derived().resolve_symlink_or_reparse_point(); if (!r.ok()) return r.propagate(); break; } @@ -503,7 +506,7 @@ class POSIX_Path_Canonicalizer return failed_result(POSIX_File_IO_Error{errno}); } if (S_ISLNK(s.st_mode)) { - return File_Type::symlink; + return File_Type::symlink_or_reparse_point; } if (S_ISDIR(s.st_mode)) { return File_Type::directory; @@ -511,7 +514,8 @@ class POSIX_Path_Canonicalizer return File_Type::other; } - quick_lint_js::Result resolve_symlink() { + quick_lint_js::Result + resolve_symlink_or_reparse_point() { symlink_depth_ += 1; if (symlink_depth_ >= symlink_depth_limit_) { return failed_result(Canonicalizing_Path_IO_Error{ @@ -586,10 +590,19 @@ class Windows_Path_Canonicalizer quick_lint_js::Result process_start_of_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()); + // FIXME(strager): Do we need to copy to add the null terminator? If the + // null terminator is guaranteed to already exists, we should remove this + // copy. + std::wstring path(path_to_process_); + + // HACK(strager): PathCchSkipRoot and PathAllocCanonicalize don't support + // \??\ but does support \\?\. Convert \??\ to \\?\. + if (starts_with(std::wstring_view(path), LR"(\??\)"sv)) { + path[1] = L'\\'; + } + + Simplified_Path simplified_path = + simplify_path_and_make_absolute(&this->allocator_, path.c_str()); this->canonical_ = simplified_path.root; this->path_to_process_ = simplified_path.relative; this->need_root_slash_ = true; @@ -628,7 +641,7 @@ class Windows_Path_Canonicalizer return failed_result(Windows_File_IO_Error{error}); } if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) { - return File_Type::symlink; + return File_Type::symlink_or_reparse_point; } if (attributes & FILE_ATTRIBUTE_DIRECTORY) { return File_Type::directory; @@ -636,9 +649,95 @@ class Windows_Path_Canonicalizer return File_Type::other; } - quick_lint_js::Result resolve_symlink() { - // TODO(strager): Support symlinks on Windows. - QLJS_UNIMPLEMENTED(); + quick_lint_js::Result + resolve_symlink_or_reparse_point() { + symlink_depth_ += 1; + if (symlink_depth_ >= symlink_depth_limit_) { + return failed_result(Canonicalizing_Path_IO_Error{ + .canonicalizing_path = canonical_, + .io_error = Windows_File_IO_Error{ERROR_CANT_RESOLVE_FILENAME}, + }); + } + + Windows_Handle_File file( + ::CreateFileW(canonical_.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*lpSecurityAttributes=*/nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + /*hTemplateFile=*/nullptr)); + if (!file.valid()) { + return failed_result(Canonicalizing_Path_IO_Error{ + .canonicalizing_path = canonical_, + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } + + Monotonic_Allocator memory("resolve_symlink_or_reparse_point"); + Vector reparse_data_raw("reparse_data", &memory); + // See NOTE[reparse-point-null-terminator] for why we add sizeof(wchar_t). + reparse_data_raw.resize(MAXIMUM_REPARSE_DATA_BUFFER_SIZE + sizeof(wchar_t)); + + ::DWORD bytes_returned; + if (!::DeviceIoControl( + file.get(), FSCTL_GET_REPARSE_POINT, + /*lpInBuffer=*/nullptr, /*nInBufferSize=*/0, + /*lpOutBuffer=*/reparse_data_raw.data(), + /*nOutBufferSize=*/narrow_cast<::DWORD>(reparse_data_raw.size()), + &bytes_returned, /*lpOverlapped=*/nullptr)) { + return failed_result(Canonicalizing_Path_IO_Error{ + .canonicalizing_path = canonical_, + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } + file.close(); + + ::REPARSE_DATA_BUFFER *reparse_data = + reinterpret_cast<::REPARSE_DATA_BUFFER *>(reparse_data_raw.data()); + switch (reparse_data->ReparseTag) { + case IO_REPARSE_TAG_MOUNT_POINT: + QLJS_UNIMPLEMENTED(); + break; + + case IO_REPARSE_TAG_SYMLINK: { + auto &symlink = reparse_data->SymbolicLinkReparseBuffer; + std::wstring &new_readlink_buffer = + readlink_buffers_[1 - used_readlink_buffer_]; + // NOTE[reparse-point-null-terminator]: The path is not null-terminated. + std::wstring_view symlink_target( + &symlink.PathBuffer[symlink.SubstituteNameOffset / sizeof(wchar_t)], + symlink.SubstituteNameLength / sizeof(wchar_t)); + + if (symlink.Flags & SYMLINK_FLAG_RELATIVE) { + canonical_ = parent_path(std::move(canonical_)); + new_readlink_buffer.reserve(canonical_.size() + 1 + + symlink_target.size() + + path_to_process_.size()); + new_readlink_buffer.clear(); + new_readlink_buffer += canonical_; + new_readlink_buffer += L'\\'; + new_readlink_buffer += symlink_target; + } else { + new_readlink_buffer.reserve(symlink_target.size() + + path_to_process_.size()); + new_readlink_buffer = symlink_target; + } + new_readlink_buffer += path_to_process_; + path_to_process_ = new_readlink_buffer; + // After assigning to path_to_process_, + // readlink_buffers_[used_readlink_buffer_] is no longer in use. + swap_readlink_buffers(); + + Result r = process_start_of_path(); + if (!r.ok()) return r.propagate(); + + break; + } + + default: + QLJS_UNIMPLEMENTED(); + break; + } + return {}; } diff --git a/src/quick-lint-js/io/file-path.cpp b/src/quick-lint-js/io/file-path.cpp index 6ae361bbb9..b1597995ae 100644 --- a/src/quick-lint-js/io/file-path.cpp +++ b/src/quick-lint-js/io/file-path.cpp @@ -35,20 +35,22 @@ void remove_trailing_slashes(std::string_view& path) { } #if QLJS_HAVE_WINDOWS_H -std::wstring wide_path_with_backslashes(const std::string& path) { - std::optional wpath = mbstring_to_wstring(path.c_str()); - if (!wpath.has_value()) { - QLJS_UNIMPLEMENTED(); - } - +void force_backslashes_in_path(std::wstring& path) { // The PathCch functions only support '\' as a directory separator. Convert // all '/'s into '\'s. - for (wchar_t& c : *wpath) { + for (wchar_t& c : path) { if (c == L'/') { c = L'\\'; } } +} +std::wstring wide_path_with_backslashes(const std::string& path) { + std::optional wpath = mbstring_to_wstring(path.c_str()); + if (!wpath.has_value()) { + QLJS_UNIMPLEMENTED(); + } + force_backslashes_in_path(*wpath); return std::move(*wpath); } @@ -80,6 +82,8 @@ std::string parent_path(std::string&& path) { #if QLJS_HAVE_DIRNAME return ::dirname(path.data()); #elif QLJS_HAVE_WINDOWS_H + // See also the std::wstring overload of parent_path. + HRESULT result; if (path == R"(\\?\)"sv || path == R"(\\?)"sv) { @@ -130,6 +134,45 @@ std::string parent_path(std::string&& path) { #endif } +#if QLJS_HAVE_WINDOWS_H +std::wstring parent_path(std::wstring&& path) { + // See also the std::string overload of parent_path. + + HRESULT result; + + if (path == LR"(\\?\)"sv || path == LR"(\\?)"sv) { + // Invalid path. Leave as-is. + return path; + } + + force_backslashes_in_path(path); + safely_remove_trailing_backslashes(path); + + result = ::PathCchRemoveFileSpec(path.data(), path.size() + 1); + switch (result) { + case S_OK: + break; + case S_FALSE: + // Path is a root path already. + break; + case HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER): + // Path is invalid. + QLJS_UNIMPLEMENTED(); + break; + default: + QLJS_UNIMPLEMENTED(); + break; + } + + path.resize(std::wcslen(path.data())); + if (path.empty()) { + return L"."; + } + + return path.substr(0, path.size()); +} +#endif + std::string_view path_file_name(std::string_view path) { #if QLJS_HAVE_WINDOWS_H { diff --git a/src/quick-lint-js/io/file-path.h b/src/quick-lint-js/io/file-path.h index 4accc479f9..75a5570580 100644 --- a/src/quick-lint-js/io/file-path.h +++ b/src/quick-lint-js/io/file-path.h @@ -23,6 +23,9 @@ namespace quick_lint_js { std::string parent_path(std::string&&); +#if defined(_WIN32) +std::wstring parent_path(std::wstring&&); +#endif std::string_view path_file_name(std::string_view); diff --git a/src/quick-lint-js/io/file.cpp b/src/quick-lint-js/io/file.cpp index 20d5de399f..8b16bdccae 100644 --- a/src/quick-lint-js/io/file.cpp +++ b/src/quick-lint-js/io/file.cpp @@ -168,6 +168,15 @@ std::string Write_File_IO_Error::to_string() const { std::exit(1); } +std::string Delete_File_IO_Error::to_string() const { + return "failed to delete "s + this->path + ": "s + this->io_error.to_string(); +} + +[[noreturn]] void Delete_File_IO_Error::print_and_exit() const { + std::fprintf(stderr, "error: %s\n", this->to_string().c_str()); + std::exit(1); +} + std::string Symlink_IO_Error::to_string() const { return "failed to create symlink to "s + this->target + " at " + this->path + ": "s + this->io_error.to_string(); @@ -500,6 +509,62 @@ void create_posix_file_symbolic_link_or_exit(const char *path, result.error().print_and_exit(); } } + +Result delete_posix_symbolic_link( + const char *path) { +#if defined(QLJS_FILE_POSIX) + int rc = ::unlink(path); + if (rc != 0) { + return failed_result(Delete_File_IO_Error{ + .path = path, + .io_error = POSIX_File_IO_Error{errno}, + }); + } + return {}; +#elif defined(QLJS_FILE_WINDOWS) + std::optional wpath = mbstring_to_wstring(path); + if (!wpath.has_value()) { + return failed_result(Delete_File_IO_Error{ + .path = path, + .io_error = Windows_File_IO_Error{ERROR_INVALID_PARAMETER}, + }); + } + + ::DWORD attributes = ::GetFileAttributesW(wpath->c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + return failed_result(Delete_File_IO_Error{ + .path = path, + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } + if (attributes & FILE_ATTRIBUTE_DIRECTORY) { + if (!::RemoveDirectoryW(wpath->c_str())) { + return failed_result(Delete_File_IO_Error{ + .path = path, + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } + } else { + if (!::DeleteFileW(wpath->c_str())) { + return failed_result(Delete_File_IO_Error{ + .path = path, + .io_error = Windows_File_IO_Error{::GetLastError()}, + }); + } + } + + return {}; +#else +#error "Unknown platform" +#endif +} + +void delete_posix_symbolic_link_or_exit(const char *path) { + Result result = delete_posix_symbolic_link(path); + if (!result.ok()) { + result.error().print_and_exit(); + } +} } #endif diff --git a/src/quick-lint-js/io/file.h b/src/quick-lint-js/io/file.h index 0a0be40638..bb3e9b1be1 100644 --- a/src/quick-lint-js/io/file.h +++ b/src/quick-lint-js/io/file.h @@ -55,6 +55,14 @@ struct Symlink_IO_Error { [[noreturn]] void print_and_exit() const; }; +struct Delete_File_IO_Error { + std::string path; + Platform_File_IO_Error io_error; + + std::string to_string() const; + [[noreturn]] void print_and_exit() const; +}; + Result read_file(const std::string &path); Result read_file(const char *path); Result read_file(const char *path, @@ -104,6 +112,9 @@ void create_posix_directory_symbolic_link_or_exit(const char *path, const char *target); void create_posix_file_symbolic_link_or_exit(const char *path, const char *target); + +Result delete_posix_symbolic_link(const char *path); +void delete_posix_symbolic_link_or_exit(const char *path); } #endif diff --git a/src/quick-lint-js/port/windows.h b/src/quick-lint-js/port/windows.h index c316b5ef54..053c06f2d8 100644 --- a/src/quick-lint-js/port/windows.h +++ b/src/quick-lint-js/port/windows.h @@ -64,6 +64,36 @@ HRESULT WINAPI SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription); } #endif +// +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer +typedef struct _REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLinkReparseBuffer; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPointReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + } DUMMYUNIONNAME; +} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; + +#define SYMLINK_FLAG_RELATIVE 1 + // quick-lint-js finds bugs in JavaScript programs. // Copyright (C) 2020 Matthew "strager" Glazar // diff --git a/test/test-configuration-loader.cpp b/test/test-configuration-loader.cpp index abb9400184..b983f1eb97 100644 --- a/test/test-configuration-loader.cpp +++ b/test/test-configuration-loader.cpp @@ -1963,8 +1963,6 @@ TEST_F(Test_Configuration_Loader, } #endif -// TODO(strager): Test symlinks on Windows too. -#if QLJS_HAVE_UNISTD_H TEST_F(Test_Configuration_Loader, changing_direct_config_path_symlink_is_detected_as_change) { std::string project_dir = this->make_temporary_directory(); @@ -1974,15 +1972,14 @@ TEST_F(Test_Configuration_Loader, std::string after_config_file = project_dir + "/after.config"; write_file_or_exit(after_config_file, u8R"({"globals": {"after": true}})"_sv); std::string config_symlink = project_dir + "/quick-lint-js.config"; - ASSERT_EQ(::symlink("before.config", config_symlink.c_str()), 0) - << std::strerror(errno); + create_posix_file_symbolic_link_or_exit(config_symlink.c_str(), "before.config"); Change_Detecting_Configuration_Loader loader; loader.watch_and_load_config_file(config_symlink, /*token=*/&config_symlink); ASSERT_EQ(std::remove(config_symlink.c_str()), 0) << std::strerror(errno); - ASSERT_EQ(::symlink("after.config", config_symlink.c_str()), 0) - << std::strerror(errno); + + create_posix_file_symbolic_link_or_exit(config_symlink.c_str(), "after.config"); Span changes = loader.detect_changes_and_refresh(); ASSERT_THAT(changes, ElementsAreArray({::testing::_})); @@ -2009,16 +2006,14 @@ TEST_F(Test_Configuration_Loader, std::string after_config_file = project_dir + "/after/quick-lint-js.config"; write_file_or_exit(after_config_file, u8R"({"globals": {"after": true}})"_sv); std::string subdir_symlink = project_dir + "/subdir"; - ASSERT_EQ(::symlink("before", subdir_symlink.c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit(subdir_symlink.c_str(), "before"); Change_Detecting_Configuration_Loader loader; loader.watch_and_load_config_file(subdir_symlink + "/quick-lint-js.config", /*token=*/nullptr); - ASSERT_EQ(std::remove(subdir_symlink.c_str()), 0) << std::strerror(errno); - ASSERT_EQ(::symlink("after", subdir_symlink.c_str()), 0) - << std::strerror(errno); + delete_posix_symbolic_link_or_exit(subdir_symlink.c_str()); + create_posix_directory_symbolic_link_or_exit(subdir_symlink.c_str(), "after"); Span changes = loader.detect_changes_and_refresh(); ASSERT_THAT(changes, ElementsAreArray({::testing::_})); @@ -2032,7 +2027,6 @@ TEST_F(Test_Configuration_Loader, EXPECT_THAT(loader.detect_changes_and_refresh(), IsEmpty()); } -#endif TEST_F(Test_Configuration_Loader, swapping_parent_directory_with_another_is_detected_as_change) { diff --git a/test/test-file-canonical.cpp b/test/test-file-canonical.cpp index 2b994425aa..566c264c88 100644 --- a/test/test-file-canonical.cpp +++ b/test/test-file-canonical.cpp @@ -666,15 +666,11 @@ TEST_F(Test_File_Canonical, canonical_path_with_root_as_cwd) { EXPECT_SAME_FILE(canonical->path(), input_path); } -// TODO(strager): Test symlinks on Windows too. -#if QLJS_HAVE_UNISTD_H TEST_F(Test_File_Canonical, canonical_path_resolves_file_absolute_symlinks) { std::string temp_dir = this->make_temporary_directory(); write_file_or_exit(temp_dir + "/realfile", u8""_sv); - ASSERT_EQ(::symlink((temp_dir + "/realfile").c_str(), - (temp_dir + "/linkfile").c_str()), - 0) - << std::strerror(errno); + create_posix_file_symbolic_link_or_exit((temp_dir + "/linkfile").c_str(), + (temp_dir + "/realfile").c_str()); std::string input_path = temp_dir + "/linkfile"; Result canonical = @@ -691,8 +687,8 @@ TEST_F(Test_File_Canonical, canonical_path_resolves_file_absolute_symlinks) { TEST_F(Test_File_Canonical, canonical_path_resolves_file_relative_symlinks) { std::string temp_dir = this->make_temporary_directory(); write_file_or_exit(temp_dir + "/realfile", u8""_sv); - ASSERT_EQ(::symlink("realfile", (temp_dir + "/linkfile").c_str()), 0) - << std::strerror(errno); + create_posix_file_symbolic_link_or_exit((temp_dir + "/linkfile").c_str(), + "realfile"); std::string input_path = temp_dir + "/linkfile"; Result canonical = @@ -710,10 +706,8 @@ TEST_F(Test_File_Canonical, canonical_path_resolves_directory_absolute_symlinks) { std::string temp_dir = this->make_temporary_directory(); create_directory_or_exit(temp_dir + "/realdir"); - ASSERT_EQ(::symlink((temp_dir + "/realdir").c_str(), - (temp_dir + "/linkdir").c_str()), - 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/linkdir").c_str(), + (temp_dir + "/realdir").c_str()); write_file_or_exit(temp_dir + "/realdir/temp.js", u8""_sv); std::string input_path = temp_dir + "/linkdir/temp.js"; @@ -733,8 +727,8 @@ TEST_F(Test_File_Canonical, canonical_path_resolves_directory_relative_symlinks) { std::string temp_dir = this->make_temporary_directory(); create_directory_or_exit(temp_dir + "/realdir"); - ASSERT_EQ(::symlink("realdir", (temp_dir + "/linkdir").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/linkdir").c_str(), + "realdir"); write_file_or_exit(temp_dir + "/realdir/temp.js", u8""_sv); std::string input_path = temp_dir + "/linkdir/temp.js"; @@ -750,15 +744,41 @@ TEST_F(Test_File_Canonical, EXPECT_SAME_FILE(canonical->path(), input_path); } +// Windows and POSIX behave differently. Windows resolves '..' in the path prior +// to following the symlink. POSIX follows '..' in the path after following the +// symlink. +#if defined(_WIN32) TEST_F(Test_File_Canonical, - canonical_path_resolves_dot_dot_with_directory_symlinks) { + canonical_path_resolves_dot_dot_before_following_directory_symlinks) { std::string temp_dir = this->make_temporary_directory(); create_directory_or_exit(temp_dir + "/dir"); create_directory_or_exit(temp_dir + "/dir/subdir"); - ASSERT_EQ(::symlink((temp_dir + "/dir/subdir").c_str(), - (temp_dir + "/linkdir").c_str()), - 0) - << std::strerror(errno); + // NOTE(strager): In this test, linkdir isn't used. (This behavior differs + // from POSIX.) + create_posix_directory_symbolic_link_or_exit( + (temp_dir + "/linkdir").c_str(), (temp_dir + "/dir/subdir").c_str()); + write_file_or_exit(temp_dir + "/temp.js", u8""_sv); + + std::string input_path = temp_dir + "/linkdir/../temp.js"; + Result canonical = + canonicalize_path(input_path); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/linkdir/"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\linkdir\\"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/dir/"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\dir\\"))); + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/temp.js"); + EXPECT_SAME_FILE(canonical->path(), input_path); +} +#else +TEST_F(Test_File_Canonical, + canonical_path_resolves_dot_dot_after_following_directory_symlinks) { + std::string temp_dir = this->make_temporary_directory(); + create_directory_or_exit(temp_dir + "/dir"); + create_directory_or_exit(temp_dir + "/dir/subdir"); + create_posix_directory_symbolic_link_or_exit( + (temp_dir + "/linkdir").c_str(), (temp_dir + "/dir/subdir").c_str()); write_file_or_exit(temp_dir + "/dir/temp.js", u8""_sv); std::string input_path = temp_dir + "/linkdir/../temp.js"; @@ -771,13 +791,15 @@ TEST_F(Test_File_Canonical, EXPECT_SAME_FILE(canonical->path(), temp_dir + "/dir/temp.js"); EXPECT_SAME_FILE(canonical->path(), input_path); } +#endif TEST_F(Test_File_Canonical, canonical_path_resolves_dot_dot_inside_symlinks) { std::string temp_dir = this->make_temporary_directory(); create_directory_or_exit(temp_dir + "/dir"); create_directory_or_exit(temp_dir + "/otherdir"); - ASSERT_EQ(::symlink("../otherdir", (temp_dir + "/dir/linkdir").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit( + (temp_dir + "/dir/linkdir").c_str(), + ".." QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "otherdir"); write_file_or_exit(temp_dir + "/otherdir/temp.js", u8""_sv); std::string input_path = temp_dir + "/dir/linkdir/temp.js"; @@ -794,10 +816,10 @@ TEST_F(Test_File_Canonical, canonical_path_resolves_dot_dot_inside_symlinks) { TEST_F(Test_File_Canonical, canonical_path_fails_with_symlink_loop_in_directory) { std::string temp_dir = this->make_temporary_directory(); - ASSERT_EQ(::symlink("link1", (temp_dir + "/link2").c_str()), 0) - << std::strerror(errno); - ASSERT_EQ(::symlink("link2", (temp_dir + "/link1").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/link2").c_str(), + "link1"); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/link1").c_str(), + "link2"); std::string input_path = temp_dir + "/link1/file.js"; Result canonical = @@ -819,8 +841,8 @@ TEST_F(Test_File_Canonical, canonicalize_path(temp_dir); ASSERT_TRUE(temp_dir_canonical.ok()) << temp_dir_canonical.error().to_string(); - ASSERT_EQ(::symlink("does-not-exist", (temp_dir + "/testlink").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/testlink").c_str(), + "does-not-exist"); std::string input_path = temp_dir + "/testlink/file.js"; Result canonical = @@ -844,8 +866,8 @@ TEST_F(Test_File_Canonical, canonical_path_with_broken_symlink_file_fails) { canonicalize_path(temp_dir); ASSERT_TRUE(temp_dir_canonical.ok()) << temp_dir_canonical.error().to_string(); - ASSERT_EQ(::symlink("does-not-exist", (temp_dir + "/testlink").c_str()), 0) - << std::strerror(errno); + create_posix_file_symbolic_link_or_exit((temp_dir + "/testlink").c_str(), + "does-not-exist"); std::string input_path = temp_dir + "/testlink"; Result canonical = @@ -865,8 +887,8 @@ TEST_F(Test_File_Canonical, canonical_path_with_broken_symlink_file_fails) { TEST_F(Test_File_Canonical, canonical_path_resolves_symlinks_in_cwd) { std::string temp_dir = this->make_temporary_directory(); create_directory_or_exit(temp_dir + "/realdir"); - ASSERT_EQ(::symlink("realdir", (temp_dir + "/linkdir").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit((temp_dir + "/linkdir").c_str(), + "realdir"); write_file_or_exit(temp_dir + "/realdir/temp.js", u8""_sv); this->set_current_working_directory(temp_dir + "/linkdir"); @@ -884,11 +906,11 @@ TEST_F(Test_File_Canonical, canonical_path_resolves_symlinks_in_cwd) { TEST_F(Test_File_Canonical, canonical_path_resolves_symlink_pointing_to_symlink) { std::string temp_dir = this->make_temporary_directory(); - ASSERT_EQ(::symlink("otherlinkdir/subdir", (temp_dir + "/linkdir").c_str()), - 0) - << std::strerror(errno); - ASSERT_EQ(::symlink("realdir", (temp_dir + "/otherlinkdir").c_str()), 0) - << std::strerror(errno); + create_posix_directory_symbolic_link_or_exit( + (temp_dir + "/linkdir").c_str(), + "otherlinkdir" QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "subdir"); + create_posix_directory_symbolic_link_or_exit( + (temp_dir + "/otherlinkdir").c_str(), "realdir"); create_directory_or_exit(temp_dir + "/realdir"); create_directory_or_exit(temp_dir + "/realdir/subdir"); write_file_or_exit(temp_dir + "/realdir/subdir/hello.js", u8""_sv); @@ -906,7 +928,8 @@ TEST_F(Test_File_Canonical, EXPECT_SAME_FILE(canonical->path(), temp_dir + "/realdir/subdir/hello.js"); EXPECT_SAME_FILE(canonical->path(), input_path); } -#endif + +// TODO(strager): Test Windows junctions. #if QLJS_HAVE_UNISTD_H TEST_F(Test_File_Canonical, unsearchable_parent_directory) {