Kaydet (Commit) 98eb3605 authored tarafından Steve Dower's avatar Steve Dower

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

üst 954c7dd0
......@@ -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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment