Skip to content

Commit 67cbaae

Browse files
committed
pythonGH-73991: Disallow copying directory into itself via pathlib.Path.copy()
1 parent a6644d4 commit 67cbaae

File tree

2 files changed

+57
-6
lines changed

2 files changed

+57
-6
lines changed

Lib/pathlib/_abc.py

+32-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
resemble pathlib's PurePath and Path respectively.
1212
"""
1313

14+
import errno
1415
import functools
1516
import operator
1617
import posixpath
@@ -564,14 +565,33 @@ def samefile(self, other_path):
564565
return (st.st_ino == other_st.st_ino and
565566
st.st_dev == other_st.st_dev)
566567

567-
def _samefile_safe(self, other_path):
568+
def _ensure_different_file(self, other_path):
568569
"""
569-
Like samefile(), but returns False rather than raising OSError.
570+
Raise OSError(EINVAL) if both paths refer to the same file.
570571
"""
571572
try:
572-
return self.samefile(other_path)
573+
if not self.samefile(other_path):
574+
return
573575
except (OSError, ValueError):
574-
return False
576+
return
577+
err = OSError(errno.EINVAL, "Source and target are the same file")
578+
err.filename = str(self)
579+
err.filename2 = str(other_path)
580+
raise err
581+
582+
def _ensure_distinct_path(self, other_path):
583+
"""
584+
Raise OSError(EINVAL) if the other path is within this path.
585+
"""
586+
if self == other_path:
587+
err = OSError(errno.EINVAL, "Source and target are the same path")
588+
elif self in other_path.parents:
589+
err = OSError(errno.EINVAL, "Source path is a parent of target path")
590+
else:
591+
return
592+
err.filename = str(self)
593+
err.filename2 = str(other_path)
594+
raise err
575595

576596
def open(self, mode='r', buffering=-1, encoding=None,
577597
errors=None, newline=None):
@@ -826,8 +846,7 @@ def _copy_file(self, target):
826846
"""
827847
Copy the contents of this file to the given target.
828848
"""
829-
if self._samefile_safe(target):
830-
raise OSError(f"{self!r} and {target!r} are the same file")
849+
self._ensure_different_file(target)
831850
with self.open('rb') as source_f:
832851
try:
833852
with target.open('wb') as target_f:
@@ -847,6 +866,13 @@ def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
847866
"""
848867
if not isinstance(target, PathBase):
849868
target = self.with_segments(target)
869+
try:
870+
self._ensure_distinct_path(target)
871+
except OSError as err:
872+
if on_error is None:
873+
raise
874+
on_error(err)
875+
return
850876
stack = [(self, target)]
851877
while stack:
852878
src, dst = stack.pop()

Lib/test/test_pathlib/test_pathlib_abc.py

+25
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,12 @@ def test_copy_file_empty(self):
18231823
self.assertTrue(target.exists())
18241824
self.assertEqual(target.read_bytes(), b'')
18251825

1826+
def test_copy_file_to_itself(self):
1827+
base = self.cls(self.base)
1828+
source = base / 'empty'
1829+
source.write_bytes(b'')
1830+
self.assertRaises(OSError, source.copy, source)
1831+
18261832
def test_copy_dir_simple(self):
18271833
base = self.cls(self.base)
18281834
source = base / 'dirC'
@@ -1909,6 +1915,25 @@ def test_copy_dir_to_existing_directory_dirs_exist_ok(self):
19091915
self.assertTrue(target.joinpath('fileC').read_text(),
19101916
"this is file C\n")
19111917

1918+
def test_copy_dir_to_itself(self):
1919+
base = self.cls(self.base)
1920+
source = base / 'dirC'
1921+
self.assertRaises(OSError, source.copy, source)
1922+
1923+
def test_copy_dir_to_itself_on_error(self):
1924+
base = self.cls(self.base)
1925+
source = base / 'dirC'
1926+
errors = []
1927+
source.copy(source, on_error=errors.append)
1928+
self.assertEqual(len(errors), 1)
1929+
self.assertIsInstance(errors[0], OSError)
1930+
1931+
def test_copy_dir_into_itself(self):
1932+
base = self.cls(self.base)
1933+
source = base / 'dirC'
1934+
target = base / 'dirC' / 'dirD' /'copyC'
1935+
self.assertRaises(OSError, source.copy, target)
1936+
19121937
def test_copy_missing_on_error(self):
19131938
base = self.cls(self.base)
19141939
source = base / 'foo'

0 commit comments

Comments
 (0)