bpo-14094: Use _getfinalpathname to implement realpath by vladima · Pull Request #11248 · python/cpython

I think all comments are addressed, is there anything else?

Feedback from core devs would be appreciated (@vstinner, @serhiy-storchaka, @zooba, @ZackerySpytz, @pfmoore, @tjguk, @zware).

The short-name test needs to be hardened to get the short name of the directory via FindFirstFileW. Here's a function to get the short name of a file (alternatively, for the entire path, call GetShortPathNameW):

import ctypes
from ctypes import wintypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
kernel32.FindFirstFileW.restype = wintypes.HANDLE
kernel32.FindClose.argtypes = (wintypes.HANDLE,)

def get_short_name(path):
    info = wintypes.WIN32_FIND_DATAW()
    h = kernel32.FindFirstFileW(path, ctypes.byref(info))
    if h == INVALID_HANDLE_VALUE:
        raise ctypes.WinError(ctypes.get_last_error())
    kernel32.FindClose(h)
    return info.cAlternateFileName

Skip the test if get_short_name returns an empty string. In this case, the file system of the temp directory may not support short names, or automatic creation of short names may be disabled.

I think more cases need to be tested, including adapting tests from posixpath, if any are relevant.

  • An input extended path should always be returned as an extended path.
  • Normal paths replace forward slash with backslash, but extended paths do not. Join the junction path with non-existent "spam/eggs". Resolve it as a normal path, and verify that the junction target is resolved and the unresolved part is normalized to "spam\eggs". Do the same but with an extended path, and verify that the unresolved part is left as "spam/eggs".
  • Normal paths resolve "." and ".." components in user mode, before making a system call. Join the junction path with ".\spam\..". Resolve it using a normal path, and verify that it's the junction target. Resolve it using an extended path, and verify that the result is an extended path for the junction target that's joined with the unresolved part ".\spam\..". (Windows diverges from Unix on this point, and it shows up especially when handling symlinks and junction mountpoints. It's also why we can lead with an abspath call in realpath, which minimizes the risk of a working-directory race-condition for a relative path and immediately resolves reserved DOS device names. File systems may reserve "." and ".." as names, but these names have no other significance in the kernel. Using an extended path, FAT32 will even let us create directories or files named "." and "..". They will then appear twice in the directory listing, except only once in the root directory.)
  • Normal paths strip trailing spaces and dots from the final component. Use an extended path to create a file named "spam. . .". Resolve it using a normal path, and verify that it returns non-existing "spam" as the name. Create a symlink to the extended-path target. The symlink privilege may be required; skip the rest if this fails. Resolve the link using a normal path, and verify that it returns an extended path for "spam. . ." . (We can't target a directory named "spam. . ." using a junction, since _winapi.CreateJunction mistakenly calls GetFullPathNameW on an extended-path target. If this gets fixed, we can use a junction instead.)
  • Normal paths reserve DOS device names in the final path component of logical-drive paths. Use an extended path to create a file named "nul: .txt"; make sure this is a logical-drive path. Resolve it using a normal path, and verify that it returns the device path "\\.\nul". Create a symlink to the extended-path target. Skip the rest if this fails. Resolve the link using a normal path, and verify that it returns an extended path for "nul: .txt".