]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.11] gh-113188: Fix shutil.copymode() and shutil.copystat() on Windows (GH-113285...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 23 Dec 2023 10:53:48 +0000 (11:53 +0100)
committerGitHub <noreply@github.com>
Sat, 23 Dec 2023 10:53:48 +0000 (12:53 +0200)
Previously they worked differenly if dst is a symbolic link:
they modified the permission bits of dst itself rather than the file
it points to if follow_symlinks is true or src is not a symbolic link,
and did nothing if follow_symlinks is false and src is a symbolic link.
(cherry picked from commit c7874bb56f862dd96a9a74b809dbea5688fa6c1c)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Lib/shutil.py
Lib/test/test_shutil.py
Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst [new file with mode: 0644]

index cae65a3bed2e1805360421e4ae89b076dde64362..eecc4be5e37b78597cd6db29e2c2ddf69ed87c50 100644 (file)
@@ -298,11 +298,15 @@ def copymode(src, dst, *, follow_symlinks=True):
     sys.audit("shutil.copymode", src, dst)
 
     if not follow_symlinks and _islink(src) and os.path.islink(dst):
-        if hasattr(os, 'lchmod'):
+        if os.name == 'nt':
+            stat_func, chmod_func = os.lstat, os.chmod
+        elif hasattr(os, 'lchmod'):
             stat_func, chmod_func = os.lstat, os.lchmod
         else:
             return
     else:
+        if os.name == 'nt' and os.path.islink(dst):
+            dst = os.path.realpath(dst, strict=True)
         stat_func, chmod_func = _stat, os.chmod
 
     st = stat_func(src)
@@ -378,8 +382,16 @@ def copystat(src, dst, *, follow_symlinks=True):
     # We must copy extended attributes before the file is (potentially)
     # chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
     _copyxattr(src, dst, follow_symlinks=follow)
+    _chmod = lookup("chmod")
+    if os.name == 'nt':
+        if follow:
+            if os.path.islink(dst):
+                dst = os.path.realpath(dst, strict=True)
+        else:
+            def _chmod(*args, **kwargs):
+                os.chmod(*args)
     try:
-        lookup("chmod")(dst, mode, follow_symlinks=follow)
+        _chmod(dst, mode, follow_symlinks=follow)
     except NotImplementedError:
         # if we got a NotImplementedError, it's because
         #   * follow_symlinks=False,
index 08441052fc318423f16fea674a6a739808f52700..f4fdeef280854720423ee35b91b26dea55ea718a 100644 (file)
@@ -877,23 +877,23 @@ class TestCopy(BaseTest, unittest.TestCase):
         shutil.copymode(src, dst)
         self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
         # On Windows, os.chmod does not follow symlinks (issue #15411)
-        if os.name != 'nt':
-            # follow src link
-            os.chmod(dst, stat.S_IRWXO)
-            shutil.copymode(src_link, dst)
-            self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
-            # follow dst link
-            os.chmod(dst, stat.S_IRWXO)
-            shutil.copymode(src, dst_link)
-            self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
-            # follow both links
-            os.chmod(dst, stat.S_IRWXO)
-            shutil.copymode(src_link, dst_link)
-            self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
-
-    @unittest.skipUnless(hasattr(os, 'lchmod'), 'requires os.lchmod')
+        # follow src link
+        os.chmod(dst, stat.S_IRWXO)
+        shutil.copymode(src_link, dst)
+        self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+        # follow dst link
+        os.chmod(dst, stat.S_IRWXO)
+        shutil.copymode(src, dst_link)
+        self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+        # follow both links
+        os.chmod(dst, stat.S_IRWXO)
+        shutil.copymode(src_link, dst_link)
+        self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+
+    @unittest.skipUnless(hasattr(os, 'lchmod') or os.name == 'nt', 'requires os.lchmod')
     @os_helper.skip_unless_symlink
     def test_copymode_symlink_to_symlink(self):
+        _lchmod = os.chmod if os.name == 'nt' else os.lchmod
         tmp_dir = self.mkdtemp()
         src = os.path.join(tmp_dir, 'foo')
         dst = os.path.join(tmp_dir, 'bar')
@@ -905,20 +905,20 @@ class TestCopy(BaseTest, unittest.TestCase):
         os.symlink(dst, dst_link)
         os.chmod(src, stat.S_IRWXU|stat.S_IRWXG)
         os.chmod(dst, stat.S_IRWXU)
-        os.lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
+        _lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
         # link to link
-        os.lchmod(dst_link, stat.S_IRWXO)
+        _lchmod(dst_link, stat.S_IRWXO)
         old_mode = os.stat(dst).st_mode
         shutil.copymode(src_link, dst_link, follow_symlinks=False)
         self.assertEqual(os.lstat(src_link).st_mode,
                          os.lstat(dst_link).st_mode)
         self.assertEqual(os.stat(dst).st_mode, old_mode)
         # src link - use chmod
-        os.lchmod(dst_link, stat.S_IRWXO)
+        _lchmod(dst_link, stat.S_IRWXO)
         shutil.copymode(src_link, dst, follow_symlinks=False)
         self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
         # dst link - use chmod
-        os.lchmod(dst_link, stat.S_IRWXO)
+        _lchmod(dst_link, stat.S_IRWXO)
         shutil.copymode(src, dst_link, follow_symlinks=False)
         self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
 
@@ -955,11 +955,13 @@ class TestCopy(BaseTest, unittest.TestCase):
         os.symlink(dst, dst_link)
         if hasattr(os, 'lchmod'):
             os.lchmod(src_link, stat.S_IRWXO)
+        elif os.name == 'nt':
+            os.chmod(src_link, stat.S_IRWXO)
         if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
             os.lchflags(src_link, stat.UF_NODUMP)
         src_link_stat = os.lstat(src_link)
         # follow
-        if hasattr(os, 'lchmod'):
+        if hasattr(os, 'lchmod') or os.name == 'nt':
             shutil.copystat(src_link, dst_link, follow_symlinks=True)
             self.assertNotEqual(src_link_stat.st_mode, os.stat(dst).st_mode)
         # don't follow
@@ -970,7 +972,7 @@ class TestCopy(BaseTest, unittest.TestCase):
                 # The modification times may be truncated in the new file.
                 self.assertLessEqual(getattr(src_link_stat, attr),
                                      getattr(dst_link_stat, attr) + 1)
-        if hasattr(os, 'lchmod'):
+        if hasattr(os, 'lchmod') or os.name == 'nt':
             self.assertEqual(src_link_stat.st_mode, dst_link_stat.st_mode)
         if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
             self.assertEqual(src_link_stat.st_flags, dst_link_stat.st_flags)
diff --git a/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst b/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst
new file mode 100644 (file)
index 0000000..17c6957
--- /dev/null
@@ -0,0 +1,6 @@
+Fix :func:`shutil.copymode` and :func:`shutil.copystat` on Windows.
+Previously they worked differenly if *dst* is a symbolic link:
+they modified the permission bits of *dst* itself
+rather than the file it points to if *follow_symlinks* is true or *src* is
+not a symbolic link, and did not modify the permission bits if
+*follow_symlinks* is false and *src* is a symbolic link.