[committed] libstdc++: Fix filesystem::rename on Windows [PR 98985]

Jonathan Wakely jwakely@redhat.com
Fri Feb 12 15:50:39 GMT 2021


The _wrename function won't overwrite an existing file, so use
MoveFileEx instead. That allows renaming directories over files, which
POSIX doesn't allow, so check for that case explicitly and report an
error.

Also document the deviation from the expected behaviour, and add a test
for filesystem::rename which was previously missing.

The Filesystem TS experimental::filesystem::rename doesn't have that
extra code to handle directories correctly, so the relevant parts of the
new test are not run on Windows.

libstdc++-v3/ChangeLog:

	* doc/xml/manual/status_cxx2014.xml: Document implementation
	specific properties of std::experimental::filesystem::rename.
	* doc/xml/manual/status_cxx2017.xml: Document implementation
	specific properties of std::filesystem::rename.
	* doc/html/*: Regenerate.
	* src/c++17/fs_ops.cc (fs::rename): Implement correct behaviour
	for directories on Windows.
	* src/filesystem/ops-common.h (__gnu_posix::rename): Use
	MoveFileExW on Windows.
	* testsuite/27_io/filesystem/operations/rename.cc: New test.
	* testsuite/experimental/filesystem/operations/rename.cc: New test.

Tested x86_64-linux and x86_64-w64-mingw32. Committed to trunk.

-------------- next part --------------
commit 1dfd95f0a0ca1d9e6cbc00e6cbfd1fa20a98f312
Author: Jonathan Wakely <jwakely@redhat.com>
Date:   Fri Feb 12 15:13:02 2021

    libstdc++: Fix filesystem::rename on Windows [PR 98985]
    
    The _wrename function won't overwrite an existing file, so use
    MoveFileEx instead. That allows renaming directories over files, which
    POSIX doesn't allow, so check for that case explicitly and report an
    error.
    
    Also document the deviation from the expected behaviour, and add a test
    for filesystem::rename which was previously missing.
    
    The Filesystem TS experimental::filesystem::rename doesn't have that
    extra code to handle directories correctly, so the relevant parts of the
    new test are not run on Windows.
    
    libstdc++-v3/ChangeLog:
    
            * doc/xml/manual/status_cxx2014.xml: Document implementation
            specific properties of std::experimental::filesystem::rename.
            * doc/xml/manual/status_cxx2017.xml: Document implementation
            specific properties of std::filesystem::rename.
            * doc/html/*: Regenerate.
            * src/c++17/fs_ops.cc (fs::rename): Implement correct behaviour
            for directories on Windows.
            * src/filesystem/ops-common.h (__gnu_posix::rename): Use
            MoveFileExW on Windows.
            * testsuite/27_io/filesystem/operations/rename.cc: New test.
            * testsuite/experimental/filesystem/operations/rename.cc: New test.

diff --git a/libstdc++-v3/doc/xml/manual/status_cxx2014.xml b/libstdc++-v3/doc/xml/manual/status_cxx2014.xml
index 2cf5f629efb..5dc287707d8 100644
--- a/libstdc++-v3/doc/xml/manual/status_cxx2014.xml
+++ b/libstdc++-v3/doc/xml/manual/status_cxx2014.xml
@@ -1717,9 +1717,33 @@ not in any particular release.
       </entry>
     </row>
 
-
   </tbody>
 </tgroup>
 </table>
 
+<section xml:id="iso.2014.specific" xreflabel="Implementation Specific"><info><title>Implementation Specific Behavior</title></info>
+
+  <section xml:id="iso.2014.filesystemts" xreflabel="Implementation Specific Behavior of the Filesystem TS"><info><title>Filesystem TS</title></info>
+    <para>
+      <emphasis>2.1 POSIX conformance [fs.conform.9945]</emphasis>
+      The behavior of the filesystem library implementation will depend on
+      the target operating system. Some features will not be supported
+      on some targets. Symbolic links and file permissions
+      are not supported on Windows.
+    </para>
+    <para>
+      <emphasis>15.30 Rename [fs.op.rename]</emphasis>
+      On Windows, <code>experimental::filesystem::rename</code>
+      is implemented by calling <code>MoveFileExW</code> and so
+      does not meet the requirements of POSIX <code>rename</code>
+      when one or both of the paths resolves to an existing directory.
+      Specifically, it is possible to rename a directory so it replaces
+      a non-directory (POSIX requires an error in that case),
+      and it is not possible to rename a directory to replace another
+      directory (POSIX requires that to work if the directory being
+      replaced is empty).
+    </para>
+  </section>
+</section>
+
 </section>
diff --git a/libstdc++-v3/doc/xml/manual/status_cxx2017.xml b/libstdc++-v3/doc/xml/manual/status_cxx2017.xml
index b1c12bd3799..7f64c47bdfe 100644
--- a/libstdc++-v3/doc/xml/manual/status_cxx2017.xml
+++ b/libstdc++-v3/doc/xml/manual/status_cxx2017.xml
@@ -2992,7 +2992,8 @@ since C++14 and the implementation is complete.
       <emphasis>30.10.2.1 [fs.conform.9945]</emphasis>
       The behavior of the filesystem library implementation will depend on
       the target operating system. Some features will not be supported
-      on some targets.
+      on some targets. Symbolic links and file permissions
+      are not supported on Windows.
    </para>
 
    <para>
@@ -3025,6 +3026,18 @@ since C++14 and the implementation is complete.
       If <code>!is_regular_file(p)</code>, an error is reported.
    </para>
 
+    <para>
+      <emphasis>30.10.15.32 [fs.op.rename]</emphasis>
+      On Windows, <code>filesystem::rename</code>
+      is implemented by calling <code>MoveFileExW</code> and so
+      does not meet the requirements of POSIX <code>rename</code>
+      when one or both of the paths resolves to an existing directory.
+      Specifically, it is not possible to rename a directory to replace another
+      directory (POSIX requires that to work if the directory being
+      replaced is empty).
+    </para>
+
+
    <section xml:id="iso.2017.par2ts" xreflabel="Implementation Specific Behavior of the Parallelism 2 TS"><info><title>Parallelism 2 TS</title></info>
 
      <para>
diff --git a/libstdc++-v3/src/c++17/fs_ops.cc b/libstdc++-v3/src/c++17/fs_ops.cc
index 3817655471c..3e1671e611e 100644
--- a/libstdc++-v3/src/c++17/fs_ops.cc
+++ b/libstdc++-v3/src/c++17/fs_ops.cc
@@ -1394,6 +1394,36 @@ fs::rename(const path& from, const path& to)
 void
 fs::rename(const path& from, const path& to, error_code& ec) noexcept
 {
+#if _GLIBCXX_FILESYSTEM_IS_WINDOWS
+  const auto to_status = fs::status(to, ec);
+  if (to_status.type() == file_type::not_found)
+    ec.clear();
+  else if (ec)
+    return;
+
+  if (fs::exists(to_status))
+  {
+    const auto from_status = fs::status(from, ec);
+    if (ec)
+      return;
+
+    if (fs::is_directory(to_status))
+    {
+      if (!fs::is_directory(from_status))
+      {
+	// Cannot rename a non-directory over an existing directory.
+	ec = std::make_error_code(std::errc::is_a_directory);
+	return;
+      }
+    }
+    else if (fs::is_directory(from_status))
+    {
+      // Cannot rename a directory over an existing non-directory.
+      ec = std::make_error_code(std::errc::not_a_directory);
+      return;
+    }
+  }
+#endif
   if (posix::rename(from.c_str(), to.c_str()))
     ec.assign(errno, std::generic_category());
   else
diff --git a/libstdc++-v3/src/filesystem/ops-common.h b/libstdc++-v3/src/filesystem/ops-common.h
index 118256a0d22..529d4e09016 100644
--- a/libstdc++-v3/src/filesystem/ops-common.h
+++ b/libstdc++-v3/src/filesystem/ops-common.h
@@ -104,7 +104,16 @@ namespace __gnu_posix
 #endif
 
   inline int rename(const wchar_t* oldname, const wchar_t* newname)
-  { return _wrename(oldname, newname); }
+  {
+    if (MoveFileExW(oldname, newname,
+		    MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))
+      return 0;
+    if (GetLastError() == ERROR_ACCESS_DENIED)
+      errno = EACCES;
+    else
+      errno = EIO;
+    return -1;
+  }
 
   inline int truncate(const wchar_t* path, _off64_t length)
   {
diff --git a/libstdc++-v3/testsuite/27_io/filesystem/operations/rename.cc b/libstdc++-v3/testsuite/27_io/filesystem/operations/rename.cc
new file mode 100644
index 00000000000..c873811ad93
--- /dev/null
+++ b/libstdc++-v3/testsuite/27_io/filesystem/operations/rename.cc
@@ -0,0 +1,181 @@
+// Copyright (C) 2021 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library.  This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3.  If not see
+// <http://www.gnu.org/licenses/>.
+
+// { dg-options "-std=gnu++17" }
+// { dg-do run { target c++17 } }
+// { dg-require-filesystem-ts "" }
+
+#include <filesystem>
+#include <testsuite_hooks.h>
+#include <testsuite_fs.h>
+
+namespace fs = std::filesystem;
+
+void
+test01()
+{
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  auto p1 = __gnu_test::nonexistent_path();
+  auto p2 = __gnu_test::nonexistent_path();
+
+  fs::rename(p1, p2, ec);
+  VERIFY( ec );
+
+  ec.clear();
+  fs::rename(p1, "", ec);
+  VERIFY( ec );
+
+  ec.clear();
+  fs::rename("", p1, ec);
+  VERIFY( ec );
+
+  ec = bad_ec;
+  std::ofstream{p1}; // create file
+  fs::rename(p1, p1, ec); // no-op
+  VERIFY( !ec );
+  VERIFY( is_regular_file(p1) );
+
+  ec.clear();
+  rename(p2, p1, ec);
+  VERIFY( ec );
+  VERIFY( ec.value() == ENOENT );
+  VERIFY( is_regular_file(p1) );
+
+  ec = bad_ec;
+  fs::rename(p1, p2, ec);
+  VERIFY( !ec );
+  VERIFY( !exists(p1) );
+  VERIFY( is_regular_file(p2) );
+
+  ec = bad_ec;
+  std::ofstream{p1}; // create file
+  fs::rename(p1, p2, ec);
+  VERIFY( !ec );
+  VERIFY( !exists(p1) );
+  VERIFY( is_regular_file(p2) );
+
+  fs::remove(p2, ec);
+}
+
+void
+test_symlinks()
+{
+#if defined(__MINGW32__) || defined(__MINGW64__)
+  // No symlink support
+#else
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  const auto dir = __gnu_test::nonexistent_path();
+  fs::create_directory(dir);
+
+  create_symlink(dir/"nonesuch", dir/"link");  // dangling symlink
+  ec = bad_ec;
+  fs::rename(dir/"link", dir/"newlink", ec);
+  VERIFY( !ec );
+  VERIFY( !exists(symlink_status(dir/"link")) );
+  VERIFY( is_symlink(dir/"newlink") );
+
+  __gnu_test::scoped_file f(dir/"file");
+  create_symlink(dir/"file", dir/"link");
+  ec = bad_ec;
+  fs::rename(dir/"link", dir/"newerlink", ec);
+  VERIFY( !ec );
+  VERIFY( !exists(symlink_status(dir/"link")) );
+  VERIFY( is_symlink(dir/"newerlink") );
+  VERIFY( is_regular_file(dir/"file") );
+
+  fs::remove_all(dir, ec);
+  f.path.clear();
+#endif
+}
+
+void
+test_directories()
+{
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  const auto dir = __gnu_test::nonexistent_path();
+  fs::create_directory(dir);
+  __gnu_test::scoped_file f(dir/"file");
+  fs::create_directory(dir/"subdir");
+
+  // Rename directory.
+  ec = bad_ec;
+  fs::rename(dir/"subdir", dir/"subdir2", ec);
+  VERIFY( !ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( !exists(dir/"subdir") );
+
+  // Cannot rename a directory to a sub-directory of itself.
+  fs::rename(dir/"subdir2", dir/"subdir2/subsubdir", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( !exists(dir/"subdir2"/"subsubdir") );
+
+  // Cannot rename a file to the name of an existing directory.
+  ec.clear();
+  fs::rename(dir/"file", dir/"subdir2", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"file") );
+
+  // Cannot rename a directory to the name of an existing non-directory
+  ec.clear();
+  fs::rename(dir/"subdir2", dir/"file", ec);
+  VERIFY( ec );
+  VERIFY( is_regular_file(dir/"file") );
+  VERIFY( is_directory(dir/"subdir2") );
+
+  // Cannot rename directory to the name of a non-empty directory.
+  ec.clear();
+  __gnu_test::scoped_file f2(dir/"subdir2/file");
+  fs::create_directory(dir/"subdir");
+  fs::rename(dir/"subdir", dir/"subdir2", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir") );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"subdir2/file") );
+
+#if defined(__MINGW32__) || defined(__MINGW64__)
+  // Cannot rename a directory to an existing directory
+#else
+  // Can rename a non-empty directory to the name of an empty directory.
+  ec = bad_ec;
+  fs::rename(dir/"subdir2", dir/"subdir", ec);
+  VERIFY( !ec );
+  VERIFY( is_directory(dir/"subdir") );
+  VERIFY( !exists(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"subdir/file") );
+#endif
+
+  f2.path.clear();
+  f.path.clear();
+
+  fs::remove_all(dir, ec);
+}
+
+int
+main()
+{
+  test01();
+  test_symlinks();
+  test_directories();
+}
diff --git a/libstdc++-v3/testsuite/experimental/filesystem/operations/rename.cc b/libstdc++-v3/testsuite/experimental/filesystem/operations/rename.cc
new file mode 100644
index 00000000000..56039e7f37d
--- /dev/null
+++ b/libstdc++-v3/testsuite/experimental/filesystem/operations/rename.cc
@@ -0,0 +1,180 @@
+// Copyright (C) 2021 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library.  This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3.  If not see
+// <http://www.gnu.org/licenses/>.
+
+// { dg-options "-DUSE_FILESYSTEM_TS -lstdc++fs" }
+// { dg-do run { target c++11 } }
+// { dg-require-filesystem-ts "" }
+
+#include <experimental/filesystem>
+#include <testsuite_hooks.h>
+#include <testsuite_fs.h>
+
+namespace fs = std::experimental::filesystem;
+
+void
+test01()
+{
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  auto p1 = __gnu_test::nonexistent_path();
+  auto p2 = __gnu_test::nonexistent_path();
+
+  fs::rename(p1, p2, ec);
+  VERIFY( ec );
+
+  ec.clear();
+  fs::rename(p1, "", ec);
+  VERIFY( ec );
+
+  ec.clear();
+  fs::rename("", p1, ec);
+  VERIFY( ec );
+
+  ec = bad_ec;
+  std::ofstream{p1}; // create file
+  fs::rename(p1, p1, ec); // no-op
+  VERIFY( !ec );
+  VERIFY( is_regular_file(p1) );
+
+  ec.clear();
+  rename(p2, p1, ec);
+  VERIFY( ec );
+  VERIFY( is_regular_file(p1) );
+
+  ec = bad_ec;
+  fs::rename(p1, p2, ec);
+  VERIFY( !ec );
+  VERIFY( !exists(p1) );
+  VERIFY( is_regular_file(p2) );
+
+  ec = bad_ec;
+  std::ofstream{p1}; // create file
+  fs::rename(p1, p2, ec);
+  VERIFY( !ec );
+  VERIFY( !exists(p1) );
+  VERIFY( is_regular_file(p2) );
+
+  fs::remove(p2, ec);
+}
+
+void
+test_symlinks()
+{
+#if defined(__MINGW32__) || defined(__MINGW64__)
+  // No symlink support
+#else
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  const auto dir = __gnu_test::nonexistent_path();
+  fs::create_directory(dir);
+
+  create_symlink(dir/"nonesuch", dir/"link");  // dangling symlink
+  ec = bad_ec;
+  fs::rename(dir/"link", dir/"newlink", ec);
+  VERIFY( !ec );
+  VERIFY( !exists(symlink_status(dir/"link")) );
+  VERIFY( is_symlink(dir/"newlink") );
+
+  __gnu_test::scoped_file f(dir/"file");
+  create_symlink(dir/"file", dir/"link");
+  ec = bad_ec;
+  fs::rename(dir/"link", dir/"newerlink", ec);
+  VERIFY( !ec );
+  VERIFY( !exists(symlink_status(dir/"link")) );
+  VERIFY( is_symlink(dir/"newerlink") );
+  VERIFY( is_regular_file(dir/"file") );
+
+  fs::remove_all(dir, ec);
+  f.path.clear();
+#endif
+}
+
+void
+test_directories()
+{
+  std::error_code ec;
+  const std::error_code bad_ec = make_error_code(std::errc::invalid_argument);
+
+  const auto dir = __gnu_test::nonexistent_path();
+  fs::create_directory(dir);
+  __gnu_test::scoped_file f(dir/"file");
+  fs::create_directory(dir/"subdir");
+
+  // Rename directory.
+  ec = bad_ec;
+  fs::rename(dir/"subdir", dir/"subdir2", ec);
+  VERIFY( !ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( !exists(dir/"subdir") );
+
+  // Cannot rename a directory to a sub-directory of itself.
+  fs::rename(dir/"subdir2", dir/"subdir2/subsubdir", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( !exists(dir/"subdir2"/"subsubdir") );
+
+  // Cannot rename a file to the name of an existing directory.
+  ec.clear();
+  fs::rename(dir/"file", dir/"subdir2", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"file") );
+
+#if defined(__MINGW32__) || defined(__MINGW64__)
+  // XXX broken on Windows, see PR 98985
+#else
+  // Cannot rename a directory to the name of an existing non-directory
+  ec.clear();
+  fs::rename(dir/"subdir2", dir/"file", ec);
+  VERIFY( ec );
+  VERIFY( is_regular_file(dir/"file") );
+  VERIFY( is_directory(dir/"subdir2") );
+
+  // Cannot rename directory to the name of a non-empty directory.
+  ec.clear();
+  __gnu_test::scoped_file f2(dir/"subdir2/file");
+  fs::create_directory(dir/"subdir");
+  fs::rename(dir/"subdir", dir/"subdir2", ec);
+  VERIFY( ec );
+  VERIFY( is_directory(dir/"subdir") );
+  VERIFY( is_directory(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"subdir2/file") );
+
+  // Can rename a non-empty directory to the name of an empty directory.
+  ec = bad_ec;
+  fs::rename(dir/"subdir2", dir/"subdir", ec);
+  VERIFY( !ec );
+  VERIFY( is_directory(dir/"subdir") );
+  VERIFY( !exists(dir/"subdir2") );
+  VERIFY( is_regular_file(dir/"subdir/file") );
+  f2.path.clear();
+
+  f.path.clear();
+#endif
+
+  fs::remove_all(dir, ec);
+}
+
+int
+main()
+{
+  test01();
+  test_symlinks();
+  test_directories();
+}


More information about the Libstdc++ mailing list