This patch adds symlink support on Windows. I tested it on x86_64-w64-mingw32 with msys2. A few notes about this patch:
1. Symlinks can only be created while running as admin, or if Developer Mode is enabled on Windows 10/11. 2. Symlinks on Windows are either file symlinks or directory symlinks. I decided that fs::create_symlink should only be used for creating file symlinks, unlike the implementation on Linux. I am not sure if this was the correct decision. 3. Windows seems to process dotdots in paths before it resolves symlinks. This results in errors. For example, in test03 for fs::canonical, there is a directory setup with dir, dir/foo, and dir/bar. dir/foo contains a symlink, baz, which points to ../bar. The test checks that fs::canonical(dir/foo/baz/../bar) equals dir/foo/../bar/../bar or dir/bar. However, Windows interprets the path as dir/foo/bar. This is the reason my patch treats paths with a dotdot carefully in fs::absolute and fs::canonical. This is my first contribution to libstdc++ and I would prefer the FSF hold copyright of the code rather than me. libstdc++-v3/Changelog: * src/c++17/fs_ops.cc: Include <winioctl.h> for FSCTL_GET_REPARSE_POINT. (path_starts_with_dotdot): New helper function for fs::absolute. (fs::absolute): Call GetFullPathNameW on part of path before ".." and then append the rest to the result. (fs::canonical): Don't use lexically_normal in call to fs::absolute. Don't check if path exists if it contains "..". Check that component of path exists if it isn't empty or "." or "..". (windows_create_symlink): New helper function for fs::create_symlink and fs::create_directory_symlink. (fs::create_directory_symlink): Call windows_create_symlink on Windows. (fs::create_symlink): Call windows_create_symlink on Windows. (auto_win_file_handle::auto_win_file_handle): Add follow_symlink parameter to control whether the handle should open the symlink or the target, with a default value of true. (windows_read_symlink_handle): New helper function for fs::read_symlink. (fs::read_symlink): Call windows_read_symlink_handle on Windows. (fs::remove): Call RemoveDirectoryW only for directories, and DeleteFileW for regular files, but attempt both for symlinks. (fs::remove_all): Return immediately if path is empty. Check if path points to a symlink, and if so, remove the symlink using fs::remove. * src/filesystem/ops-common.h: Define S_IFLNK and S_ISLNK. (__gnu_posix::__open_for_stat): New helper function for stat and lstat. (__gnu_posix::__is_handle_symlink): New helper function for stat, lstat, and fs::read_symlink. (__gnu_posix::__stat_windows): New helper function for stat and lstat. (__gnu_posix::stat, __gnu_posix::lstat): Use __stat_windows to properly follow or not follow symlinks, and check if file is a symlink. * testsuite/27_io/filesystem/operations/canonical.cc (test03): Use fs::create_directory_symlink instead of fs::create_symlink. * testsuite/27_io/filesystem/operations/copy.cc (test02): Create a symlink to temporary file instead of ".". Use fs::exists(symlink_status()) instead of fs::exists for symlinks. * testsuite/27_io/filesystem/operations/weakly_canonical.cc (test01): Use fs::create_directory_symlink instead of fs::create_symlink. * testsuite/util/testsuite_fs.h: Do not define NO_SYMLINKS on Windows. --- libstdc++-v3/src/c++17/fs_ops.cc | 289 +++++++++++++++++- libstdc++-v3/src/filesystem/ops-common.h | 75 ++++- .../27_io/filesystem/operations/canonical.cc | 2 +- .../27_io/filesystem/operations/copy.cc | 7 +- .../filesystem/operations/weakly_canonical.cc | 2 +- libstdc++-v3/testsuite/util/testsuite_fs.h | 2 + 6 files changed, 357 insertions(+), 20 deletions(-) diff --git a/libstdc++-v3/src/c++17/fs_ops.cc b/libstdc++-v3/src/c++17/fs_ops.cc index 4f188153ae3..901ed395fe5 100644 --- a/libstdc++-v3/src/c++17/fs_ops.cc +++ b/libstdc++-v3/src/c++17/fs_ops.cc @@ -56,6 +56,7 @@ #ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS # define WIN32_LEAN_AND_MEAN # include <windows.h> +# include <winioctl.h> // FSCTL_GET_REPARSE_POINT #endif #define _GLIBCXX_BEGIN_NAMESPACE_FILESYSTEM namespace filesystem { @@ -78,6 +79,23 @@ fs::absolute(const path& p) return ret; } +#ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS +namespace +{ + bool + path_starts_with_dotdot(std::wstring_view p) + { + if (p.size() < 2) + return false; + if (p[0] == L'.' && p[1] == L'.') + return true; + if (p.size() < 3 || !(p[0] == L'/' || p[0] == L'\\')) + return false; + return p[1] == L'.' && p[2] == L'.'; + } +} +#endif + fs::path fs::absolute(const path& p, error_code& ec) { @@ -97,6 +115,8 @@ fs::absolute(const path& p, error_code& ec) #ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS // s must remain null-terminated wstring_view s = p.native(); + path after_dotdot_p; + path before_dotdot_p; if (p.has_root_directory()) // implies !p.has_root_name() { @@ -108,6 +128,25 @@ fs::absolute(const path& p, error_code& ec) s.remove_prefix(std::min(s.length(), pos) - 1); } + + // GetFullPathNameW does not work correctly with a .. right after a symlink, + // so if we have a .. in the path, run GetFullPathNameW on the part before + // the dotdot, and then append the rest. + if (!path_starts_with_dotdot(s) && s.find(L"..") != wstring_view::npos) + { + bool before_dotdot = true; + for (const auto& component : p) + { + if (before_dotdot && !wcscmp(component.c_str(), L"..")) + before_dotdot = false; + if (before_dotdot) + before_dotdot_p /= component; + else + after_dotdot_p /= component; + } + s = before_dotdot_p.native(); + } + uint32_t len = 1024; wstring buf; do @@ -123,6 +162,9 @@ fs::absolute(const path& p, error_code& ec) ec = __last_system_error(); else ret = std::move(buf); + + if (!after_dotdot_p.empty()) + ret /= after_dotdot_p; #else ret = current_path(ec); ret /= p; @@ -162,11 +204,7 @@ fs::path fs::canonical(const path& p, error_code& ec) { path result; -#ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS - const path pa = absolute(p.lexically_normal(), ec); -#else const path pa = absolute(p, ec); -#endif if (ec) return result; @@ -192,7 +230,17 @@ fs::canonical(const path& p, error_code& ec) } #endif +#if _GLIBCXX_FILESYSTEM_IS_WINDOWS + // Windows handles relative paths after symlinks incorrectly. + // If we have a .. after a symlink, the .. will be cancelled before the + // symlink is resolved. For example, if we have baz -> ../bar, + // and we have the path dir/foo/baz/../bar, Linux would interpret this as + // dir/foo/../bar/../bar = dir/bar, but Windows thinks the path is + // dir/foo/bar. This leads to possible false negatives using exists. + if (pa.native().find(L"..") == std::wstring::npos && !exists(pa, ec)) +#else if (!exists(pa, ec)) +#endif { if (!ec) ec = make_error_code(std::errc::no_such_file_or_directory); @@ -234,7 +282,15 @@ fs::canonical(const path& p, error_code& ec) { result /= f; - if (is_symlink(result, ec)) + auto st = symlink_status(result, ec); + if (!exists(st)) + { + if (!ec) + ec.assign(ENOENT, std::generic_category()); + result.clear(); + return result; + } + if (is_symlink(st)) { path link = read_symlink(result, ec); if (!ec) @@ -644,6 +700,42 @@ fs::create_directory(const path& p, const path& attributes, #endif } +namespace +{ + void + windows_create_symlink(const fs::path& to, const fs::path& new_symlink, + const fs::file_type target_type, + std::error_code& ec) noexcept + { + auto symlink_flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (target_type == fs::file_type::directory) + symlink_flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + // Windows can't handle relative symlinks with non-preferred slashes. + // Creating the symlink will succeed, but the symlink won't resolve + // correctly in later operations. + const fs::path* preferred_to = &to; + fs::path to2; + if (to.native().find(L'/') != std::string::npos) + { + __try + { + to2 = to; + to2.make_preferred(); + preferred_to = &to2; + } + __catch (const std::bad_alloc&) + { + ec = std::make_error_code(std::errc::not_enough_memory); + return; + } + } + if (CreateSymbolicLinkW(new_symlink.c_str(), preferred_to->c_str(), + symlink_flags)) + ec.clear(); + else + ec = std::__last_system_error(); + } +} void fs::create_directory_symlink(const path& to, const path& new_symlink) @@ -660,7 +752,7 @@ fs::create_directory_symlink(const path& to, const path& new_symlink, error_code& ec) noexcept { #ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS - ec = std::make_error_code(std::errc::function_not_supported); + windows_create_symlink(to, new_symlink, file_type::directory, ec); #else create_symlink(to, new_symlink, ec); #endif @@ -715,6 +807,8 @@ fs::create_symlink(const path& to, const path& new_symlink, ec.assign(errno, std::generic_category()); else ec.clear(); +#elif _GLIBCXX_FILESYSTEM_IS_WINDOWS + windows_create_symlink(to, new_symlink, file_type::regular, ec); #else ec = std::make_error_code(std::errc::function_not_supported); #endif @@ -829,10 +923,14 @@ namespace struct auto_win_file_handle { explicit - auto_win_file_handle(const wchar_t* p, std::error_code& ec) noexcept + auto_win_file_handle(const wchar_t* p, std::error_code& ec, + const bool follow_symlink = true) noexcept : handle(CreateFileW(p, 0, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, - 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0)), + 0, OPEN_EXISTING, + (FILE_FLAG_BACKUP_SEMANTICS + | (FILE_FLAG_OPEN_REPARSE_POINT * !follow_symlink)), + 0)), ec(ec) { if (handle == INVALID_HANDLE_VALUE) @@ -1193,6 +1291,100 @@ fs::proximate(const path& p, const path& base, error_code& ec) return result; } +#if _GLIBCXX_FILESYSTEM_IS_WINDOWS +namespace +{ + // This would be defined in <ntifs.h> if it were included. + 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; + + void + windows_read_symlink_handle(auto_win_file_handle& link_handle, + std::error_code& ec, + fs::path& result) + { + REPARSE_DATA_BUFFER * reparse_buffer = nullptr; + std::unique_ptr<char[]> big_buffer; + + // Allocate enough memory on the stack to get the reparse data + // plus a 260 character path. Should be sufficient in most cases. + // Allocate an extra wchar_t to ensure we can manually null terminate. + static constexpr size_t small_buffer_size = sizeof(REPARSE_DATA_BUFFER) + + 260 * sizeof(wchar_t); + char small_buffer[small_buffer_size + sizeof(wchar_t)]; + reparse_buffer = reinterpret_cast<REPARSE_DATA_BUFFER*>(&small_buffer[0]); + long unsigned int bytes_returned, big_buffer_size; + + // Attempt to get the reparse data with the buffer on the stack + // before allocating the exact amount needed on the heap. + bool got_reparse_data = DeviceIoControl(link_handle.handle, + FSCTL_GET_REPARSE_POINT, + nullptr, 0, + &small_buffer, small_buffer_size, + &bytes_returned, nullptr); + + int last_error = GetLastError(); + if (!got_reparse_data && last_error == ERROR_MORE_DATA) + { + big_buffer_size = bytes_returned; + big_buffer.reset(new char[big_buffer_size + sizeof(wchar_t)]); + got_reparse_data = DeviceIoControl(link_handle.handle, + FSCTL_GET_REPARSE_POINT, + nullptr, 0, + big_buffer.get(), big_buffer_size, + &bytes_returned, nullptr); + if (!got_reparse_data) + { + ec = std::__last_system_error(); + return; + } + + reparse_buffer + = reinterpret_cast<REPARSE_DATA_BUFFER*>(big_buffer.get()); + + } + else + { + if (!got_reparse_data) + { + ec = std::__last_system_error(); + return; + } + } + + ec.clear(); + auto& symlink_buffer = reparse_buffer->SymbolicLinkReparseBuffer; + wchar_t* target_name = &symlink_buffer.PathBuffer[0]; + target_name += symlink_buffer.PrintNameOffset / sizeof(wchar_t); + target_name[symlink_buffer.PrintNameLength / sizeof(wchar_t)] = L'\0'; + result = target_name; + } +}; +#endif // _GLIBCXX_FILESYSTEM_IS_WINDOWS + fs::path fs::read_symlink(const path& p) { @@ -1248,6 +1440,25 @@ fs::path fs::read_symlink(const path& p, error_code& ec) bufsz *= 2; } while (true); +#elif _GLIBCXX_FILESYSTEM_IS_WINDOWS + auto_win_file_handle link_handle(p.c_str(), ec, false); + if (!link_handle) + return result; + + int is_symlink = posix::__is_handle_symlink(link_handle.handle); + if (is_symlink == -1) + { + ec = __last_system_error(); + return result; + } + + if (!is_symlink) + { + ec.assign(EINVAL, std::generic_category()); + return result; + } + + windows_read_symlink_handle(link_handle, ec, result); #else ec = std::make_error_code(std::errc::function_not_supported); #endif @@ -1291,8 +1502,14 @@ fs::remove(const path& p, error_code& ec) noexcept auto st = symlink_status(p, ec); if (exists(st)) { - if ((is_directory(p, ec) && RemoveDirectoryW(p.c_str())) - || DeleteFileW(p.c_str())) + if ((is_directory(st) || is_symlink(st)) + && RemoveDirectoryW(p.c_str())) + { + ec.clear(); + return true; + } + else if ((is_regular_file(st) || is_symlink(st)) + && DeleteFileW(p.c_str())) { ec.clear(); return true; @@ -1320,6 +1537,30 @@ std::uintmax_t fs::remove_all(const path& p) { error_code ec; +#if _GLIBCXX_FILESYSTEM_IS_WINDOWS + if (p.empty()) + return 0; + // The current opendir implementation on Windows always follows an initial + // symlink. Therefore, if remove_all is called on a symlink, + // the target is removed. Call remove if we have a symlink. + auto p_status = symlink_status(p, ec); + if (!exists(p_status)) + { + int err = ec.default_error_condition().value(); + bool not_found = !ec || is_not_found_errno(err); + if (!not_found) + _GLIBCXX_THROW_OR_ABORT(filesystem_error("cannot remove all", + p, ec)); + return 0; + } + if (is_symlink(p_status)) + { + if (!remove(p, ec)) + _GLIBCXX_THROW_OR_ABORT(filesystem_error("cannot remove all", + p, ec)); + return 1; + } +#endif uintmax_t count = 0; recursive_directory_iterator dir(p, directory_options{64|128}, ec); switch (ec.value()) // N.B. assumes ec.category() == std::generic_category() @@ -1363,6 +1604,34 @@ fs::remove_all(const path& p) std::uintmax_t fs::remove_all(const path& p, error_code& ec) { +#if _GLIBCXX_FILESYSTEM_IS_WINDOWS + if (p.empty()) + { + ec.clear(); + return 0; + } + // The current opendir implementation on Windows always follows an initial + // symlink. Therefore, if remove_all is called on a symlink, + // the target is removed. Call remove if we have a symlink, then. + auto p_status = symlink_status(p, ec); + if (!exists(p_status)) + { + int err = ec.default_error_condition().value(); + bool not_found = !ec || is_not_found_errno(err); + if (not_found) + { + ec.clear(); + return 0; + } + return -1; + } + if (is_symlink(p_status)) + { + if (remove(p, ec)) + return 1; + return ec ? -1 : 0; + } +#endif uintmax_t count = 0; recursive_directory_iterator dir(p, directory_options{64|128}, ec); switch (ec.value()) // N.B. assumes ec.category() == std::generic_category() diff --git a/libstdc++-v3/src/filesystem/ops-common.h b/libstdc++-v3/src/filesystem/ops-common.h index 4feacfdb932..424bab9b55d 100644 --- a/libstdc++-v3/src/filesystem/ops-common.h +++ b/libstdc++-v3/src/filesystem/ops-common.h @@ -120,15 +120,80 @@ namespace __gnu_posix using stat_type = struct ::__stat64; - inline int stat(const wchar_t* path, stat_type* buffer) - { return ::_wstat64(path, buffer); } +#define S_IFLNK 0xC000 +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) - inline int lstat(const wchar_t* path, stat_type* buffer) + inline HANDLE __open_for_stat(const wchar_t* path, bool following_symlinks) { - // FIXME: symlinks not currently supported - return stat(path, buffer); + constexpr auto share_flags + = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE; + auto file_flags = FILE_FLAG_BACKUP_SEMANTICS; + if (!following_symlinks) + file_flags |= FILE_FLAG_OPEN_REPARSE_POINT; + HANDLE handle; + handle = CreateFileW(path, 0, share_flags, 0, OPEN_EXISTING, file_flags, 0); + + if (handle == INVALID_HANDLE_VALUE) + { + // CreateFileW does not set errno. + std::error_condition generic_error + = std::__last_system_error().default_error_condition(); + errno = generic_error.value(); + } + + return handle; } + // -1 error, 0 not a symlink, 1 a symlink + inline int __is_handle_symlink(HANDLE handle) + { + FILE_ATTRIBUTE_TAG_INFO type_info; + if (!GetFileInformationByHandleEx(handle, FileAttributeTagInfo, + &type_info, sizeof(type_info))) + { + errno = std::__last_system_error().default_error_condition().value(); + return -1; + } + return type_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT + && type_info.ReparseTag == IO_REPARSE_TAG_SYMLINK; + } + + inline int __stat_windows(const wchar_t* path, stat_type* buffer, + bool following_symlinks) + { + HANDLE handle = __open_for_stat(path, following_symlinks); + if (handle == INVALID_HANDLE_VALUE) + return -1; + // Manually check for symlink, because _fstat does not. + int is_symlink = __is_handle_symlink(handle); + if (is_symlink == -1) + { + CloseHandle(handle); + return -1; + } + int fd = ::_open_osfhandle((intptr_t)handle, _O_RDONLY); + if (fd == -1) + { + CloseHandle(handle); + return -1; + } + int stat_result = ::_fstat64(fd, buffer); + if (is_symlink) + { + // Clear the previous file type. + buffer->st_mode &= ~S_IFMT; + buffer->st_mode |= S_IFLNK; + } + ::_close(fd); + return stat_result; + } + + inline int stat(const wchar_t* path, stat_type* buffer) + { return __stat_windows(path, buffer, true); } + + inline int lstat(const wchar_t* path, stat_type* buffer) + { return __stat_windows(path, buffer, false); } + using ::mode_t; inline int chmod(const wchar_t* path, mode_t mode) diff --git a/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc b/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc index 6eb7ce28928..6005e423852 100644 --- a/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc +++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc @@ -118,7 +118,7 @@ test03() const fs::path baz = dir/"foo//../bar///"; #endif #else - fs::create_symlink("../bar", foo/"baz"); + fs::create_directory_symlink("../bar", foo/"baz"); const fs::path baz = dir/"foo//./baz///"; #endif diff --git a/libstdc++-v3/testsuite/27_io/filesystem/operations/copy.cc b/libstdc++-v3/testsuite/27_io/filesystem/operations/copy.cc index 289bef6160b..50c7b0d1577 100644 --- a/libstdc++-v3/testsuite/27_io/filesystem/operations/copy.cc +++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/copy.cc @@ -68,13 +68,14 @@ test02() { #ifndef NO_SYMLINKS const std::error_code bad_ec = make_error_code(std::errc::invalid_argument); + __gnu_test::scoped_file tmp_file; auto from = __gnu_test::nonexistent_path(); std::error_code ec; ec = bad_ec; - fs::create_symlink(".", from, ec); + fs::create_symlink(tmp_file.path, from, ec); VERIFY( !ec ); - VERIFY( fs::exists(from) ); + VERIFY( fs::exists(symlink_status(from)) ); auto to = __gnu_test::nonexistent_path(); ec = bad_ec; @@ -97,7 +98,7 @@ test02() ec = bad_ec; fs::copy(from, to, fs::copy_options::copy_symlinks, ec); VERIFY( !ec ); - VERIFY( fs::exists(to) ); + VERIFY( fs::exists(symlink_status(to)) ); VERIFY( is_symlink(to) ); ec.clear(); diff --git a/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc b/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc index 6085d5568e6..1c63bf4834d 100644 --- a/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc +++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc @@ -40,7 +40,7 @@ test01() fs::path p; #ifndef NO_SYMLINKS - fs::create_symlink("../bar", foo/"bar"); + fs::create_directory_symlink("../bar", foo/"bar"); p = fs::weakly_canonical(dir/"foo//./bar///../biz/."); VERIFY( p == dirc/"biz/" ); diff --git a/libstdc++-v3/testsuite/util/testsuite_fs.h b/libstdc++-v3/testsuite/util/testsuite_fs.h index 9cf400d87db..cc0264b2563 100644 --- a/libstdc++-v3/testsuite/util/testsuite_fs.h +++ b/libstdc++-v3/testsuite/util/testsuite_fs.h @@ -43,8 +43,10 @@ namespace test_fs = std::experimental::filesystem; #endif #ifndef _GLIBCXX_HAVE_SYMLINK +#ifndef _GLIBCXX_FILESYSTEM_IS_WINDOWS #define NO_SYMLINKS #endif +#endif #if !defined (_GLIBCXX_HAVE_SYS_STATVFS_H) \ && !defined (_GLIBCXX_FILESYSTEM_IS_WINDOWS) -- 2.50.0