summaryrefslogtreecommitdiffstats
path: root/Lib/posixpath.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/posixpath.py')
-rw-r--r--Lib/posixpath.py88
1 files changed, 54 insertions, 34 deletions
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
index 76ee721..0e8bb5a 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath.py
@@ -403,55 +403,66 @@ def realpath(filename, *, strict=False):
"""Return the canonical path of the specified filename, eliminating any
symbolic links encountered in the path."""
filename = os.fspath(filename)
- path, ok = _joinrealpath(filename[:0], filename, strict, {})
- return abspath(path)
-
-# Join two paths, normalizing and eliminating any symbolic links
-# encountered in the second path.
-# Two leading slashes are replaced by a single slash.
-def _joinrealpath(path, rest, strict, seen):
- if isinstance(path, bytes):
+ if isinstance(filename, bytes):
sep = b'/'
curdir = b'.'
pardir = b'..'
+ getcwd = os.getcwdb
else:
sep = '/'
curdir = '.'
pardir = '..'
+ getcwd = os.getcwd
+
+ # The stack of unresolved path parts. When popped, a special value of None
+ # indicates that a symlink target has been resolved, and that the original
+ # symlink path can be retrieved by popping again. The [::-1] slice is a
+ # very fast way of spelling list(reversed(...)).
+ rest = filename.split(sep)[::-1]
+
+ # The resolved path, which is absolute throughout this function.
+ # Note: getcwd() returns a normalized and symlink-free path.
+ path = sep if filename.startswith(sep) else getcwd()
- if rest.startswith(sep):
- rest = rest[1:]
- path = sep
+ # Mapping from symlink paths to *fully resolved* symlink targets. If a
+ # symlink is encountered but not yet resolved, the value is None. This is
+ # used both to detect symlink loops and to speed up repeated traversals of
+ # the same links.
+ seen = {}
+
+ # Whether we're calling lstat() and readlink() to resolve symlinks. If we
+ # encounter an OSError for a symlink loop in non-strict mode, this is
+ # switched off.
+ querying = True
while rest:
- name, _, rest = rest.partition(sep)
+ name = rest.pop()
+ if name is None:
+ # resolved symlink target
+ seen[rest.pop()] = path
+ continue
if not name or name == curdir:
# current dir
continue
if name == pardir:
# parent dir
- if path:
- parent, name = split(path)
- if name == pardir:
- # ../..
- path = join(path, pardir)
- else:
- # foo/bar/.. -> foo
- path = parent
- else:
- # ..
- path = pardir
+ path = path[:path.rindex(sep)] or sep
+ continue
+ if path == sep:
+ newpath = path + name
+ else:
+ newpath = path + sep + name
+ if not querying:
+ path = newpath
continue
- newpath = join(path, name)
try:
st = os.lstat(newpath)
+ if not stat.S_ISLNK(st.st_mode):
+ path = newpath
+ continue
except OSError:
if strict:
raise
- is_link = False
- else:
- is_link = stat.S_ISLNK(st.st_mode)
- if not is_link:
path = newpath
continue
# Resolve the symbolic link
@@ -467,14 +478,23 @@ def _joinrealpath(path, rest, strict, seen):
os.stat(newpath)
else:
# Return already resolved part + rest of the path unchanged.
- return join(newpath, rest), False
+ path = newpath
+ querying = False
+ continue
seen[newpath] = None # not resolved symlink
- path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen)
- if not ok:
- return join(path, rest), False
- seen[newpath] = path # resolved symlink
+ target = os.readlink(newpath)
+ if target.startswith(sep):
+ # Symlink target is absolute; reset resolved path.
+ path = sep
+ # Push the symlink path onto the stack, and signal its specialness by
+ # also pushing None. When these entries are popped, we'll record the
+ # fully-resolved symlink target in the 'seen' mapping.
+ rest.append(newpath)
+ rest.append(None)
+ # Push the unresolved symlink target parts onto the stack.
+ rest.extend(target.split(sep)[::-1])
- return path, True
+ return path
supports_unicode_filenames = (sys.platform == 'darwin')