Issue #19717: Makes Path.resolve() succeed on paths that do not exist (patch by Vajrasky Kok)

This commit is contained in:
Steve Dower 2016-11-09 12:58:17 -08:00
parent 954c7dd0cc
commit 98eb360531
4 changed files with 81 additions and 17 deletions

View File

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

View File

@ -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

View File

@ -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

View File

@ -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.