[Python-checkins] 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)

zooba webhook-mailer at python.org
Wed Apr 7 18:50:19 EDT 2021


https://github.com/python/cpython/commit/3f3d82b84823eb28abeedf317bbe107bbe7f6492
commit: 3f3d82b84823eb28abeedf317bbe107bbe7f6492
branch: master
author: Barney Gale <barney.gale at gmail.com>
committer: zooba <steve.dower at microsoft.com>
date: 2021-04-07T23:50:13+01:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2020-03-09-20-36-07.bpo-39899.9adF3E.rst
M Doc/library/os.path.rst
M Doc/library/pathlib.rst
M Lib/ntpath.py
M Lib/pathlib.py
M Lib/test/test_ntpath.py
M Lib/test/test_pathlib.py

diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
index 251df4d516eb5..e2f335ee8c309 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 e269bf9a7be12..f15fed3f02a04 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 6f771773a7d1b..421db50a3b7f5 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 9e682dcad9eac..19d45a3d2ba78 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -246,34 +246,6 @@ def make_uri(self, path):
             # 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 @@ def make_uri(self, path):
         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 @@ def group(self, path):
 
     getcwd = os.getcwd
 
+    expanduser = staticmethod(os.path.expanduser)
+
 
 _normal_accessor = _NormalAccessor()
 
@@ -1105,7 +1064,7 @@ def home(cls):
         """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 @@ def expanduser(self):
         """
         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 a8f764f48ca00..f97aca5f94f57 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -503,34 +503,47 @@ def test_expanduser(self):
             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 2643119352790..0c89b6ef141d7 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -2609,7 +2609,7 @@ def check():
                 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 0000000000000..5239553d51c67
--- /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.



More information about the Python-checkins mailing list