Skip to content

Commit a3185a3

Browse files
committed
gh-139134: fix pathlib.Path.copy() Windows privilege errors with copyfile2 fallback (GH-139134)
1 parent f752fde commit a3185a3

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

Lib/pathlib/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
import grp
2828
except ImportError:
2929
grp = None
30+
try:
31+
import _winapi
32+
except ImportError:
33+
_winapi = None
3034

3135
from pathlib._os import (
3236
vfsopen, vfspath,
@@ -1382,11 +1386,20 @@ def _copy_from_file(self, source, preserve_metadata=False):
13821386
_copy_from_file_fallback = _copy_from_file
13831387
def _copy_from_file(self, source, preserve_metadata=False):
13841388
try:
1385-
source = os.fspath(source)
1389+
source_fspath = os.fspath(source)
13861390
except TypeError:
13871391
pass
13881392
else:
1389-
copyfile2(source, str(self))
1393+
try:
1394+
copyfile2(source_fspath, str(self))
1395+
except OSError as exc:
1396+
winerror = getattr(exc, 'winerror', None)
1397+
if (_winapi is not None and
1398+
winerror in (_winapi.ERROR_PRIVILEGE_NOT_HELD,
1399+
_winapi.ERROR_ACCESS_DENIED)):
1400+
self._copy_from_file_fallback(source, preserve_metadata)
1401+
return
1402+
raise
13901403
return
13911404
self._copy_from_file_fallback(source, preserve_metadata)
13921405

Lib/test/test_pathlib/test_copy.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"""
44

55
import contextlib
6+
import errno
7+
import os
68
import unittest
9+
from unittest import mock
710

811
from .support import is_pypi
912
from .support.local_path import LocalPathGround
@@ -169,6 +172,74 @@ class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
169172
source_ground = LocalPathGround(Path)
170173
target_ground = LocalPathGround(Path)
171174

175+
@unittest.skipUnless(os.name == 'nt', 'needs Windows for CopyFile2 fallback')
176+
def test_copy_hidden_file_fallback_on_access_denied(self):
177+
import _winapi
178+
import ctypes
179+
import pathlib
180+
181+
if pathlib.copyfile2 is None:
182+
self.skipTest('copyfile2 unavailable')
183+
184+
source = self.source_root / 'fileA'
185+
target = self.target_root / 'copy_hidden'
186+
187+
kernel32 = ctypes.windll.kernel32
188+
GetFileAttributesW = kernel32.GetFileAttributesW
189+
SetFileAttributesW = kernel32.SetFileAttributesW
190+
GetFileAttributesW.argtypes = [ctypes.c_wchar_p]
191+
GetFileAttributesW.restype = ctypes.c_uint32
192+
SetFileAttributesW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32]
193+
SetFileAttributesW.restype = ctypes.c_int
194+
195+
path_str = str(source)
196+
original_attrs = GetFileAttributesW(path_str)
197+
if original_attrs in (0xFFFFFFFF, ctypes.c_uint32(-1).value):
198+
self.skipTest('GetFileAttributesW failed')
199+
hidden_attrs = original_attrs | 0x2 # FILE_ATTRIBUTE_HIDDEN
200+
if not SetFileAttributesW(path_str, hidden_attrs):
201+
self.skipTest('SetFileAttributesW failed')
202+
self.addCleanup(SetFileAttributesW, path_str, original_attrs)
203+
204+
def raise_access_denied(*args, **kwargs):
205+
exc = OSError(errno.EACCES, 'Access denied')
206+
exc.winerror = _winapi.ERROR_ACCESS_DENIED
207+
raise exc
208+
209+
with mock.patch('pathlib.copyfile2', side_effect=raise_access_denied) as mock_copy:
210+
result = source.copy(target)
211+
212+
self.assertEqual(result, target)
213+
self.assertTrue(self.target_ground.isfile(result))
214+
self.assertEqual(self.source_ground.readbytes(source),
215+
self.target_ground.readbytes(result))
216+
self.assertEqual(mock_copy.call_count, 1)
217+
218+
@unittest.skipUnless(os.name == 'nt', 'needs Windows for CopyFile2 fallback')
219+
def test_copy_file_fallback_on_privilege_not_held(self):
220+
import _winapi
221+
import pathlib
222+
223+
if pathlib.copyfile2 is None:
224+
self.skipTest('copyfile2 unavailable')
225+
226+
source = self.source_root / 'fileA'
227+
target = self.target_root / 'copy_privilege'
228+
229+
def raise_privilege_not_held(*args, **kwargs):
230+
exc = OSError(errno.EPERM, 'Privilege not held')
231+
exc.winerror = _winapi.ERROR_PRIVILEGE_NOT_HELD
232+
raise exc
233+
234+
with mock.patch('pathlib.copyfile2', side_effect=raise_privilege_not_held) as mock_copy:
235+
result = source.copy(target)
236+
237+
self.assertEqual(result, target)
238+
self.assertTrue(self.target_ground.isfile(result))
239+
self.assertEqual(self.source_ground.readbytes(source),
240+
self.target_ground.readbytes(result))
241+
self.assertEqual(mock_copy.call_count, 1)
242+
172243

173244
if __name__ == "__main__":
174245
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ``pathlib.Path.copy()`` failing on Windows when copying files that require elevated privileges (e.g., hidden or system files) by adding a fallback mechanism when ``copyfile2`` encounters privilege-related errors.

0 commit comments

Comments
 (0)