From 137cde4fab3cf95ba6315b0974187b7a1b10286f Mon Sep 17 00:00:00 2001 From: Matthew Glazar Date: Sun, 11 Feb 2024 21:53:38 -0500 Subject: [PATCH] feat(io): support Windows junctions We support symlinks on Windows but not junctions (which are similar). Teach canonicalize_path to follow junctions, handling them similarly to symlinks. (The implementation is wrong, but wrong in some edge cases (https://github.com/quick-lint/quick-lint-js/issues/1200) is perhaps a bit better than consistently crashing (QLJS_UNIMPLEMENTED).) --- src/quick-lint-js/io/file-canonical.cpp | 29 +++++++- src/quick-lint-js/io/file.cpp | 73 +++++++++++++++++++ src/quick-lint-js/io/file.h | 4 ++ test/test-file-canonical.cpp | 96 +++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/quick-lint-js/io/file-canonical.cpp b/src/quick-lint-js/io/file-canonical.cpp index 98a1ccddf..aa546b017 100644 --- a/src/quick-lint-js/io/file-canonical.cpp +++ b/src/quick-lint-js/io/file-canonical.cpp @@ -694,9 +694,34 @@ class Windows_Path_Canonicalizer ::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(); + // NOTE(strager): Mount points are also known as junctions. + case IO_REPARSE_TAG_MOUNT_POINT: { + // TODO(#1200): Make '..' after following the junction escape the + // junction. + + auto &mount_point = reparse_data->MountPointReparseBuffer; + 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 target( + &mount_point + .PathBuffer[mount_point.SubstituteNameOffset / sizeof(wchar_t)], + mount_point.SubstituteNameLength / sizeof(wchar_t)); + + // NOTE(strager): Mount point targets are always absolute. + new_readlink_buffer.reserve(target.size() + path_to_process_.size()); + new_readlink_buffer = 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; + } case IO_REPARSE_TAG_SYMLINK: { auto &symlink = reparse_data->SymbolicLinkReparseBuffer; diff --git a/src/quick-lint-js/io/file.cpp b/src/quick-lint-js/io/file.cpp index 8b16bdcca..018cdd7cf 100644 --- a/src/quick-lint-js/io/file.cpp +++ b/src/quick-lint-js/io/file.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #if QLJS_HAVE_WINDOWS_H #include +#include #endif #if QLJS_HAVE_WINDOWS_H @@ -565,6 +567,77 @@ void delete_posix_symbolic_link_or_exit(const char *path) { result.error().print_and_exit(); } } + +void create_windows_junction_or_exit(const char *path, const char *target) { + Monotonic_Allocator memory("create_windows_junction"); + + std::optional wpath = mbstring_to_wstring(path); + std::optional wtarget = mbstring_to_wstring(target); + if (!wpath.has_value() || !wtarget.has_value()) { + std::fprintf(stderr, "fatal: cannot convert path to wide string\n"); + std::abort(); + } + if (!::CreateDirectoryW(wpath->c_str(), /*lpSecurityAttributes=*/nullptr)) { + std::fprintf(stderr, "fatal: CreateDirectoryW failed\n"); + std::abort(); + } + Windows_Handle_File file( + ::CreateFileW(wpath->c_str(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*lpSecurityAttributes=*/nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + /*hTemplateFile=*/nullptr)); + + std::size_t reparse_data_data_size = + offsetof(decltype(std::declval<::REPARSE_DATA_BUFFER>() + .MountPointReparseBuffer), + PathBuffer) + + // SubsituteName. +1 for null terminator. + (wtarget->size() + 1) * sizeof(wchar_t) + + // PrintName. Empty string with null terminator. + sizeof(wchar_t); + std::size_t reparse_data_total_size = + offsetof(::REPARSE_DATA_BUFFER, MountPointReparseBuffer) + + reparse_data_data_size; + ::REPARSE_DATA_BUFFER *reparse_data = + reinterpret_cast<::REPARSE_DATA_BUFFER *>(memory.allocate( + reparse_data_total_size, alignof(::REPARSE_DATA_BUFFER))); + reparse_data->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + reparse_data->ReparseDataLength = reparse_data_data_size; + reparse_data->Reserved = 0; + + // Write the null-terminated SubstituteName (*wtarget) followed by the + // null-terminated PrintName (L""). + { + wchar_t *path_buffer = reparse_data->MountPointReparseBuffer.PathBuffer; + auto current_offset = [&]() -> ::USHORT { + return narrow_cast<::USHORT>( + (reinterpret_cast(path_buffer) - + reinterpret_cast( + reparse_data->MountPointReparseBuffer.PathBuffer))); + }; + reparse_data->MountPointReparseBuffer.SubstituteNameOffset = + current_offset(); + reparse_data->MountPointReparseBuffer.SubstituteNameLength = + wtarget->size() * sizeof(wchar_t); + path_buffer = std::copy(wtarget->begin(), wtarget->end(), path_buffer); + *path_buffer++ = L'\0'; + + reparse_data->MountPointReparseBuffer.PrintNameOffset = current_offset(); + reparse_data->MountPointReparseBuffer.PrintNameLength = 0; + *path_buffer++ = L'\0'; + } + + if (!::DeviceIoControl(file.get(), FSCTL_SET_REPARSE_POINT, reparse_data, + narrow_cast<::DWORD>(reparse_data_total_size), + /*lpOutBuffer=*/nullptr, + /*nOutBufferSize=*/0, + /*lpBytesReturned=*/nullptr, + /*lpOverlapped=*/nullptr)) { + std::fprintf(stderr, "fatal: DeviceIoControl failed\n"); + std::abort(); + } +} } #endif diff --git a/src/quick-lint-js/io/file.h b/src/quick-lint-js/io/file.h index bb3e9b1be..732e3d7a4 100644 --- a/src/quick-lint-js/io/file.h +++ b/src/quick-lint-js/io/file.h @@ -115,6 +115,10 @@ void create_posix_file_symbolic_link_or_exit(const char *path, Result delete_posix_symbolic_link(const char *path); void delete_posix_symbolic_link_or_exit(const char *path); + +// target must begin with \??\. +void create_windows_junction_or_exit(const char *path, const char *target); + } #endif diff --git a/test/test-file-canonical.cpp b/test/test-file-canonical.cpp index 566c264c8..7894c645a 100644 --- a/test/test-file-canonical.cpp +++ b/test/test-file-canonical.cpp @@ -723,6 +723,102 @@ TEST_F(Test_File_Canonical, EXPECT_SAME_FILE(canonical->path(), input_path); } +#if defined(_WIN32) +TEST_F(Test_File_Canonical, canonical_path_resolves_directory_junctions) { + std::string temp_dir = this->make_temporary_directory(); + create_directory_or_exit(temp_dir + "/realdir"); + create_windows_junction_or_exit( + (temp_dir + "/linkdir").c_str(), + ("\\??\\" + temp_dir + QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "realdir") + .c_str()); + write_file_or_exit(temp_dir + "/realdir/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()), + AnyOf(HasSubstr("/realdir/"), HasSubstr("\\realdir\\"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/linkdir/"))); + EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\linkdir\\"))); + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/realdir/temp.js"); + EXPECT_SAME_FILE(canonical->path(), input_path); +} +#endif + +#if defined(_WIN32) +TEST_F(Test_File_Canonical, symlink_with_dot_dot_escapes_symlinked_dir) { + // $temp_dir\dir [directory] + // $temp_dir\dir\file.txt [file] + // $temp_dir\dir\subdir [directory] + // $temp_dir\dir\subdir\filelink.txt [symlink] -> ..\file.txt + // $temp_dir\linkdir [junction] -> $temp_dir\dir\subdir + // $temp_dir\file.txt [file] + // + // $temp_dir\linkdir\filelink.txt + // is the same as $temp_dir\dir\subdir\..\file.txt + // is the same as $temp_dir\dir\file.txt + + std::string temp_dir = this->make_temporary_directory(); + create_directory_or_exit(temp_dir + "/dir"); + write_file_or_exit(temp_dir + "/dir/file.txt", u8"$tempdir/dir/file.txt"); + create_directory_or_exit(temp_dir + "/dir/subdir"); + create_posix_file_symbolic_link_or_exit( + (temp_dir + "/dir/subdir/filelink.txt").c_str(), "..\\file.txt"); + create_posix_directory_symbolic_link( + (temp_dir + "/linkdir").c_str(), + "dir" QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "subdir"); + write_file_or_exit(temp_dir + "/file.txt", u8"$tempdir/file.txt"); + + std::string input_path = temp_dir + "/linkdir/filelink.txt"; + Result canonical = + canonicalize_path(input_path); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/dir/subdir/../file.txt"); + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/dir/file.txt"); +} +#endif + +#if defined(_WIN32) +// TODO(#1200): Fix canonicalize_file when it encounters junctions. +TEST_F(Test_File_Canonical, DISABLED_symlink_with_dot_dot_escapes_junction) { + // $temp_dir\dir [directory] + // $temp_dir\dir\file.txt [file] + // $temp_dir\dir\subdir [directory] + // $temp_dir\dir\subdir\filelink.txt [symlink] -> ..\file.txt + // $temp_dir\linkdir [junction] -> $temp_dir\dir\subdir + // $temp_dir\file.txt [file] + // + // $temp_dir\linkdir\filelink.txt + // is the same as $temp_dir\linkdir\..\file.txt + // is the same as $temp_dir\file.txt + + std::string temp_dir = this->make_temporary_directory(); + create_directory_or_exit(temp_dir + "/dir"); + write_file_or_exit(temp_dir + "/dir/file.txt", u8"$tempdir/dir/file.txt"); + create_directory_or_exit(temp_dir + "/dir/subdir"); + create_posix_file_symbolic_link_or_exit( + (temp_dir + "/dir/subdir/filelink.txt").c_str(), "..\\file.txt"); + create_windows_junction_or_exit( + (temp_dir + "/linkdir").c_str(), + ("\\??\\" + temp_dir + + QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR + "dir" QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "subdir") + .c_str()); + write_file_or_exit(temp_dir + "/file.txt", u8"$tempdir/file.txt"); + + std::string input_path = temp_dir + "/linkdir/filelink.txt"; + Result canonical = + canonicalize_path(input_path); + ASSERT_TRUE(canonical.ok()) << canonical.error().to_string(); + + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/linkdir/../file.txt"); + EXPECT_SAME_FILE(canonical->path(), temp_dir + "/file.txt"); +} +#endif + TEST_F(Test_File_Canonical, canonical_path_resolves_directory_relative_symlinks) { std::string temp_dir = this->make_temporary_directory();