Skip to content

Commit 3f3d82b

Browse files
authored
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.
1 parent df5dc1c commit 3f3d82b

File tree

7 files changed

+63
-68
lines changed

7 files changed

+63
-68
lines changed

Doc/library/os.path.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ the :mod:`glob` module.)
175175

176176
On Windows, :envvar:`USERPROFILE` will be used if set, otherwise a combination
177177
of :envvar:`HOMEPATH` and :envvar:`HOMEDRIVE` will be used. An initial
178-
``~user`` is handled by stripping the last directory component from the created
179-
user path derived above.
178+
``~user`` is handled by checking that the last directory component of the current
179+
user's home directory matches :envvar:`USERNAME`, and replacing it if so.
180180

181181
If the expansion fails or if the path does not begin with a tilde, the path is
182182
returned unchanged.

Doc/library/pathlib.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,10 @@ call fails (for example because the path doesn't exist).
705705
.. classmethod:: Path.home()
706706

707707
Return a new path object representing the user's home directory (as
708-
returned by :func:`os.path.expanduser` with ``~`` construct)::
708+
returned by :func:`os.path.expanduser` with ``~`` construct). If the home
709+
directory can't be resolved, :exc:`RuntimeError` is raised.
710+
711+
::
709712

710713
>>> Path.home()
711714
PosixPath('/home/antoine')
@@ -773,7 +776,10 @@ call fails (for example because the path doesn't exist).
773776
.. method:: Path.expanduser()
774777

775778
Return a new path with expanded ``~`` and ``~user`` constructs,
776-
as returned by :meth:`os.path.expanduser`::
779+
as returned by :meth:`os.path.expanduser`. If a home directory can't be
780+
resolved, :exc:`RuntimeError` is raised.
781+
782+
::
777783

778784
>>> p = PosixPath('~/films/Monty Python')
779785
>>> p.expanduser()

Lib/ntpath.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,24 @@ def expanduser(path):
312312
drive = ''
313313
userhome = join(drive, os.environ['HOMEPATH'])
314314

315+
if i != 1: #~user
316+
# Try to guess user home directory. By default all users directories
317+
# are located in the same place and are named by corresponding
318+
# usernames. If current user home directory points to nonstandard
319+
# place, this guess is likely wrong, and so we bail out.
320+
current_user = os.environ.get('USERNAME')
321+
if current_user != basename(userhome):
322+
return path
323+
324+
target_user = path[1:i]
325+
if isinstance(target_user, bytes):
326+
target_user = os.fsdecode(target_user)
327+
if target_user != current_user:
328+
userhome = join(dirname(userhome), target_user)
329+
315330
if isinstance(path, bytes):
316331
userhome = os.fsencode(userhome)
317332

318-
if i != 1: #~user
319-
userhome = join(dirname(userhome), path[1:i])
320-
321333
return userhome + path[i:]
322334

323335

Lib/pathlib.py

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -246,34 +246,6 @@ def make_uri(self, path):
246246
# It's a path on a network drive => 'file://host/share/a/b'
247247
return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8'))
248248

249-
def gethomedir(self, username):
250-
if 'USERPROFILE' in os.environ:
251-
userhome = os.environ['USERPROFILE']
252-
elif 'HOMEPATH' in os.environ:
253-
try:
254-
drv = os.environ['HOMEDRIVE']
255-
except KeyError:
256-
drv = ''
257-
userhome = drv + os.environ['HOMEPATH']
258-
else:
259-
raise RuntimeError("Can't determine home directory")
260-
261-
if username:
262-
# Try to guess user home directory. By default all users
263-
# directories are located in the same place and are named by
264-
# corresponding usernames. If current user home directory points
265-
# to nonstandard place, this guess is likely wrong.
266-
if os.environ['USERNAME'] != username:
267-
drv, root, parts = self.parse_parts((userhome,))
268-
if parts[-1] != os.environ['USERNAME']:
269-
raise RuntimeError("Can't determine home directory "
270-
"for %r" % username)
271-
parts[-1] = username
272-
if drv or root:
273-
userhome = drv + root + self.join(parts[1:])
274-
else:
275-
userhome = self.join(parts)
276-
return userhome
277249

278250
class _PosixFlavour(_Flavour):
279251
sep = '/'
@@ -364,21 +336,6 @@ def make_uri(self, path):
364336
bpath = bytes(path)
365337
return 'file://' + urlquote_from_bytes(bpath)
366338

367-
def gethomedir(self, username):
368-
if not username:
369-
try:
370-
return os.environ['HOME']
371-
except KeyError:
372-
import pwd
373-
return pwd.getpwuid(os.getuid()).pw_dir
374-
else:
375-
import pwd
376-
try:
377-
return pwd.getpwnam(username).pw_dir
378-
except KeyError:
379-
raise RuntimeError("Can't determine home directory "
380-
"for %r" % username)
381-
382339

383340
_windows_flavour = _WindowsFlavour()
384341
_posix_flavour = _PosixFlavour()
@@ -463,6 +420,8 @@ def group(self, path):
463420

464421
getcwd = os.getcwd
465422

423+
expanduser = staticmethod(os.path.expanduser)
424+
466425

467426
_normal_accessor = _NormalAccessor()
468427

@@ -1105,7 +1064,7 @@ def home(cls):
11051064
"""Return a new path pointing to the user's home directory (as
11061065
returned by os.path.expanduser('~')).
11071066
"""
1108-
return cls(cls()._flavour.gethomedir(None))
1067+
return cls("~").expanduser()
11091068

11101069
def samefile(self, other_path):
11111070
"""Return whether other_path is the same or not as this file
@@ -1517,7 +1476,9 @@ def expanduser(self):
15171476
"""
15181477
if (not (self._drv or self._root) and
15191478
self._parts and self._parts[0][:1] == '~'):
1520-
homedir = self._flavour.gethomedir(self._parts[0][1:])
1479+
homedir = self._accessor.expanduser(self._parts[0])
1480+
if homedir[:1] == "~":
1481+
raise RuntimeError("Could not determine home directory.")
15211482
return self._from_parts([homedir] + self._parts[1:])
15221483

15231484
return self

Lib/test/test_ntpath.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -503,34 +503,47 @@ def test_expanduser(self):
503503
env.clear()
504504
tester('ntpath.expanduser("~test")', '~test')
505505

506-
env['HOMEPATH'] = 'eric\\idle'
507506
env['HOMEDRIVE'] = 'C:\\'
508-
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
509-
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
507+
env['HOMEPATH'] = 'Users\\eric'
508+
env['USERNAME'] = 'eric'
509+
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
510+
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')
510511

511512
del env['HOMEDRIVE']
512-
tester('ntpath.expanduser("~test")', 'eric\\test')
513-
tester('ntpath.expanduser("~")', 'eric\\idle')
513+
tester('ntpath.expanduser("~test")', 'Users\\test')
514+
tester('ntpath.expanduser("~")', 'Users\\eric')
514515

515516
env.clear()
516-
env['USERPROFILE'] = 'C:\\eric\\idle'
517-
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
518-
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
517+
env['USERPROFILE'] = 'C:\\Users\\eric'
518+
env['USERNAME'] = 'eric'
519+
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
520+
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')
519521
tester('ntpath.expanduser("~test\\foo\\bar")',
520-
'C:\\eric\\test\\foo\\bar')
522+
'C:\\Users\\test\\foo\\bar')
521523
tester('ntpath.expanduser("~test/foo/bar")',
522-
'C:\\eric\\test/foo/bar')
524+
'C:\\Users\\test/foo/bar')
523525
tester('ntpath.expanduser("~\\foo\\bar")',
524-
'C:\\eric\\idle\\foo\\bar')
526+
'C:\\Users\\eric\\foo\\bar')
525527
tester('ntpath.expanduser("~/foo/bar")',
526-
'C:\\eric\\idle/foo/bar')
528+
'C:\\Users\\eric/foo/bar')
527529

528530
# bpo-36264: ignore `HOME` when set on windows
529531
env.clear()
530532
env['HOME'] = 'F:\\'
531-
env['USERPROFILE'] = 'C:\\eric\\idle'
532-
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
533-
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
533+
env['USERPROFILE'] = 'C:\\Users\\eric'
534+
env['USERNAME'] = 'eric'
535+
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
536+
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')
537+
538+
# bpo-39899: don't guess another user's home directory if
539+
# `%USERNAME% != basename(%USERPROFILE%)`
540+
env.clear()
541+
env['USERPROFILE'] = 'C:\\Users\\eric'
542+
env['USERNAME'] = 'idle'
543+
tester('ntpath.expanduser("~test")', '~test')
544+
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')
545+
546+
534547

535548
@unittest.skipUnless(nt, "abspath requires 'nt' module")
536549
def test_abspath(self):

Lib/test/test_pathlib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2609,7 +2609,7 @@ def check():
26092609
env.pop('USERNAME', None)
26102610
self.assertEqual(p1.expanduser(),
26112611
P('C:/Users/alice/My Documents'))
2612-
self.assertRaises(KeyError, p2.expanduser)
2612+
self.assertRaises(RuntimeError, p2.expanduser)
26132613
env['USERNAME'] = 'alice'
26142614
self.assertEqual(p2.expanduser(),
26152615
P('C:/Users/alice/My Documents'))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
: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.
2+
3+
: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 commit comments

Comments
 (0)