mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
GH-109187: Improve symlink loop handling in pathlib.Path.resolve()
(GH-109192)
Treat symlink loops like other errors: in strict mode, raise `OSError`, and in non-strict mode, do not raise any exception.
This commit is contained in:
parent
859618c8cd
commit
ecd813f054
4 changed files with 21 additions and 30 deletions
|
@ -1381,15 +1381,19 @@ call fails (for example because the path doesn't exist).
|
||||||
>>> p.resolve()
|
>>> p.resolve()
|
||||||
PosixPath('/home/antoine/pathlib/setup.py')
|
PosixPath('/home/antoine/pathlib/setup.py')
|
||||||
|
|
||||||
If the path doesn't exist and *strict* is ``True``, :exc:`FileNotFoundError`
|
If a path doesn't exist or a symlink loop is encountered, and *strict* is
|
||||||
is raised. If *strict* is ``False``, the path is resolved as far as possible
|
``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
|
||||||
and any remainder is appended without checking whether it exists. If an
|
resolved as far as possible and any remainder is appended without checking
|
||||||
infinite loop is encountered along the resolution path, :exc:`RuntimeError`
|
whether it exists.
|
||||||
is raised.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.6
|
.. versionchanged:: 3.6
|
||||||
The *strict* parameter was added (pre-3.6 behavior is strict).
|
The *strict* parameter was added (pre-3.6 behavior is strict).
|
||||||
|
|
||||||
|
.. versionchanged:: 3.13
|
||||||
|
Symlink loops are treated like other errors: :exc:`OSError` is raised in
|
||||||
|
strict mode, and no exception is raised in non-strict mode. In previous
|
||||||
|
versions, :exc:`RuntimeError` is raised no matter the value of *strict*.
|
||||||
|
|
||||||
.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)
|
.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)
|
||||||
|
|
||||||
Glob the given relative *pattern* recursively. This is like calling
|
Glob the given relative *pattern* recursively. This is like calling
|
||||||
|
|
|
@ -1230,26 +1230,7 @@ class Path(PurePath):
|
||||||
normalizing it.
|
normalizing it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check_eloop(e):
|
return self.with_segments(os.path.realpath(self, strict=strict))
|
||||||
winerror = getattr(e, 'winerror', 0)
|
|
||||||
if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME:
|
|
||||||
raise RuntimeError("Symlink loop from %r" % e.filename)
|
|
||||||
|
|
||||||
try:
|
|
||||||
s = os.path.realpath(self, strict=strict)
|
|
||||||
except OSError as e:
|
|
||||||
check_eloop(e)
|
|
||||||
raise
|
|
||||||
p = self.with_segments(s)
|
|
||||||
|
|
||||||
# In non-strict mode, realpath() doesn't raise on symlink loops.
|
|
||||||
# Ensure we get an exception by calling stat()
|
|
||||||
if not strict:
|
|
||||||
try:
|
|
||||||
p.stat()
|
|
||||||
except OSError as e:
|
|
||||||
check_eloop(e)
|
|
||||||
return p
|
|
||||||
|
|
||||||
def owner(self):
|
def owner(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3178,10 +3178,11 @@ class PosixPathTest(PathTest):
|
||||||
self.assertEqual(str(P('//a').absolute()), '//a')
|
self.assertEqual(str(P('//a').absolute()), '//a')
|
||||||
self.assertEqual(str(P('//a/b').absolute()), '//a/b')
|
self.assertEqual(str(P('//a/b').absolute()), '//a/b')
|
||||||
|
|
||||||
def _check_symlink_loop(self, *args, strict=True):
|
def _check_symlink_loop(self, *args):
|
||||||
path = self.cls(*args)
|
path = self.cls(*args)
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(OSError) as cm:
|
||||||
print(path.resolve(strict))
|
path.resolve(strict=True)
|
||||||
|
self.assertEqual(cm.exception.errno, errno.ELOOP)
|
||||||
|
|
||||||
@unittest.skipIf(
|
@unittest.skipIf(
|
||||||
is_emscripten or is_wasi,
|
is_emscripten or is_wasi,
|
||||||
|
@ -3240,7 +3241,8 @@ class PosixPathTest(PathTest):
|
||||||
os.symlink('linkZ/../linkZ', join('linkZ'))
|
os.symlink('linkZ/../linkZ', join('linkZ'))
|
||||||
self._check_symlink_loop(BASE, 'linkZ')
|
self._check_symlink_loop(BASE, 'linkZ')
|
||||||
# Non-strict
|
# Non-strict
|
||||||
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False)
|
p = self.cls(BASE, 'linkZ', 'foo')
|
||||||
|
self.assertEqual(p.resolve(strict=False), p)
|
||||||
# Loops with absolute symlinks.
|
# Loops with absolute symlinks.
|
||||||
os.symlink(join('linkU/inside'), join('linkU'))
|
os.symlink(join('linkU/inside'), join('linkU'))
|
||||||
self._check_symlink_loop(BASE, 'linkU')
|
self._check_symlink_loop(BASE, 'linkU')
|
||||||
|
@ -3249,7 +3251,8 @@ class PosixPathTest(PathTest):
|
||||||
os.symlink(join('linkW/../linkW'), join('linkW'))
|
os.symlink(join('linkW/../linkW'), join('linkW'))
|
||||||
self._check_symlink_loop(BASE, 'linkW')
|
self._check_symlink_loop(BASE, 'linkW')
|
||||||
# Non-strict
|
# Non-strict
|
||||||
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False)
|
q = self.cls(BASE, 'linkW', 'foo')
|
||||||
|
self.assertEqual(q.resolve(strict=False), q)
|
||||||
|
|
||||||
def test_glob(self):
|
def test_glob(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
:meth:`pathlib.Path.resolve` now treats symlink loops like other errors: in
|
||||||
|
strict mode, :exc:`OSError` is raised, and in non-strict mode, no exception
|
||||||
|
is raised.
|
Loading…
Add table
Add a link
Reference in a new issue