Issue #19717: Makes Path.resolve() succeed on paths that do not exist (patch by Vajrasky Kok)
This commit is contained in:
parent
954c7dd0cc
commit
98eb360531
|
@ -919,7 +919,7 @@ call fails (for example because the path doesn't exist):
|
|||
to an existing file or directory, it will be unconditionally replaced.
|
||||
|
||||
|
||||
.. method:: Path.resolve()
|
||||
.. method:: Path.resolve(strict=False)
|
||||
|
||||
Make the path absolute, resolving any symlinks. A new path object is
|
||||
returned::
|
||||
|
@ -936,10 +936,14 @@ call fails (for example because the path doesn't exist):
|
|||
>>> p.resolve()
|
||||
PosixPath('/home/antoine/pathlib/setup.py')
|
||||
|
||||
If the path doesn't exist, :exc:`FileNotFoundError` is raised. If an
|
||||
infinite loop is encountered along the resolution path,
|
||||
:exc:`RuntimeError` is raised.
|
||||
If the path doesn't exist and *strict* is ``True``, :exc:`FileNotFoundError`
|
||||
is raised. If *strict* is ``False``, the path is resolved as far as possible
|
||||
and any remainder is appended without checking whether it exists. If an
|
||||
infinite loop is encountered along the resolution path, :exc:`RuntimeError`
|
||||
is raised.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
The *strict* argument.
|
||||
|
||||
.. method:: Path.rglob(pattern)
|
||||
|
||||
|
|
|
@ -178,12 +178,26 @@ class _WindowsFlavour(_Flavour):
|
|||
def casefold_parts(self, parts):
|
||||
return [p.lower() for p in parts]
|
||||
|
||||
def resolve(self, path):
|
||||
def resolve(self, path, strict=False):
|
||||
s = str(path)
|
||||
if not s:
|
||||
return os.getcwd()
|
||||
previous_s = None
|
||||
if _getfinalpathname is not None:
|
||||
return self._ext_to_normal(_getfinalpathname(s))
|
||||
if strict:
|
||||
return self._ext_to_normal(_getfinalpathname(s))
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
s = self._ext_to_normal(_getfinalpathname(s))
|
||||
except FileNotFoundError:
|
||||
previous_s = s
|
||||
s = os.path.abspath(os.path.join(s, os.pardir))
|
||||
else:
|
||||
if previous_s is None:
|
||||
return s
|
||||
else:
|
||||
return s + os.path.sep + os.path.basename(previous_s)
|
||||
# Means fallback on absolute
|
||||
return None
|
||||
|
||||
|
@ -285,7 +299,7 @@ class _PosixFlavour(_Flavour):
|
|||
def casefold_parts(self, parts):
|
||||
return parts
|
||||
|
||||
def resolve(self, path):
|
||||
def resolve(self, path, strict=False):
|
||||
sep = self.sep
|
||||
accessor = path._accessor
|
||||
seen = {}
|
||||
|
@ -315,7 +329,10 @@ class _PosixFlavour(_Flavour):
|
|||
target = accessor.readlink(newpath)
|
||||
except OSError as e:
|
||||
if e.errno != EINVAL:
|
||||
raise
|
||||
if strict:
|
||||
raise
|
||||
else:
|
||||
return newpath
|
||||
# Not a symlink
|
||||
path = newpath
|
||||
else:
|
||||
|
@ -1092,7 +1109,7 @@ class Path(PurePath):
|
|||
obj._init(template=self)
|
||||
return obj
|
||||
|
||||
def resolve(self):
|
||||
def resolve(self, strict=False):
|
||||
"""
|
||||
Make the path absolute, resolving all symlinks on the way and also
|
||||
normalizing it (for example turning slashes into backslashes under
|
||||
|
@ -1100,7 +1117,7 @@ class Path(PurePath):
|
|||
"""
|
||||
if self._closed:
|
||||
self._raise_closed()
|
||||
s = self._flavour.resolve(self)
|
||||
s = self._flavour.resolve(self, strict=strict)
|
||||
if s is None:
|
||||
# No symlink resolution => for consistency, raise an error if
|
||||
# the path doesn't exist or is forbidden
|
||||
|
|
|
@ -1486,8 +1486,8 @@ class _BasePathTest(object):
|
|||
self.assertEqual(set(p.glob("../xyzzy")), set())
|
||||
|
||||
|
||||
def _check_resolve(self, p, expected):
|
||||
q = p.resolve()
|
||||
def _check_resolve(self, p, expected, strict=True):
|
||||
q = p.resolve(strict)
|
||||
self.assertEqual(q, expected)
|
||||
|
||||
# this can be used to check both relative and absolute resolutions
|
||||
|
@ -1498,8 +1498,17 @@ class _BasePathTest(object):
|
|||
P = self.cls
|
||||
p = P(BASE, 'foo')
|
||||
with self.assertRaises(OSError) as cm:
|
||||
p.resolve()
|
||||
p.resolve(strict=True)
|
||||
self.assertEqual(cm.exception.errno, errno.ENOENT)
|
||||
# Non-strict
|
||||
self.assertEqual(str(p.resolve(strict=False)),
|
||||
os.path.join(BASE, 'foo'))
|
||||
p = P(BASE, 'foo', 'in', 'spam')
|
||||
self.assertEqual(str(p.resolve(strict=False)),
|
||||
os.path.join(BASE, 'foo'))
|
||||
p = P(BASE, '..', 'foo', 'in', 'spam')
|
||||
self.assertEqual(str(p.resolve(strict=False)),
|
||||
os.path.abspath(os.path.join('foo')))
|
||||
# These are all relative symlinks
|
||||
p = P(BASE, 'dirB', 'fileB')
|
||||
self._check_resolve_relative(p, p)
|
||||
|
@ -1509,6 +1518,18 @@ class _BasePathTest(object):
|
|||
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
|
||||
p = P(BASE, 'dirB', 'linkD', 'fileB')
|
||||
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
|
||||
# Non-strict
|
||||
p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
|
||||
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo'), False)
|
||||
p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
|
||||
if os.name == 'nt':
|
||||
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
|
||||
# resolves to 'dirA' without resolving linkY first.
|
||||
self._check_resolve_relative(p, P(BASE, 'dirA', 'foo'), False)
|
||||
else:
|
||||
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
|
||||
# resolves to 'dirB/..' first before resolving to parent of dirB.
|
||||
self._check_resolve_relative(p, P(BASE, 'foo'), False)
|
||||
# Now create absolute symlinks
|
||||
d = tempfile.mkdtemp(suffix='-dirD')
|
||||
self.addCleanup(support.rmtree, d)
|
||||
|
@ -1516,6 +1537,18 @@ class _BasePathTest(object):
|
|||
os.symlink(join('dirB'), os.path.join(d, 'linkY'))
|
||||
p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB')
|
||||
self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB'))
|
||||
# Non-strict
|
||||
p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
|
||||
self._check_resolve_relative(p, P(BASE, 'dirB', 'foo'), False)
|
||||
p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
|
||||
if os.name == 'nt':
|
||||
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
|
||||
# resolves to 'dirA' without resolving linkY first.
|
||||
self._check_resolve_relative(p, P(d, 'foo'), False)
|
||||
else:
|
||||
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
|
||||
# resolves to 'dirB/..' first before resolving to parent of dirB.
|
||||
self._check_resolve_relative(p, P(BASE, 'foo'), False)
|
||||
|
||||
@with_symlinks
|
||||
def test_resolve_dot(self):
|
||||
|
@ -1525,7 +1558,11 @@ class _BasePathTest(object):
|
|||
self.dirlink(os.path.join('0', '0'), join('1'))
|
||||
self.dirlink(os.path.join('1', '1'), join('2'))
|
||||
q = p / '2'
|
||||
self.assertEqual(q.resolve(), p)
|
||||
self.assertEqual(q.resolve(strict=True), p)
|
||||
r = q / '3' / '4'
|
||||
self.assertRaises(FileNotFoundError, r.resolve, strict=True)
|
||||
# Non-strict
|
||||
self.assertEqual(r.resolve(strict=False), p / '3')
|
||||
|
||||
def test_with(self):
|
||||
p = self.cls(BASE)
|
||||
|
@ -1972,10 +2009,10 @@ class PathTest(_BasePathTest, unittest.TestCase):
|
|||
class PosixPathTest(_BasePathTest, unittest.TestCase):
|
||||
cls = pathlib.PosixPath
|
||||
|
||||
def _check_symlink_loop(self, *args):
|
||||
def _check_symlink_loop(self, *args, strict=True):
|
||||
path = self.cls(*args)
|
||||
with self.assertRaises(RuntimeError):
|
||||
print(path.resolve())
|
||||
print(path.resolve(strict))
|
||||
|
||||
def test_open_mode(self):
|
||||
old_mask = os.umask(0)
|
||||
|
@ -2008,7 +2045,6 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
|
|||
|
||||
@with_symlinks
|
||||
def test_resolve_loop(self):
|
||||
# Loop detection for broken symlinks under POSIX
|
||||
# Loops with relative symlinks
|
||||
os.symlink('linkX/inside', join('linkX'))
|
||||
self._check_symlink_loop(BASE, 'linkX')
|
||||
|
@ -2016,6 +2052,8 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
|
|||
self._check_symlink_loop(BASE, 'linkY')
|
||||
os.symlink('linkZ/../linkZ', join('linkZ'))
|
||||
self._check_symlink_loop(BASE, 'linkZ')
|
||||
# Non-strict
|
||||
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False)
|
||||
# Loops with absolute symlinks
|
||||
os.symlink(join('linkU/inside'), join('linkU'))
|
||||
self._check_symlink_loop(BASE, 'linkU')
|
||||
|
@ -2023,6 +2061,8 @@ class PosixPathTest(_BasePathTest, unittest.TestCase):
|
|||
self._check_symlink_loop(BASE, 'linkV')
|
||||
os.symlink(join('linkW/../linkW'), join('linkW'))
|
||||
self._check_symlink_loop(BASE, 'linkW')
|
||||
# Non-strict
|
||||
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False)
|
||||
|
||||
def test_glob(self):
|
||||
P = self.cls
|
||||
|
|
|
@ -23,6 +23,9 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #19717: Makes Path.resolve() succeed on paths that do not exist.
|
||||
Patch by Vajrasky Kok
|
||||
|
||||
- Issue #28563: Fixed possible DoS and arbitrary code execution when handle
|
||||
plural form selections in the gettext module. The expression parser now
|
||||
supports exact syntax supported by GNU gettext.
|
||||
|
|
Loading…
Reference in New Issue