Skip to content

Commit 748b775

Browse files
committed
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 (#1200) is perhaps a bit better than consistently crashing (QLJS_UNIMPLEMENTED).)
1 parent 6c8f89d commit 748b775

File tree

4 files changed

+203
-2
lines changed

4 files changed

+203
-2
lines changed

src/quick-lint-js/io/file-canonical.cpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,9 +694,34 @@ class Windows_Path_Canonicalizer
694694
::REPARSE_DATA_BUFFER *reparse_data =
695695
reinterpret_cast<::REPARSE_DATA_BUFFER *>(reparse_data_raw.data());
696696
switch (reparse_data->ReparseTag) {
697-
case IO_REPARSE_TAG_MOUNT_POINT:
698-
QLJS_UNIMPLEMENTED();
697+
// NOTE(strager): Mount points are also known as junctions.
698+
case IO_REPARSE_TAG_MOUNT_POINT: {
699+
// TODO(#1200): Make '..' after following the junction escape the
700+
// junction.
701+
702+
auto &mount_point = reparse_data->MountPointReparseBuffer;
703+
std::wstring &new_readlink_buffer =
704+
readlink_buffers_[1 - used_readlink_buffer_];
705+
// NOTE[reparse-point-null-terminator]: The path is not null-terminated.
706+
std::wstring_view target(
707+
&mount_point
708+
.PathBuffer[mount_point.SubstituteNameOffset / sizeof(wchar_t)],
709+
mount_point.SubstituteNameLength / sizeof(wchar_t));
710+
711+
// NOTE(strager): Mount point targets are always absolute.
712+
new_readlink_buffer.reserve(target.size() + path_to_process_.size());
713+
new_readlink_buffer = target;
714+
new_readlink_buffer += path_to_process_;
715+
path_to_process_ = new_readlink_buffer;
716+
// After assigning to path_to_process_,
717+
// readlink_buffers_[used_readlink_buffer_] is no longer in use.
718+
swap_readlink_buffers();
719+
720+
Result<void, Canonicalizing_Path_IO_Error> r = process_start_of_path();
721+
if (!r.ok()) return r.propagate();
722+
699723
break;
724+
}
700725

701726
case IO_REPARSE_TAG_SYMLINK: {
702727
auto &symlink = reparse_data->SymbolicLinkReparseBuffer;

src/quick-lint-js/io/file.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <cstring>
1212
#include <optional>
1313
#include <quick-lint-js/assert.h>
14+
#include <quick-lint-js/container/monotonic-allocator.h>
1415
#include <quick-lint-js/io/file-handle.h>
1516
#include <quick-lint-js/io/file.h>
1617
#include <quick-lint-js/port/char8.h>
@@ -40,6 +41,7 @@
4041

4142
#if QLJS_HAVE_WINDOWS_H
4243
#include <quick-lint-js/port/windows.h>
44+
#include <winioctl.h>
4345
#endif
4446

4547
#if QLJS_HAVE_WINDOWS_H
@@ -565,6 +567,80 @@ void delete_posix_symbolic_link_or_exit(const char *path) {
565567
result.error().print_and_exit();
566568
}
567569
}
570+
571+
#if defined(QLJS_FILE_WINDOWS)
572+
void create_windows_junction_or_exit(const char *path, const char *target) {
573+
Monotonic_Allocator memory("create_windows_junction");
574+
575+
std::optional<std::wstring> wpath = mbstring_to_wstring(path);
576+
std::optional<std::wstring> wtarget = mbstring_to_wstring(target);
577+
if (!wpath.has_value() || !wtarget.has_value()) {
578+
std::fprintf(stderr, "fatal: cannot convert path to wide string\n");
579+
std::abort();
580+
}
581+
if (!::CreateDirectoryW(wpath->c_str(), /*lpSecurityAttributes=*/nullptr)) {
582+
std::fprintf(stderr, "fatal: CreateDirectoryW failed\n");
583+
std::abort();
584+
}
585+
Windows_Handle_File file(
586+
::CreateFileW(wpath->c_str(), GENERIC_WRITE,
587+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
588+
/*lpSecurityAttributes=*/nullptr, OPEN_EXISTING,
589+
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
590+
/*hTemplateFile=*/nullptr));
591+
592+
std::size_t reparse_data_data_size =
593+
offsetof(
594+
decltype(
595+
std::declval<::REPARSE_DATA_BUFFER>().MountPointReparseBuffer),
596+
PathBuffer) +
597+
// SubsituteName. +1 for null terminator.
598+
(wtarget->size() + 1) * sizeof(wchar_t) +
599+
// PrintName. Empty string with null terminator.
600+
sizeof(wchar_t);
601+
std::size_t reparse_data_total_size =
602+
offsetof(::REPARSE_DATA_BUFFER, MountPointReparseBuffer) +
603+
reparse_data_data_size;
604+
::REPARSE_DATA_BUFFER *reparse_data =
605+
reinterpret_cast<::REPARSE_DATA_BUFFER *>(memory.allocate(
606+
reparse_data_total_size, alignof(::REPARSE_DATA_BUFFER)));
607+
reparse_data->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
608+
reparse_data->ReparseDataLength = reparse_data_data_size;
609+
reparse_data->Reserved = 0;
610+
611+
// Write the null-terminated SubstituteName (*wtarget) followed by the
612+
// null-terminated PrintName (L"").
613+
{
614+
wchar_t *path_buffer = reparse_data->MountPointReparseBuffer.PathBuffer;
615+
auto current_offset = [&]() -> ::USHORT {
616+
return narrow_cast<::USHORT>(
617+
(reinterpret_cast<char *>(path_buffer) -
618+
reinterpret_cast<char *>(
619+
reparse_data->MountPointReparseBuffer.PathBuffer)));
620+
};
621+
reparse_data->MountPointReparseBuffer.SubstituteNameOffset =
622+
current_offset();
623+
reparse_data->MountPointReparseBuffer.SubstituteNameLength =
624+
wtarget->size() * sizeof(wchar_t);
625+
path_buffer = std::copy(wtarget->begin(), wtarget->end(), path_buffer);
626+
*path_buffer++ = L'\0';
627+
628+
reparse_data->MountPointReparseBuffer.PrintNameOffset = current_offset();
629+
reparse_data->MountPointReparseBuffer.PrintNameLength = 0;
630+
*path_buffer++ = L'\0';
631+
}
632+
633+
if (!::DeviceIoControl(file.get(), FSCTL_SET_REPARSE_POINT, reparse_data,
634+
narrow_cast<::DWORD>(reparse_data_total_size),
635+
/*lpOutBuffer=*/nullptr,
636+
/*nOutBufferSize=*/0,
637+
/*lpBytesReturned=*/nullptr,
638+
/*lpOverlapped=*/nullptr)) {
639+
std::fprintf(stderr, "fatal: DeviceIoControl failed\n");
640+
std::abort();
641+
}
642+
}
643+
#endif
568644
}
569645

570646
#endif

src/quick-lint-js/io/file.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ void create_posix_file_symbolic_link_or_exit(const char *path,
115115

116116
Result<void, Delete_File_IO_Error> delete_posix_symbolic_link(const char *path);
117117
void delete_posix_symbolic_link_or_exit(const char *path);
118+
119+
// target must begin with \??\.
120+
void create_windows_junction_or_exit(const char *path, const char *target);
121+
118122
}
119123

120124
#endif

test/test-file-canonical.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,102 @@ TEST_F(Test_File_Canonical,
723723
EXPECT_SAME_FILE(canonical->path(), input_path);
724724
}
725725

726+
#if defined(_WIN32)
727+
TEST_F(Test_File_Canonical, canonical_path_resolves_directory_junctions) {
728+
std::string temp_dir = this->make_temporary_directory();
729+
create_directory_or_exit(temp_dir + "/realdir");
730+
create_windows_junction_or_exit(
731+
(temp_dir + "/linkdir").c_str(),
732+
("\\??\\" + temp_dir + QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "realdir")
733+
.c_str());
734+
write_file_or_exit(temp_dir + "/realdir/temp.js", u8""_sv);
735+
736+
std::string input_path = temp_dir + "/linkdir/temp.js";
737+
Result<Canonical_Path_Result, Canonicalize_Path_IO_Error> canonical =
738+
canonicalize_path(input_path);
739+
ASSERT_TRUE(canonical.ok()) << canonical.error().to_string();
740+
741+
EXPECT_THAT(std::string(canonical->path()),
742+
AnyOf(HasSubstr("/realdir/"), HasSubstr("\\realdir\\")));
743+
EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("/linkdir/")));
744+
EXPECT_THAT(std::string(canonical->path()), Not(HasSubstr("\\linkdir\\")));
745+
EXPECT_SAME_FILE(canonical->path(), temp_dir + "/realdir/temp.js");
746+
EXPECT_SAME_FILE(canonical->path(), input_path);
747+
}
748+
#endif
749+
750+
#if defined(_WIN32)
751+
TEST_F(Test_File_Canonical, symlink_with_dot_dot_escapes_symlinked_dir) {
752+
// $temp_dir\dir [directory]
753+
// $temp_dir\dir\file.txt [file]
754+
// $temp_dir\dir\subdir [directory]
755+
// $temp_dir\dir\subdir\filelink.txt [symlink] -> ..\file.txt
756+
// $temp_dir\linkdir [junction] -> $temp_dir\dir\subdir
757+
// $temp_dir\file.txt [file]
758+
//
759+
// $temp_dir\linkdir\filelink.txt
760+
// is the same as $temp_dir\dir\subdir\..\file.txt
761+
// is the same as $temp_dir\dir\file.txt
762+
763+
std::string temp_dir = this->make_temporary_directory();
764+
create_directory_or_exit(temp_dir + "/dir");
765+
write_file_or_exit(temp_dir + "/dir/file.txt", u8"$tempdir/dir/file.txt");
766+
create_directory_or_exit(temp_dir + "/dir/subdir");
767+
create_posix_file_symbolic_link_or_exit(
768+
(temp_dir + "/dir/subdir/filelink.txt").c_str(), "..\\file.txt");
769+
create_posix_directory_symbolic_link(
770+
(temp_dir + "/linkdir").c_str(),
771+
"dir" QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "subdir");
772+
write_file_or_exit(temp_dir + "/file.txt", u8"$tempdir/file.txt");
773+
774+
std::string input_path = temp_dir + "/linkdir/filelink.txt";
775+
Result<Canonical_Path_Result, Canonicalize_Path_IO_Error> canonical =
776+
canonicalize_path(input_path);
777+
ASSERT_TRUE(canonical.ok()) << canonical.error().to_string();
778+
779+
EXPECT_SAME_FILE(canonical->path(), temp_dir + "/dir/subdir/../file.txt");
780+
EXPECT_SAME_FILE(canonical->path(), temp_dir + "/dir/file.txt");
781+
}
782+
#endif
783+
784+
#if defined(_WIN32)
785+
// TODO(#1200): Fix canonicalize_file when it encounters junctions.
786+
TEST_F(Test_File_Canonical, DISABLED_symlink_with_dot_dot_escapes_junction) {
787+
// $temp_dir\dir [directory]
788+
// $temp_dir\dir\file.txt [file]
789+
// $temp_dir\dir\subdir [directory]
790+
// $temp_dir\dir\subdir\filelink.txt [symlink] -> ..\file.txt
791+
// $temp_dir\linkdir [junction] -> $temp_dir\dir\subdir
792+
// $temp_dir\file.txt [file]
793+
//
794+
// $temp_dir\linkdir\filelink.txt
795+
// is the same as $temp_dir\linkdir\..\file.txt
796+
// is the same as $temp_dir\file.txt
797+
798+
std::string temp_dir = this->make_temporary_directory();
799+
create_directory_or_exit(temp_dir + "/dir");
800+
write_file_or_exit(temp_dir + "/dir/file.txt", u8"$tempdir/dir/file.txt");
801+
create_directory_or_exit(temp_dir + "/dir/subdir");
802+
create_posix_file_symbolic_link_or_exit(
803+
(temp_dir + "/dir/subdir/filelink.txt").c_str(), "..\\file.txt");
804+
create_windows_junction_or_exit(
805+
(temp_dir + "/linkdir").c_str(),
806+
("\\??\\" + temp_dir +
807+
QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR
808+
"dir" QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "subdir")
809+
.c_str());
810+
write_file_or_exit(temp_dir + "/file.txt", u8"$tempdir/file.txt");
811+
812+
std::string input_path = temp_dir + "/linkdir/filelink.txt";
813+
Result<Canonical_Path_Result, Canonicalize_Path_IO_Error> canonical =
814+
canonicalize_path(input_path);
815+
ASSERT_TRUE(canonical.ok()) << canonical.error().to_string();
816+
817+
EXPECT_SAME_FILE(canonical->path(), temp_dir + "/linkdir/../file.txt");
818+
EXPECT_SAME_FILE(canonical->path(), temp_dir + "/file.txt");
819+
}
820+
#endif
821+
726822
TEST_F(Test_File_Canonical,
727823
canonical_path_resolves_directory_relative_symlinks) {
728824
std::string temp_dir = this->make_temporary_directory();

0 commit comments

Comments
 (0)