https://gcc.gnu.org/g:88856ad855ae7a157a1babac467ac3ffe4afb901
commit r17-2010-g88856ad855ae7a157a1babac467ac3ffe4afb901 Author: Adam Wood <[email protected]> Date: Mon Feb 16 14:36:58 2026 -0800 libstdc++: Add symlink support on Windows Tested on x86_64-w64-mingw32 on Windows 11. The the new symlink functions are only defined/enabled on versions of Windows with symlink support. Because path resolution may be different between Windows and POSIX when a dotdot follows a symlink, I also made some edits to tests involving that specific case. Finally, I dealt with the issue that creating symlinks with the unprivileged flag returns an error in earlier versions of Windows by just trying again if ERROR_INVALID_PARAMETER occurs without the unprivileged flag. This is how MSVC STL does it as well. The patch does not support junctions or mount points. libstdc++-v3/Changelog: * src/c++17/fs_ops.cc: Include <winioctl.h> for FSCTL_GET_REPARSE_POINT. Include <ntdef.h> for REPARSE_DATA_BUFFER. (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 [_GLIBCXX_FILESYSTEM_IS_WINDOWS] (S_IFLNK, S_ISLNK): Define. (__detail::__open_for_stat): New helper function for stat and lstat. (__detail::FileType): New enum type. (__detail::__check_handle_type): New helper function for stat and lstat. (__detail::__is_handle_symlink): New helper function for fs::read_symlink. (__detail::__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. Check if NO_SYMLINKS or _GLIBCXX_FILESYSTEM_IS_WINDOWS instead of just checking NO_SYMLINKS when defining baz. * 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. Wrap statements that test a dotdot after a symlink in an ifndef _GLIBCXX_FILESYSTEM_IS_WINDOWS. * testsuite/util/testsuite_fs.h: Do not define NO_SYMLINKS on Windows. Co-authored-by: Jonathan Wakely <[email protected]> Diff: --- libstdc++-v3/src/c++17/fs_ops.cc | 210 ++++++++++++++++++++- libstdc++-v3/src/filesystem/ops-common.h | 106 ++++++++++- .../27_io/filesystem/operations/canonical.cc | 4 +- .../testsuite/27_io/filesystem/operations/copy.cc | 7 +- .../filesystem/operations/weakly_canonical.cc | 18 +- libstdc++-v3/testsuite/util/testsuite_fs.h | 2 +- 6 files changed, 329 insertions(+), 18 deletions(-) diff --git a/libstdc++-v3/src/c++17/fs_ops.cc b/libstdc++-v3/src/c++17/fs_ops.cc index 454962c75219..387869751c58 100644 --- a/libstdc++-v3/src/c++17/fs_ops.cc +++ b/libstdc++-v3/src/c++17/fs_ops.cc @@ -56,6 +56,8 @@ #ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS # define WIN32_LEAN_AND_MEAN # include <windows.h> +# include <winioctl.h> // FSCTL_GET_REPARSE_POINT +# include <ntdef.h> // REPARSE_DATA_BUFFER #endif #define _GLIBCXX_BEGIN_NAMESPACE_FILESYSTEM namespace filesystem { @@ -644,6 +646,52 @@ fs::create_directory(const path& p, const path& attributes, #endif } +#ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS +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 + { +#ifdef SYMBOLIC_LINK_FLAG_DIRECTORY // Implies CreateSymbolicLinkW support. + DWORD symlink_type = target_type == fs::file_type::directory + ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; + // 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_type + | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) + ec.clear(); + else if (GetLastError() == ERROR_INVALID_PARAMETER + && CreateSymbolicLinkW(new_symlink.c_str(), preferred_to->c_str(), + symlink_type)) + ec.clear(); + else + ec = std::__last_system_error(); +#else + ec = std::make_error_code(std::errc::function_not_supported); +#endif + } +} +#endif void fs::create_directory_symlink(const path& to, const path& new_symlink) @@ -660,7 +708,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 +763,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 +879,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 + | (follow_symlink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT)), + 0)), ec(ec) { if (handle == INVALID_HANDLE_VALUE) @@ -1193,6 +1247,74 @@ fs::proximate(const path& p, const path& base, error_code& ec) return result; } +#if defined(_GLIBCXX_FILESYSTEM_IS_WINDOWS) \ + && defined(SYMBOLIC_LINK_FLAG_DIRECTORY) +namespace +{ + void + windows_read_symlink_handle(auto_win_file_handle& link_handle, + std::error_code& ec, + fs::path& result) + { + PREPARSE_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<PREPARSE_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<PREPARSE_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 +1370,26 @@ fs::path fs::read_symlink(const path& p, error_code& ec) bufsz *= 2; } while (true); +#elif defined(_GLIBCXX_FILESYSTEM_IS_WINDOWS) \ + && defined(SYMBOLIC_LINK_FLAG_DIRECTORY) + auto_win_file_handle link_handle(p.c_str(), ec, false); + if (!link_handle) + return result; + + int is_symlink = __detail::__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 +1433,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 +1468,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 +1535,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. + 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 304d5896d026..cdb4a1ca0ca8 100644 --- a/libstdc++-v3/src/filesystem/ops-common.h +++ b/libstdc++-v3/src/filesystem/ops-common.h @@ -105,6 +105,101 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION namespace filesystem { +#ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS +namespace __detail +{ +#define S_IFLNK 0xC000 +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) + + using stat_type = struct ::__stat64; + + inline HANDLE __open_for_stat(const wchar_t* path, bool following_symlinks) + { + 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 + = CreateFileW(path, 0, share_flags, 0, OPEN_EXISTING, file_flags, 0); + + if (handle == INVALID_HANDLE_VALUE) + { + // CreateFileW does not set errno. + errno = std::__last_system_error().default_error_condition().value(); + } + + return handle; + } + + // _fstat64 in mingw-w64 does not know about symlinks and before v14.0.0 + // it does not know about directories. + // We use GetFileInformationByHandleEx to check whether the HANDLE refers + // to a symlink or directory, then fix the result of _fstat64 accordingly. + enum class FileType { Err = -1, Dir = S_IFDIR, Link = S_IFLNK, Other = 0 }; + + inline FileType __check_handle_type(HANDLE handle, bool following_symlinks) + { +#ifdef SYMBOLIC_LINK_FLAG_DIRECTORY + 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 FileType::Err; + } + // A directory symlink has both DIRECTORY and REPARSE_POINT set, + // so to detect a symlink we need to check for REPARSE_POINT first. + if (!following_symlinks) + if (type_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT + && type_info.ReparseTag == IO_REPARSE_TAG_SYMLINK) + return FileType::Link; + if (type_info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) + return FileType::Dir; +#endif + return FileType::Other; + } + + // -1 error, 0 not a symlink, 1 a symlink + inline int __is_handle_symlink(HANDLE handle) + { + FileType type = __check_handle_type(handle, false); + if (type == FileType::Err) + return -1; + return type == FileType::Link; + } + + 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 directory or symlink, because _fstat does not. + FileType type = __check_handle_type(handle, following_symlinks); + if (type == FileType::Err) + { + 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 (stat_result != -1 && type != FileType::Other) + { + // Clear the previous file type. + buffer->st_mode &= ~S_IFMT; + buffer->st_mode |= (::mode_t)type; + } + ::_close(fd); + return stat_result; + } +} +#endif namespace __gnu_posix { #ifdef _GLIBCXX_FILESYSTEM_IS_WINDOWS @@ -121,13 +216,14 @@ namespace __gnu_posix using stat_type = struct ::__stat64; inline int stat(const wchar_t* path, stat_type* buffer) - { return ::_wstat64(path, buffer); } + { return __detail::__stat_windows(path, buffer, true); } inline int lstat(const wchar_t* path, stat_type* buffer) - { - // FIXME: symlinks not currently supported - return stat(path, buffer); - } +#ifdef SYMBOLIC_LINK_FLAG_DIRECTORY + { return __detail::__stat_windows(path, buffer, false); } +#else + { return stat(path, buffer); } +#endif using ::mode_t; diff --git a/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc b/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc index 884b6da438a3..74d6fd16e21f 100644 --- a/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc +++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/canonical.cc @@ -113,14 +113,14 @@ test03() fs::path foo = dir/"foo", bar = dir/"bar"; fs::create_directory(foo); fs::create_directory(bar); -#ifdef NO_SYMLINKS +#if defined(NO_SYMLINKS) || defined(_GLIBCXX_FILESYSTEM_IS_WINDOWS) #if defined(__MINGW32__) || defined(__MINGW64__) const fs::path baz = dir/"foo\\\\..\\bar///"; #else 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 1ca4a44da59e..03960fa90f24 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 6c187c73b79f..34a664a93d3d 100644 --- a/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc +++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/weakly_canonical.cc @@ -40,19 +40,33 @@ test01() fs::path p; #ifndef NO_SYMLINKS - fs::create_symlink("../bar", foo/"bar"); + fs::create_directory_symlink(fs::path("..")/"bar", foo/"bar"); + + // This fails when under under Wine, see + // https://bugs.winehq.org/show_bug.cgi?id=59922 + p = fs::canonical(dir/"foo//./bar/."); + VERIFY( p == dirc/"bar" ); p = fs::weakly_canonical(dir/"foo//./bar///../biz/."); +#ifndef _GLIBCXX_FILESYSTEM_IS_WINDOWS VERIFY( p == dirc/"biz/" ); +#else + VERIFY( p == dirc/"foo\\biz\\" ); +#endif + p = fs::weakly_canonical(dir/"foo/.//bar/././baz/."); - VERIFY( p == dirc/"bar/baz" ); + VERIFY( p == dirc/"bar"/"baz" ); p = fs::weakly_canonical(fs::current_path()/dir/"bar//../foo/bar/baz"); VERIFY( p == dirc/"bar/baz" ); ec = bad_ec; p = fs::weakly_canonical(dir/"foo//./bar///../biz/.", ec); VERIFY( !ec ); +#ifndef _GLIBCXX_FILESYSTEM_IS_WINDOWS VERIFY( p == dirc/"biz/" ); +#else + VERIFY( p == dirc/"foo\\biz\\" ); +#endif ec = bad_ec; p = fs::weakly_canonical(dir/"foo/.//bar/././baz/.", ec); VERIFY( !ec ); diff --git a/libstdc++-v3/testsuite/util/testsuite_fs.h b/libstdc++-v3/testsuite/util/testsuite_fs.h index fa099b0986bc..f1cf28ce4dd9 100644 --- a/libstdc++-v3/testsuite/util/testsuite_fs.h +++ b/libstdc++-v3/testsuite/util/testsuite_fs.h @@ -42,7 +42,7 @@ namespace test_fs = std::experimental::filesystem; #include <stdlib.h> // mkstemp #endif -#ifndef _GLIBCXX_HAVE_SYMLINK +#if !defined _GLIBCXX_HAVE_SYMLINK && !defined _GLIBCXX_FILESYSTEM_IS_WINDOWS #define NO_SYMLINKS #endif
