import os import stat from nt import _getfullpathname, _getfinalpathname from ntpath import * def realpath(filename, *, strict=False): """Return the canonical path of the specified filename. All symbolic links are resolved if they exist, except if the target is a link cycle. If strict is true, OSError is raised for a missing path component. """ filename = os.fspath(filename) if isinstance(filename, bytes): prefix = b'\\\\?\\' device_prefix = b'\\\\.\\' else: prefix = '\\\\?\\' device_prefix = '\\\\.\\' is_verbatim_path = filename.startswith(prefix) if not is_verbatim_path: # The kernel expects the opened path to be verbatim, so # it must be pre-resolved with GetFullPathNameW(), as # the Windows API would do. filename = _getfullpathname(filename) is_device_path = filename.startswith(device_prefix) if is_device_path: # Resolve it as a \\?\ path. filename = prefix + filename[4:] path, error = _joinrealpath(filename[:0], filename, True, {}) if error is not None and strict: raise error # _joinrealpath() returns a \\?\ verbatim path. Try to convert # it to normal form if filename wasn't a verbatim path. if not is_verbatim_path: if is_device_path: npath = device_prefix + path[4:] else: npath = _verbatim_to_normal(path) # check for reserved names and allowed path length if _getfullpathname(npath) == npath: if npath.startswith(device_prefix): path = npath elif _path_canonicalize(npath) == npath: path = npath return path def _joinrealpath(path, rest, verbatim, seen): if isinstance(path, bytes): sep = b'\\' curdir = b'.' pardir = b'..' else: sep = '\\' curdir = '.' pardir = '..' error = None real_resolve = True root, rest = _path_splitroot(rest) if rest.startswith(sep): if not root: # Only drives, volume names, and UNC shares are supported # for filesystem resolution. For other devices, just # resolve the device name in the object namespace. root, rest = rest, rest[:0] else: # shift initial slashes in rest over to root. i = 1 while True: if rest[i] != sep: break root, rest = root + rest[:i], rest[i:] if root == sep: path, _ = _path_splitroot(path) elif root: # Resolve the real root path in both the object namespace and # the filesystem namespace. The result will be a real global # DOS device, with all object symlinks and filesystem symlinks # recursively resolved, taking into account symlink loops. # Substitute drives and mapped drives are resolved. UNC shares # and device names are normalized to their proper case, and # volume GUID names are mapped to drives. root_normcase = normcase(root) if root_normcase in seen: path = seen[root_normcase] if path is None: # Reparse loop. Join and normalize the rest. real_resolve = False error = _winerror.ERROR_CANT_RESOLVE_FILENAME path = root else: path = _evaluate_root(root) if normcase(path) == root_normcase: # root is real, but maybe the case was normalized. path, tail = _path_splitroot(path) rest = tail + sep + rest if rest else tail if not path and rest.startswith(sep): # It's not supported for path resolution. return rest, error if path != root: seen[root_normcase] = path else: # root is an object symlink, so resolve it. seen[root_normcase] = None path, error = _joinrealpath(path[:0], path, True, seen) if error is not None: # Join and normalize the rest real_resolve = False if not isinstance(error, OSError): error = _oserror(error, root) if path is None: path = root root, tail = _path_splitroot(path) if not root and tail.startswith(sep): # It's not supported for path resolution. if rest: path = path + sep + rest return path, error seen[root_normcase] = path while rest: name, _, rest = rest.partition(sep) if verbatim and (not name or name in (curdir, pardir)): path = path + sep + name continue elif not name or name == curdir: continue if name == pardir: path, tail = _split(path) # Traversing the root path is invalid. if not tail: return None, _winerror.ERROR_INVALID_REPARSE_DATA continue newpath = join(path, name) if not real_resolve: path = newpath continue if len(name) <= 12: # Get the long name from the parent directory. try: newpath = _getlongfilename(newpath) except OSError as ex: if error is None: error = ex error.__traceback__ = None try: st = os.lstat(newpath) # lstat() opens name-surrogate reparse points. Of those, # this routine directly handles symlinks and mountpoints. # For any other type (uncommon), reparse the final path. is_link = stat.S_ISLNK(st.st_mode) if (not is_link and st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT): if st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT: # Verify that the mountpoint can be followed. os.stat(newpath) else: newpath = _getfinalpathname(newpath) except OSError as ex: if error is None: error = ex error.__traceback__ = None is_link = False if not is_link: path = newpath continue newpath_normcase = normcase(newpath) if newpath_normcase in seen: path = seen[newpath_normcase] if path is not None: continue # Reparse loop. Join and normalize the rest. real_resolve = False if error is None: error = _winerror.ERROR_CANT_RESOLVE_FILENAME path = newpath continue seen[newpath_normcase] = None try: link_target = os.readlink(newpath) except OSError as ex: newerror = ex newerror.__traceback__ = None path = newpath else: # verbatim is False when reparsing a filesystem symlink. path, newerror = _joinrealpath(path, link_target, False, seen) if path is None: # The link target path is invalid. path = newpath if newerror: # Join and normalize the rest. real_resolve = False if error is None: error = newerror if not isinstance(error, OSError): error = _oserror(error, newpath) continue seen[newpath_normcase] = path return path, error ####################################### # support functions import ctypes from ctypes import wintypes kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) winpath = ctypes.OleDLL('api-ms-win-core-path-l1-1-0') mpr = ctypes.WinDLL('mpr') kernel32.FindFirstFileW.restype = wintypes.HANDLE kernel32.CloseHandle.argtypes = (wintypes.HANDLE,) INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value _winerrmsg = { 1921: 'The name of the file cannot be resolved by the system', 4392: 'The data present in the reparse point buffer is invalid', } _winerrcode = { 1921: 'ERROR_CANT_RESOLVE_FILENAME', 4392: 'ERROR_INVALID_REPARSE_DATA', } class _winerror: d = vars() for k, v in _winerrcode.items(): d[v] = k def _oserror(error, filename): return OSError(0, _winerrmsg.get(error, ''), filename, error) def _path_splitroot(path): path = os.fspath(path) tmp = os.fsdecode(path) if isinstance(path, bytes) else path buf = ctypes.create_unicode_buffer(tmp.replace('/', '\\')) p = ctypes.c_void_p() try: winpath.PathCchSkipRoot(buf, ctypes.byref(p)) except OSError: return path[:0], path end = (p.value - ctypes.addressof(buf)) // 2 return path[:end], path[end:] # The following functions support bytes paths with a recursive call. def _verbatim_to_normal(path): path = os.fspath(path) if isinstance(path, bytes): return os.fsencode(_verbatim_to_normal(os.fsdecode(path))) prefix = path[:8].upper() if not prefix.startswith('\\\\?\\'): return path if prefix[4:8] == 'UNC\\': path = '\\\\' + path[8:] elif prefix[5:7] == ':\\': path = path[4:] else: path = '\\\\.\\' + path[4:] return path def _getlongfilename(path): path = os.fspath(path) if isinstance(path, bytes): return os.fsencode(_getlongfilename(os.fsdecode(path))) data = wintypes.WIN32_FIND_DATAW() hfind = kernel32.FindFirstFileW(path, ctypes.byref(data)) if hfind != INVALID_HANDLE_VALUE: kernel32.CloseHandle(hfind) path = join(_split(path)[0], data.cFileName) return path def _path_canonicalize(path, flags=1): path = os.fspath(path) if isinstance(path, bytes): return os.fsencode(_path_canonicalize(os.fsdecode(path), flags)) path = path.replace('/', '\\') result = (ctypes.c_wchar * 32767)() winpath.PathCchCanonicalizeEx(result, 32767, path, flags) return result.value # ntpath.split() does not support \\?\UNC\server\share root paths. def _split(path): path = os.fspath(path) if isinstance(path, bytes): return tuple(map(os.fsencode, _split(os.fsdecode(path)))) prefix = path[:8] had_prefix = prefix.upper().replace('/', '\\') == '\\\\?\\UNC\\' if had_prefix: path = '\\\\' + path[8:] head, tail = split(path) if had_prefix: head = '\\\\?\\UNC\\' + head[2:] return head, tail # Internal function that only supports backslashes. def _evaluate_root(path): path = os.fspath(path) if isinstance(path, bytes): return os.fsencode(_evaluate_root(os.fsdecode(path))) if path.startswith('\\\\'): if path.startswith('\\\\?\\'): path = path[4:] else: path = 'UNC\\' + path[2:] i = path.find('\\') if i == -1: i = len(path) device, path = path[:i], path[i:] resolved = False try: target = _querydosdevice(device) except OSError: return '\\\\?\\' + device + path i = target.find('\\', 1) if target and target[0] == '\\' and i != -1: if target[1:i].upper() in ('??', 'DOSDEVICES'): i = i + 1 i2 = target.find('\\', i + 1) if i2 == -1: i2 = len(target) device, path = target[i:i2], target[i2:] + path else: resolved = True if resolved: # Try to expand a mapped drive and normalize UNC # share\server names and Volume{GUID} volume names. unc_path = _get_mapped_unc_path(device + path) if unc_path is not None: root, tail = _path_splitroot(unc_path) elif device.upper() == 'UNC': root, tail = _path_splitroot('\\\\?\\' + device + path) else: root, tail = '\\\\?\\' + device + '\\', path[1:] try: root = _getfinalpathname(root) except OSError: pass else: if tail and not root.endswith('\\'): return root + '\\' + tail return root + tail # Normalize the casing of DOS device names, e.g. # phsyicaldrive0 -> PhysicalDrive0 device_upper = device.upper() for name in _querydosdevice(): if name.upper() == device_upper: device = name break return '\\\\?\\' + device + path # Internal functions that only support native str and backslashes. def _querydosdevice(device=None): buflen = 32767 buf = (ctypes.c_wchar * buflen)() while True: n = kernel32.QueryDosDeviceW(device, buf, buflen) if n != 0: break error = ctypes.get_last_error() if error == 122: # ERROR_INSUFFICIENT_BUFFER buflen *= 2 buf = (ctypes.c_wchar * buflen)() continue raise ctypes.WinError(error) if device is None: return buf[:n].rstrip('\0').split('\0') return buf.value def _get_mapped_unc_path(path): if path[1:3] != ':\\': return None buf = (ctypes.c_char * 65536)() bufsize = ctypes.c_ulong(ctypes.sizeof(buf)) # UNIVERSAL_NAME_INFO_LEVEL = 1 error = mpr.WNetGetUniversalNameW(path, 1, buf, ctypes.byref(bufsize)) if error: return None path = ctypes.c_wchar_p.from_buffer(buf).value if not path.startswith('\\\\?\\'): return '\\\\?\\UNC\\' + path[2:] return path ########################################### # tests import unittest import warnings from test.support import os_helper from test.support import TestFailed import ntpath import ctypes ABSTFN = abspath(os_helper.TESTFN) def _getshortpathname(path): GSPN = ctypes.WinDLL("kernel32", use_last_error=True).GetShortPathNameW GSPN.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32] GSPN.restype = ctypes.c_uint32 result_len = GSPN(path, None, 0) if not result_len: raise OSError("failed to get short path name 0x{:08X}" .format(ctypes.get_last_error())) result = ctypes.create_unicode_buffer(result_len) result_len = GSPN(path, result, result_len) return result[:result_len] def _norm(path): if isinstance(path, (bytes, str, os.PathLike)): return ntpath.normcase(os.fsdecode(path)) elif hasattr(path, "__iter__"): return tuple(ntpath.normcase(os.fsdecode(p)) for p in path) return path def safe_rmdir(dirname): try: os.rmdir(dirname) except OSError: pass def tester(fn, wantResult): fn = fn.replace("\\", "\\\\") gotResult = eval(fn) if wantResult != gotResult and _norm(wantResult) != _norm(gotResult): raise TestFailed("%s should return: %s but returned: %s" \ %(str(fn), str(wantResult), str(gotResult))) # then with bytes fn = fn.replace("('", "(b'") fn = fn.replace('("', '(b"') fn = fn.replace("['", "[b'") fn = fn.replace('["', '[b"') fn = fn.replace(", '", ", b'") fn = fn.replace(', "', ', b"') fn = os.fsencode(fn).decode('latin1') fn = fn.encode('ascii', 'backslashreplace').decode('ascii') with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) gotResult = eval(fn) if _norm(wantResult) != _norm(gotResult): raise TestFailed("%s should return: %s but returned: %s" \ %(str(fn), str(wantResult), repr(gotResult))) class NtpathTestCase(unittest.TestCase): def assertPathEqual(self, path1, path2): if path1 == path2 or _norm(path1) == _norm(path2): return self.assertEqual(path1, path2) def assertPathIn(self, path, pathset): self.assertIn(_norm(path), _norm(pathset)) class TestNtpath(NtpathTestCase): def test_realpath_curdir(self): expected = normpath(os.getcwd()) tester("realpath('.')", expected) tester("realpath('./.')", expected) tester("realpath('/'.join(['.'] * 100))", expected) tester("realpath('.\\.')", expected) tester("realpath('\\'.join(['.'] * 100))", expected) def test_realpath_pardir(self): expected = normpath(os.getcwd()) tester("realpath('..')", dirname(expected)) tester("realpath('../..')", dirname(dirname(expected))) tester("realpath('/'.join(['..'] * 50))", splitdrive(expected)[0] + '\\') tester("realpath('..\\..')", dirname(dirname(expected))) tester("realpath('\\'.join(['..'] * 50))", splitdrive(expected)[0] + '\\') def test_realpath_basic(self): self.addCleanup(os_helper.unlink, ABSTFN) self.addCleanup(os_helper.unlink, ABSTFN + "1") open(ABSTFN, "wb").close() os.symlink(ABSTFN, ABSTFN + "1") self.assertPathEqual(realpath(ABSTFN + "1"), ABSTFN) self.assertPathEqual(realpath(os.fsencode(ABSTFN + "1")), os.fsencode(ABSTFN)) def test_realpath_relative(self): self.addCleanup(os_helper.unlink, ABSTFN) self.addCleanup(os_helper.unlink, ABSTFN + "1") open(ABSTFN, "wb").close() os.symlink(ABSTFN, relpath(ABSTFN + "1")) self.assertPathEqual(realpath(ABSTFN + "1"), ABSTFN) def test_realpath_nul(self): tester("realpath('NUL')", r'\\.\NUL') def test_realpath_cwd(self): self.addCleanup(os_helper.rmtree, ABSTFN) os_helper.unlink(ABSTFN) os_helper.rmtree(ABSTFN) os.mkdir(ABSTFN) test_dir_long = join(ABSTFN, "MyVeryLongDirectoryName") os.mkdir(test_dir_long) test_dir_short = _getshortpathname(test_dir_long) test_file_long = join(test_dir_long, "file.txt") test_file_short = join(test_dir_short, "file.txt") with open(test_file_long, "wb") as f: f.write(b"content") with os_helper.change_cwd(test_dir_long): self.assertPathEqual(test_file_long, realpath("file.txt")) with os_helper.change_cwd(test_dir_long.lower()): self.assertPathEqual(test_file_long, realpath("file.txt")) with os_helper.change_cwd(test_dir_short): self.assertPathEqual(test_file_long, realpath("file.txt")) self.assertPathEqual(test_file_long, realpath(test_file_short)) def test_realpath_symlink_prefix(self): self.addCleanup(os_helper.unlink, ABSTFN + "3") self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.") self.addCleanup(os_helper.unlink, ABSTFN + "3link") self.addCleanup(os_helper.unlink, ABSTFN + "3.link") with open(ABSTFN + "3", "wb") as f: f.write(b'0') os.symlink(ABSTFN + "3", ABSTFN + "3link") with open("\\\\?\\" + ABSTFN + "3.", "wb") as f: f.write(b'1') os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link") self.assertPathEqual(realpath(ABSTFN + "3link"), ABSTFN + "3") self.assertPathEqual(realpath(ABSTFN + "3.link"), "\\\\?\\" + ABSTFN + "3.") # Resolved paths should be usable to open target files with open(realpath(ABSTFN + "3link"), "rb") as f: self.assertEqual(f.read(), b'0') with open(realpath(ABSTFN + "3.link"), "rb") as f: self.assertEqual(f.read(), b'1') # When the prefix is included, it is not stripped self.assertPathEqual(realpath("\\\\?\\" + ABSTFN + "3link"), "\\\\?\\" + ABSTFN + "3") self.assertPathEqual(realpath("\\\\?\\" + ABSTFN + "3.link"), "\\\\?\\" + ABSTFN + "3.") def test_realpath_symlink_loops(self): # Symlink loops are non-deterministic as to which path is returned, but # it will always be the fully resolved path of one member of the cycle self.addCleanup(os_helper.unlink, ABSTFN) self.addCleanup(os_helper.unlink, ABSTFN + "1") self.addCleanup(os_helper.unlink, ABSTFN + "2") self.addCleanup(os_helper.unlink, ABSTFN + "y") self.addCleanup(os_helper.unlink, ABSTFN + "c") self.addCleanup(os_helper.unlink, ABSTFN + "a") os.symlink(ABSTFN, ABSTFN) os.symlink(ABSTFN + "1", ABSTFN + "2") os.symlink(ABSTFN + "2", ABSTFN + "1") os.symlink(ABSTFN + "x", ABSTFN + "y") os.symlink("..\\" + basename(dirname(ABSTFN)) + "\\" + basename(ABSTFN) + "c", ABSTFN + "c") os.symlink(basename(ABSTFN) + "a\\b", ABSTFN + "a") expected = (ABSTFN + "1", ABSTFN + "2") self.assertPathEqual(realpath(ABSTFN), ABSTFN) self.assertPathIn(realpath(ABSTFN + "1"), expected) self.assertPathIn(realpath(ABSTFN + "2"), expected) self.assertPathIn(realpath(ABSTFN + "1\\x"), (join(r, "x") for r in expected)) self.assertPathEqual(realpath(ABSTFN + "1\\.."), dirname(ABSTFN)) self.assertPathEqual(realpath(ABSTFN + "1\\..\\x"), dirname(ABSTFN) + "\\x") self.assertPathEqual(realpath(ABSTFN + "1\\..\\" + basename(ABSTFN) + "y"), ABSTFN + "x") self.assertPathIn(realpath(ABSTFN + "1\\..\\" + basename(ABSTFN) + "1"), expected) self.assertPathEqual(realpath(ABSTFN + "c"), ABSTFN + "c") # Test using relative path as well. self.assertPathEqual(realpath(basename(ABSTFN)), ABSTFN) # NB: changed to agree with POSIX r''' self.assertPathEqual(realpath(ABSTFN + "a"), ABSTFN + "a") ''' self.assertPathEqual(realpath(ABSTFN + "a"), ABSTFN + "a\\b") def test_realpath_broken_symlinks(self): os.mkdir(ABSTFN) self.addCleanup(os_helper.rmtree, ABSTFN) with os_helper.change_cwd(ABSTFN): os.mkdir("subdir") os.chdir("subdir") os.symlink(".", "recursive") os.symlink("..", "parent") os.chdir("..") os.symlink(".", "self") os.symlink("missing", "broken") os.symlink(ABSTFN + r"\broken", "broken4") os.symlink(r"recursive\..\broken", "broken5") self.assertPathEqual(realpath("broken"), ABSTFN + r"\missing") self.assertPathEqual(realpath(r"broken\foo"), ABSTFN + r"\missing\foo") self.assertPathEqual(realpath("broken4"), ABSTFN + r"\missing") self.assertPathEqual(realpath("broken5"), ABSTFN + r"\missing") self.assertPathEqual(realpath(b"broken"), os.fsencode(ABSTFN + r"\missing")) self.assertPathEqual(realpath(rb"broken\foo"), os.fsencode(ABSTFN + r"\missing\foo")) self.assertPathEqual(realpath(b"broken4"), os.fsencode(ABSTFN + r"\missing")) self.assertPathEqual(realpath(b"broken5"), os.fsencode(ABSTFN + r"\missing")) # NB: ntpath._joinrealpath behaves like POSIX in these cases. r''' # bpo-38453: We no longer recursively resolve segments of relative # symlinks that the OS cannot resolve. os.symlink(r"broken\bar", "broken1") os.symlink(r"self\self\broken", "broken2") os.symlink(r"subdir\parent\subdir\parent\broken", "broken3") self.assertPathEqual(realpath(r"broken1"), ABSTFN + r"\broken\bar") self.assertPathEqual(realpath(r"broken1\baz"), ABSTFN + r"\broken\bar\baz") self.assertPathEqual(realpath("broken2"), ABSTFN + r"\self\self\missing") self.assertPathEqual(realpath("broken3"), ABSTFN + r"\subdir\parent\subdir\parent\missing") ''' class PosixPathTest(unittest.TestCase): def test_realpath_curdir(self): self.assertEqual(realpath('.'), os.getcwd()) self.assertEqual(realpath('.\\.'), os.getcwd()) self.assertEqual(realpath('\\'.join(['.'] * 100)), os.getcwd()) def test_realpath_pardir(self): self.assertEqual(realpath('..'), dirname(os.getcwd())) self.assertEqual(realpath('..\\..'), dirname(dirname(os.getcwd()))) root, _ = _path_splitroot(os.getcwd()) self.assertEqual(realpath('\\'.join(['..'] * 100)), root) def test_realpath_basic(self): # Basic operation. try: os.symlink(ABSTFN+"1", ABSTFN) self.assertEqual(realpath(ABSTFN), ABSTFN+"1") finally: os_helper.unlink(ABSTFN) def test_realpath_relative(self): try: os.symlink(relpath(ABSTFN+"1"), ABSTFN) self.assertEqual(realpath(ABSTFN), ABSTFN+"1") finally: os_helper.unlink(ABSTFN) def test_realpath_repeated_indirect_symlinks(self): # Issue #6975. try: os.mkdir(ABSTFN) os.symlink('..\\' + basename(ABSTFN), ABSTFN + '\\self') os.symlink('self\\self\\self', ABSTFN + '\\link') self.assertEqual(realpath(ABSTFN + '\\link'), ABSTFN) finally: os_helper.unlink(ABSTFN + '\\self') os_helper.unlink(ABSTFN + '\\link') safe_rmdir(ABSTFN) def test_realpath_deep_recursion(self): depth = 10 try: os.mkdir(ABSTFN) for i in range(depth): os.symlink('\\'.join(['%d' % i] * 10), ABSTFN + '\\%d' % (i + 1)) os.symlink('.', ABSTFN + '\\0') self.assertEqual(realpath(ABSTFN + '\\%d' % depth), ABSTFN) # Test using relative path as well. with os_helper.change_cwd(ABSTFN): self.assertEqual(realpath('%d' % depth), ABSTFN) finally: for i in range(depth + 1): os_helper.unlink(ABSTFN + '\\%d' % i) safe_rmdir(ABSTFN) def test_realpath_resolve_parents(self): # We also need to resolve any symlinks in the parents of a relative # path passed to realpath. E.g.: current working directory is # /usr/doc with 'doc' being a symlink to /usr/share/doc. We call # realpath("a"). This should return /usr/share/doc/a/. try: os.mkdir(ABSTFN) os.mkdir(ABSTFN + "\\y") os.symlink(ABSTFN + "\\y", ABSTFN + "\\k") with os_helper.change_cwd(ABSTFN + "\\k"): self.assertEqual(realpath("a"), ABSTFN + "\\y\\a") finally: os_helper.unlink(ABSTFN + "\\k") safe_rmdir(ABSTFN + "\\y") safe_rmdir(ABSTFN) def test_realpath_resolve_first(self): # Bug #1213894: The first component of the path, if not absolute, # must be resolved too. try: os.mkdir(ABSTFN) os.mkdir(ABSTFN + "\\k") os.symlink(ABSTFN, ABSTFN + "link") with os_helper.change_cwd(dirname(ABSTFN)): base = basename(ABSTFN) self.assertEqual(realpath(base + "link"), ABSTFN) self.assertEqual(realpath(base + "link\\k"), ABSTFN + "\\k") finally: os_helper.unlink(ABSTFN + "link") safe_rmdir(ABSTFN + "\\k") safe_rmdir(ABSTFN) def test_realpath_symlink_loops(self): # Bug #930024, return the path unchanged if we get into an infinite # symlink loop. try: os.symlink(ABSTFN, ABSTFN) os.symlink(ABSTFN+"1", ABSTFN+"2") os.symlink(ABSTFN+"2", ABSTFN+"1") os.symlink(ABSTFN+"x", ABSTFN+"y") os.symlink(basename(ABSTFN) + "a\\b", ABSTFN+"a") os.symlink("..\\" + basename(dirname(ABSTFN)) + "\\" + basename(ABSTFN) + "c", ABSTFN+"c") self.assertEqual(realpath(ABSTFN), ABSTFN) self.assertEqual(realpath(ABSTFN+"1"), ABSTFN+"1") self.assertEqual(realpath(ABSTFN+"2"), ABSTFN+"2") self.assertEqual(realpath(ABSTFN+"1\\x"), ABSTFN+"1\\x") self.assertEqual(realpath(ABSTFN+"1\\.."), dirname(ABSTFN)) self.assertEqual(realpath(ABSTFN+"1\\..\\x"), dirname(ABSTFN) + "\\x") self.assertEqual(realpath(ABSTFN+"1\\..\\" + basename(ABSTFN) + "1"), ABSTFN + "1") self.assertEqual(realpath(ABSTFN+"a"), ABSTFN+"a\\b") self.assertEqual(realpath(ABSTFN+"c"), ABSTFN+"c") # Test using relative path as well. with os_helper.change_cwd(dirname(ABSTFN)): self.assertEqual(realpath(basename(ABSTFN)), ABSTFN) # NB: Removed because Windows resolves the opened path, so # `ABSTFN + "1"` is never seen by _joinrealpath(). r''' self.assertEqual(realpath(ABSTFN+"1\\..\\" + basename(ABSTFN) + "y"), ABSTFN + "y") ''' finally: os_helper.unlink(ABSTFN) os_helper.unlink(ABSTFN+"2") os_helper.unlink(ABSTFN+"1") os_helper.unlink(ABSTFN+"y") os_helper.unlink(ABSTFN+"c") os_helper.unlink(ABSTFN+"a") # NB: Windows normalizes the opened path, so this test is not applicable. r''' def test_realpath_resolve_before_normalizing(self): # Bug #990669: Symbolic links should be resolved before we # normalize the path. E.g.: if we have directories 'a', 'k' and 'y' # in the following hierarchy: # a\\k\\y # # and a symbolic link 'link-y' pointing to 'y' in directory 'a', # then realpath("link-y\\..") should return 'k', not 'a'. try: os.mkdir(ABSTFN) os.mkdir(ABSTFN + "\\k") os.mkdir(ABSTFN + "\\k\\y") os.symlink(ABSTFN + "\\k\\y", ABSTFN + "\\link-y")# # Absolute path. self.assertEqual(realpath(ABSTFN + "\\link-y\\.."), ABSTFN + "\\k") # Relative path. with os_helper.change_cwd(dirname(ABSTFN)): self.assertEqual(realpath(basename(ABSTFN) + "\\link-y\\.."), ABSTFN + "\\k") finally: os_helper.unlink(ABSTFN + "\\link-y") safe_rmdir(ABSTFN + "\\k\\y") safe_rmdir(ABSTFN + "\\k") safe_rmdir(ABSTFN) ''' if __name__ == '__main__': unittest.main()