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