Skip to content

Commit 207fdb0

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

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

Lib/pathlib/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import grp
2626
except ImportError:
2727
grp = None
28+
try:
29+
import _winapi
30+
except ImportError:
31+
_winapi = None
2832

2933
from pathlib._os import (
3034
PathInfo, DirEntryInfo,
@@ -1140,11 +1144,20 @@ def _copy_from_file(self, source, preserve_metadata=False):
11401144
_copy_from_file_fallback = _copy_from_file
11411145
def _copy_from_file(self, source, preserve_metadata=False):
11421146
try:
1143-
source = os.fspath(source)
1147+
source_fspath = os.fspath(source)
11441148
except TypeError:
11451149
pass
11461150
else:
1147-
copyfile2(source, str(self))
1151+
try:
1152+
copyfile2(source_fspath, str(self))
1153+
except OSError as exc:
1154+
winerror = getattr(exc, 'winerror', None)
1155+
if (_winapi is not None and
1156+
winerror in (_winapi.ERROR_PRIVILEGE_NOT_HELD,
1157+
_winapi.ERROR_ACCESS_DENIED)):
1158+
self._copy_from_file_fallback(source, preserve_metadata)
1159+
return
1160+
raise
11481161
return
11491162
self._copy_from_file_fallback(source, preserve_metadata)
11501163

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()

0 commit comments

Comments
 (0)