Skip to content

Commit

Permalink
bpo-39899: os.path.expanduser(): don't guess other Windows users' hom…
Browse files Browse the repository at this point in the history
…e 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.
  • Loading branch information
barneygale committed Apr 7, 2021
1 parent df5dc1c commit 3f3d82b
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 68 deletions.
4 changes: 2 additions & 2 deletions Doc/library/os.path.rst
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions Doc/library/pathlib.rst
Expand Up @@ -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')
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 15 additions & 3 deletions Lib/ntpath.py
Expand Up @@ -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:]


Expand Down
51 changes: 6 additions & 45 deletions Lib/pathlib.py
Expand Up @@ -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 = '/'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -463,6 +420,8 @@ def group(self, path):

getcwd = os.getcwd

expanduser = staticmethod(os.path.expanduser)


_normal_accessor = _NormalAccessor()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 28 additions & 15 deletions Lib/test/test_ntpath.py
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_pathlib.py
Expand Up @@ -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'))
Expand Down
@@ -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.

0 comments on commit 3f3d82b

Please sign in to comment.