Skip to content

Commit

Permalink
feat(io): support Windows junctions
Browse files Browse the repository at this point in the history
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).)
  • Loading branch information
strager committed Feb 12, 2024
1 parent 6c8f89d commit 137cde4
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 2 deletions.
29 changes: 27 additions & 2 deletions src/quick-lint-js/io/file-canonical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void, Canonicalizing_Path_IO_Error> r = process_start_of_path();
if (!r.ok()) return r.propagate();

break;
}

case IO_REPARSE_TAG_SYMLINK: {
auto &symlink = reparse_data->SymbolicLinkReparseBuffer;
Expand Down
73 changes: 73 additions & 0 deletions src/quick-lint-js/io/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <cstring>
#include <optional>
#include <quick-lint-js/assert.h>
#include <quick-lint-js/container/monotonic-allocator.h>
#include <quick-lint-js/io/file-handle.h>
#include <quick-lint-js/io/file.h>
#include <quick-lint-js/port/char8.h>
Expand Down Expand Up @@ -40,6 +41,7 @@

#if QLJS_HAVE_WINDOWS_H
#include <quick-lint-js/port/windows.h>
#include <winioctl.h>
#endif

#if QLJS_HAVE_WINDOWS_H
Expand Down Expand Up @@ -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<std::wstring> wpath = mbstring_to_wstring(path);
std::optional<std::wstring> 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<char *>(path_buffer) -
reinterpret_cast<char *>(
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
Expand Down
4 changes: 4 additions & 0 deletions src/quick-lint-js/io/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ void create_posix_file_symbolic_link_or_exit(const char *path,

Result<void, Delete_File_IO_Error> 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
Expand Down
96 changes: 96 additions & 0 deletions test/test-file-canonical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_Path_Result, Canonicalize_Path_IO_Error> 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_Path_Result, Canonicalize_Path_IO_Error> 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_Path_Result, Canonicalize_Path_IO_Error> 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();
Expand Down

0 comments on commit 137cde4

Please sign in to comment.