mirror of
https://github.com/python/cpython.git
synced 2025-07-24 19:54:21 +00:00
GH-73991: Disallow copying directory into itself via pathlib.Path.copy()
(#122924)
This commit is contained in:
parent
bf1b5d323b
commit
d7ae4dc5c1
2 changed files with 140 additions and 14 deletions
|
@ -14,6 +14,7 @@ resemble pathlib's PurePath and Path respectively.
|
||||||
import functools
|
import functools
|
||||||
import operator
|
import operator
|
||||||
import posixpath
|
import posixpath
|
||||||
|
from errno import EINVAL
|
||||||
from glob import _GlobberBase, _no_recurse_symlinks
|
from glob import _GlobberBase, _no_recurse_symlinks
|
||||||
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
|
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
|
||||||
from pathlib._os import copyfileobj
|
from pathlib._os import copyfileobj
|
||||||
|
@ -564,14 +565,38 @@ class PathBase(PurePathBase):
|
||||||
return (st.st_ino == other_st.st_ino and
|
return (st.st_ino == other_st.st_ino and
|
||||||
st.st_dev == other_st.st_dev)
|
st.st_dev == other_st.st_dev)
|
||||||
|
|
||||||
def _samefile_safe(self, other_path):
|
def _ensure_different_file(self, other_path):
|
||||||
"""
|
"""
|
||||||
Like samefile(), but returns False rather than raising OSError.
|
Raise OSError(EINVAL) if both paths refer to the same file.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.samefile(other_path)
|
if not self.samefile(other_path):
|
||||||
|
return
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
return False
|
return
|
||||||
|
err = OSError(EINVAL, "Source and target are the same file")
|
||||||
|
err.filename = str(self)
|
||||||
|
err.filename2 = str(other_path)
|
||||||
|
raise err
|
||||||
|
|
||||||
|
def _ensure_distinct_path(self, other_path):
|
||||||
|
"""
|
||||||
|
Raise OSError(EINVAL) if the other path is within this path.
|
||||||
|
"""
|
||||||
|
# Note: there is no straightforward, foolproof algorithm to determine
|
||||||
|
# if one directory is within another (a particularly perverse example
|
||||||
|
# would be a single network share mounted in one location via NFS, and
|
||||||
|
# in another location via CIFS), so we simply checks whether the
|
||||||
|
# other path is lexically equal to, or within, this path.
|
||||||
|
if self == other_path:
|
||||||
|
err = OSError(EINVAL, "Source and target are the same path")
|
||||||
|
elif self in other_path.parents:
|
||||||
|
err = OSError(EINVAL, "Source path is a parent of target path")
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
err.filename = str(self)
|
||||||
|
err.filename2 = str(other_path)
|
||||||
|
raise err
|
||||||
|
|
||||||
def open(self, mode='r', buffering=-1, encoding=None,
|
def open(self, mode='r', buffering=-1, encoding=None,
|
||||||
errors=None, newline=None):
|
errors=None, newline=None):
|
||||||
|
@ -826,8 +851,7 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
Copy the contents of this file to the given target.
|
Copy the contents of this file to the given target.
|
||||||
"""
|
"""
|
||||||
if self._samefile_safe(target):
|
self._ensure_different_file(target)
|
||||||
raise OSError(f"{self!r} and {target!r} are the same file")
|
|
||||||
with self.open('rb') as source_f:
|
with self.open('rb') as source_f:
|
||||||
try:
|
try:
|
||||||
with target.open('wb') as target_f:
|
with target.open('wb') as target_f:
|
||||||
|
@ -847,6 +871,13 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
if not isinstance(target, PathBase):
|
if not isinstance(target, PathBase):
|
||||||
target = self.with_segments(target)
|
target = self.with_segments(target)
|
||||||
|
try:
|
||||||
|
self._ensure_distinct_path(target)
|
||||||
|
except OSError as err:
|
||||||
|
if on_error is None:
|
||||||
|
raise
|
||||||
|
on_error(err)
|
||||||
|
return
|
||||||
stack = [(self, target)]
|
stack = [(self, target)]
|
||||||
while stack:
|
while stack:
|
||||||
src, dst = stack.pop()
|
src, dst = stack.pop()
|
||||||
|
|
|
@ -1501,19 +1501,20 @@ class DummyPath(PathBase):
|
||||||
raise FileNotFoundError(errno.ENOENT, "File not found", path)
|
raise FileNotFoundError(errno.ENOENT, "File not found", path)
|
||||||
|
|
||||||
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
|
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
|
||||||
path = str(self.resolve())
|
path = str(self.parent.resolve() / self.name)
|
||||||
if path in self._directories:
|
parent = str(self.parent.resolve())
|
||||||
|
if path in self._directories or path in self._symlinks:
|
||||||
if exist_ok:
|
if exist_ok:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise FileExistsError(errno.EEXIST, "File exists", path)
|
raise FileExistsError(errno.EEXIST, "File exists", path)
|
||||||
try:
|
try:
|
||||||
if self.name:
|
if self.name:
|
||||||
self._directories[str(self.parent)].add(self.name)
|
self._directories[parent].add(self.name)
|
||||||
self._directories[path] = set()
|
self._directories[path] = set()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if not parents:
|
if not parents:
|
||||||
raise FileNotFoundError(errno.ENOENT, "File not found", str(self.parent)) from None
|
raise FileNotFoundError(errno.ENOENT, "File not found", parent) from None
|
||||||
self.parent.mkdir(parents=True, exist_ok=True)
|
self.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self.mkdir(mode, parents=False, exist_ok=exist_ok)
|
self.mkdir(mode, parents=False, exist_ok=exist_ok)
|
||||||
|
|
||||||
|
@ -1758,6 +1759,32 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertEqual(source.readlink(), target.readlink())
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_symlink_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkA'
|
||||||
|
self.assertRaises(OSError, source.copy, source)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_symlink_to_existing_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'copySource'
|
||||||
|
target = base / 'copyTarget'
|
||||||
|
source.symlink_to(base / 'fileA')
|
||||||
|
target.symlink_to(base / 'dirC')
|
||||||
|
self.assertRaises(OSError, source.copy, target)
|
||||||
|
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_symlink_to_existing_directory_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'copySource'
|
||||||
|
target = base / 'copyTarget'
|
||||||
|
source.symlink_to(base / 'fileA')
|
||||||
|
target.symlink_to(base / 'dirC')
|
||||||
|
self.assertRaises(OSError, source.copy, target)
|
||||||
|
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_directory_symlink_follow_symlinks_false(self):
|
def test_copy_directory_symlink_follow_symlinks_false(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
|
@ -1769,6 +1796,42 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertEqual(source.readlink(), target.readlink())
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_directory_symlink_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkB'
|
||||||
|
self.assertRaises(OSError, source.copy, source)
|
||||||
|
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_directory_symlink_into_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkB'
|
||||||
|
target = base / 'linkB' / 'copyB'
|
||||||
|
self.assertRaises(OSError, source.copy, target)
|
||||||
|
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
|
||||||
|
self.assertFalse(target.exists())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_directory_symlink_to_existing_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'copySource'
|
||||||
|
target = base / 'copyTarget'
|
||||||
|
source.symlink_to(base / 'dirC')
|
||||||
|
target.symlink_to(base / 'fileA')
|
||||||
|
self.assertRaises(FileExistsError, source.copy, target)
|
||||||
|
self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_directory_symlink_to_existing_directory_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'copySource'
|
||||||
|
target = base / 'copyTarget'
|
||||||
|
source.symlink_to(base / 'dirC' / 'dirD')
|
||||||
|
target.symlink_to(base / 'dirC')
|
||||||
|
self.assertRaises(FileExistsError, source.copy, target)
|
||||||
|
self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
|
||||||
|
|
||||||
def test_copy_file_to_existing_file(self):
|
def test_copy_file_to_existing_file(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
|
@ -1782,8 +1845,7 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
target = base / 'dirA'
|
target = base / 'dirA'
|
||||||
with self.assertRaises(OSError):
|
self.assertRaises(OSError, source.copy, target)
|
||||||
source.copy(target)
|
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_file_to_existing_symlink(self):
|
def test_copy_file_to_existing_symlink(self):
|
||||||
|
@ -1823,6 +1885,13 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertEqual(target.read_bytes(), b'')
|
self.assertEqual(target.read_bytes(), b'')
|
||||||
|
|
||||||
|
def test_copy_file_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'empty'
|
||||||
|
source.write_bytes(b'')
|
||||||
|
self.assertRaises(OSError, source.copy, source)
|
||||||
|
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
|
||||||
|
|
||||||
def test_copy_dir_simple(self):
|
def test_copy_dir_simple(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
|
@ -1909,6 +1978,28 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.joinpath('fileC').read_text(),
|
self.assertTrue(target.joinpath('fileC').read_text(),
|
||||||
"this is file C\n")
|
"this is file C\n")
|
||||||
|
|
||||||
|
def test_copy_dir_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
self.assertRaises(OSError, source.copy, source)
|
||||||
|
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
|
||||||
|
|
||||||
|
def test_copy_dir_to_itself_on_error(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
errors = []
|
||||||
|
source.copy(source, on_error=errors.append)
|
||||||
|
self.assertEqual(len(errors), 1)
|
||||||
|
self.assertIsInstance(errors[0], OSError)
|
||||||
|
|
||||||
|
def test_copy_dir_into_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
target = base / 'dirC' / 'dirD' / 'copyC'
|
||||||
|
self.assertRaises(OSError, source.copy, target)
|
||||||
|
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
|
||||||
|
self.assertFalse(target.exists())
|
||||||
|
|
||||||
def test_copy_missing_on_error(self):
|
def test_copy_missing_on_error(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'foo'
|
source = base / 'foo'
|
||||||
|
@ -2876,8 +2967,12 @@ class DummyPathWithSymlinks(DummyPath):
|
||||||
raise FileNotFoundError(errno.ENOENT, "File not found", path)
|
raise FileNotFoundError(errno.ENOENT, "File not found", path)
|
||||||
|
|
||||||
def symlink_to(self, target, target_is_directory=False):
|
def symlink_to(self, target, target_is_directory=False):
|
||||||
self._directories[str(self.parent)].add(self.name)
|
path = str(self.parent.resolve() / self.name)
|
||||||
self._symlinks[str(self)] = str(target)
|
parent = str(self.parent.resolve())
|
||||||
|
if path in self._symlinks:
|
||||||
|
raise FileExistsError(errno.EEXIST, "File exists", path)
|
||||||
|
self._directories[parent].add(self.name)
|
||||||
|
self._symlinks[path] = str(target)
|
||||||
|
|
||||||
|
|
||||||
class DummyPathWithSymlinksTest(DummyPathTest):
|
class DummyPathWithSymlinksTest(DummyPathTest):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue