mirror of https://github.com/python/cpython
gh-82367: Use `FindFirstFile` Win32 API in `ntpath.realpath()` (GH-110298)
* Use `FindFirstFile` Win32 API to fix a bug where `ntpath.realpath()` breaks out of traversing a series of paths where a (handled) `ERROR_ACCESS_DENIED` or `ERROR_SHARING_VIOLATION` occurs. * Update docs to reflect that `ntpath.realpath()` eliminates MS-DOS style names.
This commit is contained in:
parent
2cb62c6437
commit
d33aa18f15
|
@ -377,7 +377,8 @@ the :mod:`glob` module.)
|
||||||
|
|
||||||
Return the canonical path of the specified filename, eliminating any symbolic
|
Return the canonical path of the specified filename, eliminating any symbolic
|
||||||
links encountered in the path (if they are supported by the operating
|
links encountered in the path (if they are supported by the operating
|
||||||
system).
|
system). On Windows, this function will also resolve MS-DOS (also called 8.3)
|
||||||
|
style names such as ``C:\\PROGRA~1`` to ``C:\\Program Files``.
|
||||||
|
|
||||||
If a path doesn't exist or a symlink loop is encountered, and *strict* is
|
If a path doesn't exist or a symlink loop is encountered, and *strict* is
|
||||||
``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
|
``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
|
||||||
|
|
|
@ -97,6 +97,9 @@ Other Language Changes
|
||||||
if supported.
|
if supported.
|
||||||
(Contributed by Victor Stinner in :gh:`109649`.)
|
(Contributed by Victor Stinner in :gh:`109649`.)
|
||||||
|
|
||||||
|
* :func:`os.path.realpath` now resolves MS-DOS style file names even if
|
||||||
|
the file is not accessible.
|
||||||
|
(Contributed by Moonsik Park in :gh:`82367`.)
|
||||||
|
|
||||||
New Modules
|
New Modules
|
||||||
===========
|
===========
|
||||||
|
|
|
@ -23,7 +23,6 @@ import stat
|
||||||
import genericpath
|
import genericpath
|
||||||
from genericpath import *
|
from genericpath import *
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
|
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
|
||||||
"basename","dirname","commonprefix","getsize","getmtime",
|
"basename","dirname","commonprefix","getsize","getmtime",
|
||||||
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
|
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
|
||||||
|
@ -601,7 +600,7 @@ else: # use native Windows method on Windows
|
||||||
return _abspath_fallback(path)
|
return _abspath_fallback(path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from nt import _getfinalpathname, readlink as _nt_readlink
|
from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# realpath is a no-op on systems without _getfinalpathname support.
|
# realpath is a no-op on systems without _getfinalpathname support.
|
||||||
realpath = abspath
|
realpath = abspath
|
||||||
|
@ -688,10 +687,15 @@ else:
|
||||||
except OSError:
|
except OSError:
|
||||||
# If we fail to readlink(), let's keep traversing
|
# If we fail to readlink(), let's keep traversing
|
||||||
pass
|
pass
|
||||||
|
# If we get these errors, try to get the real name of the file without accessing it.
|
||||||
|
if ex.winerror in (1, 5, 32, 50, 87, 1920, 1921):
|
||||||
|
try:
|
||||||
|
name = _findfirstfile(path)
|
||||||
|
path, _ = split(path)
|
||||||
|
except OSError:
|
||||||
|
path, name = split(path)
|
||||||
|
else:
|
||||||
path, name = split(path)
|
path, name = split(path)
|
||||||
# TODO (bpo-38186): Request the real file name from the directory
|
|
||||||
# entry using FindFirstFileW. For now, we will return the path
|
|
||||||
# as best we have it
|
|
||||||
if path and not name:
|
if path and not name:
|
||||||
return path + tail
|
return path + tail
|
||||||
tail = join(name, tail) if tail else name
|
tail = join(name, tail) if tail else name
|
||||||
|
|
|
@ -2,6 +2,7 @@ import inspect
|
||||||
import ntpath
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -637,6 +638,48 @@ class TestNtpath(NtpathTestCase):
|
||||||
with os_helper.change_cwd(test_dir_short):
|
with os_helper.change_cwd(test_dir_short):
|
||||||
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
|
||||||
|
def test_realpath_permission(self):
|
||||||
|
# Test whether python can resolve the real filename of a
|
||||||
|
# shortened file name even if it does not have permission to access it.
|
||||||
|
ABSTFN = ntpath.realpath(os_helper.TESTFN)
|
||||||
|
|
||||||
|
os_helper.unlink(ABSTFN)
|
||||||
|
os_helper.rmtree(ABSTFN)
|
||||||
|
os.mkdir(ABSTFN)
|
||||||
|
self.addCleanup(os_helper.rmtree, ABSTFN)
|
||||||
|
|
||||||
|
test_file = ntpath.join(ABSTFN, "LongFileName123.txt")
|
||||||
|
test_file_short = ntpath.join(ABSTFN, "LONGFI~1.TXT")
|
||||||
|
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"content")
|
||||||
|
# Automatic generation of short names may be disabled on
|
||||||
|
# NTFS volumes for the sake of performance.
|
||||||
|
# They're not supported at all on ReFS and exFAT.
|
||||||
|
subprocess.run(
|
||||||
|
# Try to set the short name manually.
|
||||||
|
['fsutil.exe', 'file', 'setShortName', test_file, 'LONGFI~1.TXT'],
|
||||||
|
creationflags=subprocess.DETACHED_PROCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
|
||||||
|
except AssertionError:
|
||||||
|
raise unittest.SkipTest('the filesystem seems to lack support for short filenames')
|
||||||
|
|
||||||
|
# Deny the right to [S]YNCHRONIZE on the file to
|
||||||
|
# force nt._getfinalpathname to fail with ERROR_ACCESS_DENIED.
|
||||||
|
p = subprocess.run(
|
||||||
|
['icacls.exe', test_file, '/deny', '*S-1-5-32-545:(S)'],
|
||||||
|
creationflags=subprocess.DETACHED_PROCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
if p.returncode:
|
||||||
|
raise unittest.SkipTest('failed to deny access to the test file')
|
||||||
|
|
||||||
|
self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
|
||||||
|
|
||||||
def test_expandvars(self):
|
def test_expandvars(self):
|
||||||
with os_helper.EnvironmentVarGuard() as env:
|
with os_helper.EnvironmentVarGuard() as env:
|
||||||
env.clear()
|
env.clear()
|
||||||
|
|
|
@ -1373,6 +1373,7 @@ Peter Parente
|
||||||
Alexandre Parenteau
|
Alexandre Parenteau
|
||||||
Dan Parisien
|
Dan Parisien
|
||||||
HyeSoo Park
|
HyeSoo Park
|
||||||
|
Moonsik Park
|
||||||
William Park
|
William Park
|
||||||
Claude Paroz
|
Claude Paroz
|
||||||
Heikki Partanen
|
Heikki Partanen
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
:func:`os.path.realpath` now resolves MS-DOS style file names even if
|
||||||
|
the file is not accessible. Patch by Moonsik Park.
|
|
@ -1848,6 +1848,40 @@ exit:
|
||||||
|
|
||||||
#if defined(MS_WINDOWS)
|
#if defined(MS_WINDOWS)
|
||||||
|
|
||||||
|
PyDoc_STRVAR(os__findfirstfile__doc__,
|
||||||
|
"_findfirstfile($module, path, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n"
|
||||||
|
"A function to get the real file name without accessing the file in Windows.");
|
||||||
|
|
||||||
|
#define OS__FINDFIRSTFILE_METHODDEF \
|
||||||
|
{"_findfirstfile", (PyCFunction)os__findfirstfile, METH_O, os__findfirstfile__doc__},
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__findfirstfile_impl(PyObject *module, path_t *path);
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__findfirstfile(PyObject *module, PyObject *arg)
|
||||||
|
{
|
||||||
|
PyObject *return_value = NULL;
|
||||||
|
path_t path = PATH_T_INITIALIZE("_findfirstfile", "path", 0, 0);
|
||||||
|
|
||||||
|
if (!path_converter(arg, &path)) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
return_value = os__findfirstfile_impl(module, &path);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
/* Cleanup for path */
|
||||||
|
path_cleanup(&path);
|
||||||
|
|
||||||
|
return return_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* defined(MS_WINDOWS) */
|
||||||
|
|
||||||
|
#if defined(MS_WINDOWS)
|
||||||
|
|
||||||
PyDoc_STRVAR(os__getvolumepathname__doc__,
|
PyDoc_STRVAR(os__getvolumepathname__doc__,
|
||||||
"_getvolumepathname($module, /, path)\n"
|
"_getvolumepathname($module, /, path)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
|
@ -11451,6 +11485,10 @@ exit:
|
||||||
#define OS__GETFINALPATHNAME_METHODDEF
|
#define OS__GETFINALPATHNAME_METHODDEF
|
||||||
#endif /* !defined(OS__GETFINALPATHNAME_METHODDEF) */
|
#endif /* !defined(OS__GETFINALPATHNAME_METHODDEF) */
|
||||||
|
|
||||||
|
#ifndef OS__FINDFIRSTFILE_METHODDEF
|
||||||
|
#define OS__FINDFIRSTFILE_METHODDEF
|
||||||
|
#endif /* !defined(OS__FINDFIRSTFILE_METHODDEF) */
|
||||||
|
|
||||||
#ifndef OS__GETVOLUMEPATHNAME_METHODDEF
|
#ifndef OS__GETVOLUMEPATHNAME_METHODDEF
|
||||||
#define OS__GETVOLUMEPATHNAME_METHODDEF
|
#define OS__GETVOLUMEPATHNAME_METHODDEF
|
||||||
#endif /* !defined(OS__GETVOLUMEPATHNAME_METHODDEF) */
|
#endif /* !defined(OS__GETVOLUMEPATHNAME_METHODDEF) */
|
||||||
|
@ -11986,4 +12024,4 @@ exit:
|
||||||
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
||||||
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
|
||||||
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
|
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
|
||||||
/*[clinic end generated code: output=8b60de6ddb925bc3 input=a9049054013a1b77]*/
|
/*[clinic end generated code: output=a36904281a8a7507 input=a9049054013a1b77]*/
|
||||||
|
|
|
@ -4809,6 +4809,37 @@ cleanup:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
os._findfirstfile
|
||||||
|
path: path_t
|
||||||
|
/
|
||||||
|
A function to get the real file name without accessing the file in Windows.
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
os__findfirstfile_impl(PyObject *module, path_t *path)
|
||||||
|
/*[clinic end generated code: output=106dd3f0779c83dd input=0734dff70f60e1a8]*/
|
||||||
|
{
|
||||||
|
PyObject *result;
|
||||||
|
HANDLE hFindFile;
|
||||||
|
WIN32_FIND_DATAW wFileData;
|
||||||
|
WCHAR *wRealFileName;
|
||||||
|
|
||||||
|
Py_BEGIN_ALLOW_THREADS
|
||||||
|
hFindFile = FindFirstFileW(path->wide, &wFileData);
|
||||||
|
Py_END_ALLOW_THREADS
|
||||||
|
|
||||||
|
if (hFindFile == INVALID_HANDLE_VALUE) {
|
||||||
|
path_error(path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
wRealFileName = wFileData.cFileName;
|
||||||
|
result = PyUnicode_FromWideChar(wRealFileName, wcslen(wRealFileName));
|
||||||
|
FindClose(hFindFile);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
os._getvolumepathname
|
os._getvolumepathname
|
||||||
|
@ -15961,6 +15992,7 @@ static PyMethodDef posix_methods[] = {
|
||||||
OS__GETFULLPATHNAME_METHODDEF
|
OS__GETFULLPATHNAME_METHODDEF
|
||||||
OS__GETDISKUSAGE_METHODDEF
|
OS__GETDISKUSAGE_METHODDEF
|
||||||
OS__GETFINALPATHNAME_METHODDEF
|
OS__GETFINALPATHNAME_METHODDEF
|
||||||
|
OS__FINDFIRSTFILE_METHODDEF
|
||||||
OS__GETVOLUMEPATHNAME_METHODDEF
|
OS__GETVOLUMEPATHNAME_METHODDEF
|
||||||
OS__PATH_SPLITROOT_METHODDEF
|
OS__PATH_SPLITROOT_METHODDEF
|
||||||
OS__PATH_NORMPATH_METHODDEF
|
OS__PATH_NORMPATH_METHODDEF
|
||||||
|
|
Loading…
Reference in New Issue