Skip to content

Commit

Permalink
feat(io): teach list_directory/delete_directory_recursive symlinks
Browse files Browse the repository at this point in the history
Teach some test-centric APIs about POSIX symlinks and Windows symlinks
(but not other kinds of reparse points). This will make it easier to
test symlink support in other parts of quick-lint-js, such as in
canonicalize_path.
  • Loading branch information
strager committed Feb 11, 2024
1 parent 60c8d45 commit 6179ffc
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 25 deletions.
89 changes: 89 additions & 0 deletions src/quick-lint-js/io/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ std::string Write_File_IO_Error::to_string() const {
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();
}

[[noreturn]] void Symlink_IO_Error::print_and_exit() const {
std::fprintf(stderr, "error: %s\n", this->to_string().c_str());
std::exit(1);
}

bool operator==(const Read_File_IO_Error &lhs, const Read_File_IO_Error &rhs) {
return lhs.path == rhs.path && lhs.io_error == rhs.io_error;
}
Expand Down Expand Up @@ -411,6 +421,85 @@ bool file_ids_equal(const ::FILE_ID_INFO &a, const ::FILE_ID_INFO &b) {
std::memcmp(&b.FileId, &a.FileId, sizeof(b.FileId)) == 0;
}
#endif

namespace {
Result<void, Symlink_IO_Error> create_posix_symbolic_link(
const char *path, const char *target, [[maybe_unused]] bool is_directory) {
#if defined(QLJS_FILE_POSIX)
int rc = ::symlink(target, path);
if (rc != 0) {
return failed_result(Symlink_IO_Error{
.path = path,
.target = target,
.io_error = POSIX_File_IO_Error{errno},
});
}
return {};
#elif defined(QLJS_FILE_WINDOWS)
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()) {
return failed_result(Symlink_IO_Error{
.path = path,
.target = target,
.io_error = Windows_File_IO_Error{ERROR_INVALID_PARAMETER},
});
}

// TODO(strager): Ensure a relative target path creates relative symlinks, not
// absolute symlinks resolved to the current working directory.
//
// FIXME(strager): With SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE,
// ::CreateSymbolicLinkW can fail with ERROR_INVALID_PARAMETER or maybe
// something else. Need to test more Windows versions.
//
// NOTE(strager): ::CreateSymbolicLinkW fails with ERROR_PRIVILEGE_NOT_HELD
// (1314) if SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE is not set.
if (!::CreateSymbolicLinkW(
wpath->c_str(), wtarget->c_str(),
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE |
(is_directory ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0))) {
return failed_result(Symlink_IO_Error{
.path = path,
.target = target,
.io_error = Windows_File_IO_Error{::GetLastError()},
});
}

return {};
#else
#error "Unknown platform"
#endif
}
}

Result<void, Symlink_IO_Error> create_posix_directory_symbolic_link(
const char *path, const char *target) {
return create_posix_symbolic_link(path, target, /*is_directory=*/true);
}

Result<void, Symlink_IO_Error> create_posix_file_symbolic_link(
const char *path, const char *target) {
return create_posix_symbolic_link(path, target, /*is_directory=*/false);
}

void create_posix_directory_symbolic_link_or_exit(const char *path,
const char *target) {
Result<void, Symlink_IO_Error> result =
create_posix_directory_symbolic_link(path, target);
if (!result.ok()) {
result.error().print_and_exit();
}
}

void create_posix_file_symbolic_link_or_exit(const char *path,
const char *target) {
Result<void, Symlink_IO_Error> result =
create_posix_file_symbolic_link(path, target);
if (!result.ok()) {
result.error().print_and_exit();
}
}
}

#endif
Expand Down
27 changes: 27 additions & 0 deletions src/quick-lint-js/io/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ struct Write_File_IO_Error {
[[noreturn]] void print_and_exit() const;
};

struct Symlink_IO_Error {
std::string path;
std::string target;
Platform_File_IO_Error io_error;

std::string to_string() const;
[[noreturn]] void print_and_exit() const;
};

Result<Padded_String, Read_File_IO_Error> read_file(const std::string &path);
Result<Padded_String, Read_File_IO_Error> read_file(const char *path);
Result<Padded_String, Read_File_IO_Error> read_file(const char *path,
Expand Down Expand Up @@ -77,6 +86,24 @@ Result<Platform_File, Write_File_IO_Error> open_file_for_writing(
#if QLJS_HAVE_WINDOWS_H
bool file_ids_equal(const ::FILE_ID_INFO &, const ::FILE_ID_INFO &);
#endif

// Create a POSIX/UNIX-style symbolic link.
//
// On POSIX platforms like Linux and macOS, calls ::symlink.
// create_posix_directory_symbolic_link and create_posix_file_symbolic_link
// behave identically.
//
// On Windows, calls ::CreateSymbolicLinkW with
// SYMBOLIC_LINK_FLAG_DIRECTORY (for create_posix_directory_symbolic_link) or
// without SYMBOLIC_LINK_FLAG_DIRECTORY (for create_posix_file_symbolic_link).
Result<void, Symlink_IO_Error> create_posix_directory_symbolic_link(
const char *path, const char *target);
Result<void, Symlink_IO_Error> create_posix_file_symbolic_link(
const char *path, const char *target);
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);
}

#endif
Expand Down
57 changes: 43 additions & 14 deletions src/quick-lint-js/io/temporary-directory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <quick-lint-js/port/have.h>
#include <quick-lint-js/port/unreachable.h>
#include <quick-lint-js/port/warning.h>
#include <quick-lint-js/util/enum.h>
#include <quick-lint-js/util/utf-16.h>
#include <random>
#include <string>
Expand Down Expand Up @@ -354,7 +355,7 @@ Result<void, Platform_File_IO_Error> list_directory(

Result<void, Platform_File_IO_Error> list_directory(
const char *directory,
Temporary_Function_Ref<void(const char *, bool is_directory)> visit_file) {
Temporary_Function_Ref<void(const char *, File_Type_Flags)> visit_file) {
#if QLJS_HAVE_WINDOWS_H
return list_directory_raw(directory, [&](::WIN32_FIND_DATAW &entry) -> void {
// TODO(strager): Reduce allocations.
Expand All @@ -364,9 +365,17 @@ Result<void, Platform_File_IO_Error> list_directory(
QLJS_UNIMPLEMENTED();
}
if (!is_dot_or_dot_dot(entry_name->c_str())) {
bool is_directory = (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ==
FILE_ATTRIBUTE_DIRECTORY;
visit_file(entry_name->c_str(), is_directory);
File_Type_Flags flags = File_Type_Flags::none;
if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ==
FILE_ATTRIBUTE_DIRECTORY) {
flags = enum_set_flags(flags, File_Type_Flags::is_directory);
}
if ((entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ==
FILE_ATTRIBUTE_REPARSE_POINT) {
flags = enum_set_flags(
flags, File_Type_Flags::is_symbolic_link_or_reparse_point);
}
visit_file(entry_name->c_str(), flags);
}
});
#elif QLJS_HAVE_DIRENT_H
Expand All @@ -375,8 +384,9 @@ Result<void, Platform_File_IO_Error> list_directory(
if (is_dot_or_dot_dot(entry->d_name)) {
return;
}
bool is_directory;
if (entry->d_type == DT_UNKNOWN) {
File_Type_Flags flags = File_Type_Flags::none;
switch (entry->d_type) {
case DT_UNKNOWN: {
temp_path.clear();
temp_path += directory;
temp_path += QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR;
Expand All @@ -387,14 +397,31 @@ Result<void, Platform_File_IO_Error> list_directory(
if (errno == ENOENT) {
return;
}
is_directory = false;
} else {
is_directory = S_ISDIR(s.st_mode);
if (S_ISDIR(s.st_mode)) {
flags = enum_set_flags(flags, File_Type_Flags::is_directory);
}
if (S_ISLNK(s.st_mode)) {
flags = enum_set_flags(
flags, File_Type_Flags::is_symbolic_link_or_reparse_point);
}
}
} else {
is_directory = entry->d_type == DT_DIR;
break;
}

case DT_DIR:
flags = enum_set_flags(flags, File_Type_Flags::is_directory);
break;

case DT_LNK:
flags = enum_set_flags(
flags, File_Type_Flags::is_symbolic_link_or_reparse_point);
break;

default:
break;
}
visit_file(entry->d_name, is_directory);
visit_file(entry->d_name, flags);
});
#else
#error "Unsupported platform"
Expand All @@ -419,14 +446,16 @@ void list_directory_recursively(const char *directory,
std::size_t path_length = this->path.size();

auto visit_child = [&](const char *child_name,
bool is_directory) -> void {
File_Type_Flags flags) -> void {
this->path.resize(path_length);
this->path += QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR;
this->path += child_name;
if (is_directory) {
if (enum_has_flags(flags, File_Type_Flags::is_directory) &&
!enum_has_flags(
flags, File_Type_Flags::is_symbolic_link_or_reparse_point)) {
this->recurse(depth + 1);
} else {
this->visitor.visit_file(path);
this->visitor.visit_file(path, flags);
}
};

Expand Down
14 changes: 12 additions & 2 deletions src/quick-lint-js/io/temporary-directory.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ void create_directory_or_exit(const std::string& path);
Result<std::string, Platform_File_IO_Error> make_timestamped_directory(
std::string_view parent_directory, const char* format);

enum class File_Type_Flags : std::uint8_t {
none = 0,

is_directory = 1 << 0,
is_symbolic_link_or_reparse_point = 1 << 1,
};

// Call visit_file for each child of the given directory.
//
// '.' and '..' are excluded.
Expand All @@ -48,7 +55,7 @@ Result<void, Platform_File_IO_Error> list_directory(
Temporary_Function_Ref<void(const char*)> visit_file);
Result<void, Platform_File_IO_Error> list_directory(
const char* directory,
Temporary_Function_Ref<void(const char*, bool is_directory)> visit_file);
Temporary_Function_Ref<void(const char*, File_Type_Flags)> visit_file);

QLJS_WARNING_PUSH
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69210
Expand All @@ -61,7 +68,10 @@ class List_Directory_Visitor {
// 'directory' given to list_directory_recursively.
//
// visit_file is not called for '.' or '..' entries.
virtual void visit_file(const std::string& path) = 0;
//
// On Windows, visit_file is called for directory symbolic links and directory
// reparse points.
virtual void visit_file(const std::string& path, File_Type_Flags flags) = 0;

// Called before descending into a directory.
virtual void visit_directory_pre(const std::string& path);
Expand Down
6 changes: 4 additions & 2 deletions test/quick-lint-js/file-matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ inline ::testing::AssertionResult assert_same_file(
return assert_same_file(lhs_expr, rhs_expr, lhs_path.c_str(), rhs_path);
}

// Does not follow symlinks.
inline ::testing::AssertionResult assert_file_does_not_exist(const char* expr,
const char* path) {
bool exists;
#if QLJS_HAVE_STD_FILESYSTEM
exists = std::filesystem::exists(std::filesystem::path(path));
exists = std::filesystem::exists(
std::filesystem::symlink_status(std::filesystem::path(path)));
#elif QLJS_HAVE_SYS_STAT_H
struct ::stat s;
if (::stat(path, &s) == 0) {
if (::lstat(path, &s) == 0) {
exists = true;
} else {
switch (errno) {
Expand Down
21 changes: 17 additions & 4 deletions test/quick-lint-js/filesystem-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <quick-lint-js/port/unreachable.h>
#include <quick-lint-js/port/windows-error.h>
#include <quick-lint-js/util/cast.h>
#include <quick-lint-js/util/enum.h>
#include <quick-lint-js/util/math-overflow.h>
#include <quick-lint-js/util/utf-16.h>
#include <string>
Expand All @@ -35,7 +36,8 @@
namespace quick_lint_js {
void delete_directory_recursive(const std::string& path) {
struct Delete_Visitor : public List_Directory_Visitor {
void visit_file(const std::string& path) override {
void visit_file(const std::string& path,
[[maybe_unused]] File_Type_Flags flags) override {
#if QLJS_HAVE_UNISTD_H
int rc = std::remove(path.c_str());
if (rc != 0) {
Expand All @@ -50,9 +52,20 @@ void delete_directory_recursive(const std::string& path) {
path.c_str());
return;
}
if (!::DeleteFileW(wpath->c_str())) {
std::fprintf(stderr, "warning: failed to delete %s: %s\n", path.c_str(),
windows_error_message(::GetLastError()).c_str());
if (enum_has_flags(flags, File_Type_Flags::is_directory)) {
QLJS_ASSERT(enum_has_flags(
flags, File_Type_Flags::is_symbolic_link_or_reparse_point));
if (!::RemoveDirectoryW(wpath->c_str())) {
std::fprintf(
stderr, "warning: failed to delete directory symlink %s: %s\n",
path.c_str(), windows_error_message(::GetLastError()).c_str());
}
} else {
if (!::DeleteFileW(wpath->c_str())) {
std::fprintf(stderr, "warning: failed to delete %s: %s\n",
path.c_str(),
windows_error_message(::GetLastError()).c_str());
}
}
#else
#error "Unsupported platform"
Expand Down
Loading

0 comments on commit 6179ffc

Please sign in to comment.