From 3f3d82b84823eb28abeedf317bbe107bbe7f6492 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Wed, 7 Apr 2021 23:50:13 +0100 Subject: bpo-39899: os.path.expanduser(): don't guess other Windows users' home directories if the basename of the current user's home directory doesn't match their username. (GH-18841) This makes `ntpath.expanduser()` match `pathlib.Path.expanduser()` in this regard, and is more in line with `posixpath.expanduser()`'s cautious approach. Also remove the near-duplicate implementation of `expanduser()` in pathlib, and by doing so fix a bug where KeyError could be raised when expanding another user's home directory. --- Doc/library/os.path.rst | 4 +- Doc/library/pathlib.rst | 10 ++++- Lib/ntpath.py | 18 ++++++-- Lib/pathlib.py | 51 +++------------------- Lib/test/test_ntpath.py | 43 +++++++++++------- Lib/test/test_pathlib.py | 2 +- .../2020-03-09-20-36-07.bpo-39899.9adF3E.rst | 3 ++ 7 files changed, 63 insertions(+), 68 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-03-09-20-36-07.bpo-39899.9adF3E.rst diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 251df4d..e2f335e 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -175,8 +175,8 @@ the :mod:`glob` module.) On Windows, :envvar:`USERPROFILE` will be used if set, otherwise a combination of :envvar:`HOMEPATH` and :envvar:`HOMEDRIVE` will be used. An initial - ``~user`` is handled by stripping the last directory component from the created - user path derived above. + ``~user`` is handled by checking that the last directory component of the current + user's home directory matches :envvar:`USERNAME`, and replacing it if so. If the expansion fails or if the path does not begin with a tilde, the path is returned unchanged. diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index e269bf9..f15fed3 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -705,7 +705,10 @@ call fails (for example because the path doesn't exist). .. classmethod:: Path.home() Return a new path object representing the user's home directory (as - returned by :func:`os.path.expanduser` with ``~`` construct):: + returned by :func:`os.path.expanduser` with ``~`` construct). If the home + directory can't be resolved, :exc:`RuntimeError` is raised. + + :: >>> Path.home() PosixPath('/home/antoine') @@ -773,7 +776,10 @@ call fails (for example because the path doesn't exist). .. method:: Path.expanduser() Return a new path with expanded ``~`` and ``~user`` constructs, - as returned by :meth:`os.path.expanduser`:: + as returned by :meth:`os.path.expanduser`. If a home directory can't be + resolved, :exc:`RuntimeError` is raised. + + :: >>> p = PosixPath('~/films/Monty Python') >>> p.expanduser() diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 6f77177..421db50 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -312,12 +312,24 @@ def expanduser(path): drive = '' userhome = join(drive, os.environ['HOMEPATH']) + if i != 1: #~user + # Try to guess user home directory. By default all users directories + # are located in the same place and are named by corresponding + # usernames. If current user home directory points to nonstandard + # place, this guess is likely wrong, and so we bail out. + current_user = os.environ.get('USERNAME') + if current_user != basename(userhome): + return path + + target_user = path[1:i] + if isinstance(target_user, bytes): + target_user = os.fsdecode(target_user) + if target_user != current_user: + userhome = join(dirname(userhome), target_user) + if isinstance(path, bytes): userhome = os.fsencode(userhome) - if i != 1: #~user - userhome = join(dirname(userhome), path[1:i]) - return userhome + path[i:] diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 9e682dc..19d45a3 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -246,34 +246,6 @@ class _WindowsFlavour(_Flavour): # It's a path on a network drive => 'file://host/share/a/b' return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) - def gethomedir(self, username): - if 'USERPROFILE' in os.environ: - userhome = os.environ['USERPROFILE'] - elif 'HOMEPATH' in os.environ: - try: - drv = os.environ['HOMEDRIVE'] - except KeyError: - drv = '' - userhome = drv + os.environ['HOMEPATH'] - else: - raise RuntimeError("Can't determine home directory") - - if username: - # Try to guess user home directory. By default all users - # directories are located in the same place and are named by - # corresponding usernames. If current user home directory points - # to nonstandard place, this guess is likely wrong. - if os.environ['USERNAME'] != username: - drv, root, parts = self.parse_parts((userhome,)) - if parts[-1] != os.environ['USERNAME']: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - parts[-1] = username - if drv or root: - userhome = drv + root + self.join(parts[1:]) - else: - userhome = self.join(parts) - return userhome class _PosixFlavour(_Flavour): sep = '/' @@ -364,21 +336,6 @@ class _PosixFlavour(_Flavour): bpath = bytes(path) return 'file://' + urlquote_from_bytes(bpath) - def gethomedir(self, username): - if not username: - try: - return os.environ['HOME'] - except KeyError: - import pwd - return pwd.getpwuid(os.getuid()).pw_dir - else: - import pwd - try: - return pwd.getpwnam(username).pw_dir - except KeyError: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - _windows_flavour = _WindowsFlavour() _posix_flavour = _PosixFlavour() @@ -463,6 +420,8 @@ class _NormalAccessor(_Accessor): getcwd = os.getcwd + expanduser = staticmethod(os.path.expanduser) + _normal_accessor = _NormalAccessor() @@ -1105,7 +1064,7 @@ class Path(PurePath): """Return a new path pointing to the user's home directory (as returned by os.path.expanduser('~')). """ - return cls(cls()._flavour.gethomedir(None)) + return cls("~").expanduser() def samefile(self, other_path): """Return whether other_path is the same or not as this file @@ -1517,7 +1476,9 @@ class Path(PurePath): """ if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): - homedir = self._flavour.gethomedir(self._parts[0][1:]) + homedir = self._accessor.expanduser(self._parts[0]) + if homedir[:1] == "~": + raise RuntimeError("Could not determine home directory.") return self._from_parts([homedir] + self._parts[1:]) return self diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index a8f764f..f97aca5 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -503,34 +503,47 @@ class TestNtpath(NtpathTestCase): env.clear() tester('ntpath.expanduser("~test")', '~test') - env['HOMEPATH'] = 'eric\\idle' env['HOMEDRIVE'] = 'C:\\' - tester('ntpath.expanduser("~test")', 'C:\\eric\\test') - tester('ntpath.expanduser("~")', 'C:\\eric\\idle') + env['HOMEPATH'] = 'Users\\eric' + env['USERNAME'] = 'eric' + tester('ntpath.expanduser("~test")', 'C:\\Users\\test') + tester('ntpath.expanduser("~")', 'C:\\Users\\eric') del env['HOMEDRIVE'] - tester('ntpath.expanduser("~test")', 'eric\\test') - tester('ntpath.expanduser("~")', 'eric\\idle') + tester('ntpath.expanduser("~test")', 'Users\\test') + tester('ntpath.expanduser("~")', 'Users\\eric') env.clear() - env['USERPROFILE'] = 'C:\\eric\\idle' - tester('ntpath.expanduser("~test")', 'C:\\eric\\test') - tester('ntpath.expanduser("~")', 'C:\\eric\\idle') + env['USERPROFILE'] = 'C:\\Users\\eric' + env['USERNAME'] = 'eric' + tester('ntpath.expanduser("~test")', 'C:\\Users\\test') + tester('ntpath.expanduser("~")', 'C:\\Users\\eric') tester('ntpath.expanduser("~test\\foo\\bar")', - 'C:\\eric\\test\\foo\\bar') + 'C:\\Users\\test\\foo\\bar') tester('ntpath.expanduser("~test/foo/bar")', - 'C:\\eric\\test/foo/bar') + 'C:\\Users\\test/foo/bar') tester('ntpath.expanduser("~\\foo\\bar")', - 'C:\\eric\\idle\\foo\\bar') + 'C:\\Users\\eric\\foo\\bar') tester('ntpath.expanduser("~/foo/bar")', - 'C:\\eric\\idle/foo/bar') + 'C:\\Users\\eric/foo/bar') # bpo-36264: ignore `HOME` when set on windows env.clear() env['HOME'] = 'F:\\' - env['USERPROFILE'] = 'C:\\eric\\idle' - tester('ntpath.expanduser("~test")', 'C:\\eric\\test') - tester('ntpath.expanduser("~")', 'C:\\eric\\idle') + env['USERPROFILE'] = 'C:\\Users\\eric' + env['USERNAME'] = 'eric' + tester('ntpath.expanduser("~test")', 'C:\\Users\\test') + tester('ntpath.expanduser("~")', 'C:\\Users\\eric') + + # bpo-39899: don't guess another user's home directory if + # `%USERNAME% != basename(%USERPROFILE%)` + env.clear() + env['USERPROFILE'] = 'C:\\Users\\eric' + env['USERNAME'] = 'idle' + tester('ntpath.expanduser("~test")', '~test') + tester('ntpath.expanduser("~")', 'C:\\Users\\eric') + + @unittest.skipUnless(nt, "abspath requires 'nt' module") def test_abspath(self): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 2643119..0c89b6e 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2609,7 +2609,7 @@ class WindowsPathTest(_BasePathTest, unittest.TestCase): env.pop('USERNAME', None) self.assertEqual(p1.expanduser(), P('C:/Users/alice/My Documents')) - self.assertRaises(KeyError, p2.expanduser) + self.assertRaises(RuntimeError, p2.expanduser) env['USERNAME'] = 'alice' self.assertEqual(p2.expanduser(), P('C:/Users/alice/My Documents')) diff --git a/Misc/NEWS.d/next/Library/2020-03-09-20-36-07.bpo-39899.9adF3E.rst b/Misc/NEWS.d/next/Library/2020-03-09-20-36-07.bpo-39899.9adF3E.rst new file mode 100644 index 0000000..5239553 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-03-09-20-36-07.bpo-39899.9adF3E.rst @@ -0,0 +1,3 @@ +:func:`os.path.expanduser()` now refuses to guess Windows home directories if the basename of current user's home directory does not match their username. + +:meth:`pathlib.Path.expanduser()` and :meth:`~pathlib.Path.home()` now consistently raise :exc:`RuntimeError` exception when a home directory cannot be resolved. Previously a :exc:`KeyError` exception could be raised on Windows when the ``"USERNAME"`` environment variable was unset. -- cgit v0.12