diff --git a/Include/internal/pycore_fileutils.h b/Include/internal/pycore_fileutils.h index 13f86b01bbfe8f..afe29106908bd3 100644 --- a/Include/internal/pycore_fileutils.h +++ b/Include/internal/pycore_fileutils.h @@ -279,7 +279,8 @@ extern size_t _Py_find_basename(const wchar_t *filename); // Export for '_testinternalcapi' shared extension PyAPI_FUNC(wchar_t*) _Py_normpath(wchar_t *path, Py_ssize_t size); -extern wchar_t *_Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *length); +extern wchar_t *_Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t start, + Py_ssize_t *length, int explicit_curdir); // The Windows Games API family does not provide these functions // so provide our own implementations. Remove them in case they get added diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index b7ee46d664ab08..bda759509d7dbe 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -380,7 +380,7 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None, import nt mode = nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS if '/' in name or '\\' in name: - self._name = nt._getfullpathname(self._name) + self._name = nt._path_abspath(self._name) mode |= nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR class _FuncPtr(_CFuncPtr): diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 83e2d3b865757c..1349ff0ec190ad 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -554,36 +554,47 @@ def normpath(path): return prefix + sep.join(comps) -def _abspath_fallback(path): - """Return the absolute version of a path as a fallback function in case - `nt._getfullpathname` is not available or raises OSError. See bpo-31047 for - more. - - """ - - path = os.fspath(path) - if not isabs(path): - if isinstance(path, bytes): - cwd = os.getcwdb() - else: - cwd = os.getcwd() - path = join(cwd, path) - return normpath(path) - # Return an absolute path. try: - from nt import _getfullpathname + from nt import _path_abspath except ImportError: # not running on Windows - mock up something sensible - abspath = _abspath_fallback + def abspath(path): + """Return the absolute version of a path.""" + path = os.fspath(path) + if not isabs(path): + if isinstance(path, bytes): + path = join(os.getcwdb(), path) + else: + path = join(os.getcwd(), path) + return normpath(path) else: # use native Windows method on Windows def abspath(path): """Return the absolute version of a path.""" try: - return _getfullpathname(normpath(path)) + return _path_abspath(path) except (OSError, ValueError): - return _abspath_fallback(path) + # See gh-75230, handle outside for cleaner traceback + pass + path = os.fspath(path) + if not isabs(path): + if isinstance(path, bytes): + sep = b'/' + cwd = os.getcwdb() + else: + sep = '/' + cwd = os.getcwd() + drive, root, path = splitroot(path) + if drive and drive != splitroot(cwd)[0]: + try: + path = join(_path_abspath(drive), path) + except (OSError, ValueError): + # Invalid drive \x00: on Windows; assume root directory + path = drive + sep + path + else: + path = join(cwd, root + path) + return normpath(path) try: from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 47b2aa572e5c65..b99df5a0162f88 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -372,16 +372,19 @@ def normpath(path): return path or dot -def abspath(path): - """Return an absolute path.""" - path = os.fspath(path) - if isinstance(path, bytes): - if not path.startswith(b'/'): - path = join(os.getcwdb(), path) - else: - if not path.startswith('/'): - path = join(os.getcwd(), path) - return normpath(path) +try: + from posix import _path_abspath as abspath +except ImportError: + def abspath(path): + """Return an absolute path.""" + path = os.fspath(path) + if isinstance(path, bytes): + if not path.startswith(b'/'): + path = join(os.getcwdb(), path) + else: + if not path.startswith('/'): + path = join(os.getcwd(), path) + return normpath(path) # Return a canonical path (i.e. the absolute location of a file on the diff --git a/Lib/test/test_ctypes/test_loading.py b/Lib/test/test_ctypes/test_loading.py index fc1eecb77e17e3..2ecafeac7f5017 100644 --- a/Lib/test/test_ctypes/test_loading.py +++ b/Lib/test/test_ctypes/test_loading.py @@ -185,11 +185,11 @@ def should_fail(command): should_pass("windll.kernel32.SetDllDirectoryW(None); WinDLL('_sqlite3.dll', winmode=0)") # Full path load without DLL_LOAD_DIR shouldn't find dependency - should_fail("WinDLL(nt._getfullpathname('_sqlite3.dll'), " + + should_fail("WinDLL(nt._path_abspath('_sqlite3.dll'), " + "winmode=nt._LOAD_LIBRARY_SEARCH_SYSTEM32)") # Full path load with DLL_LOAD_DIR should succeed - should_pass("WinDLL(nt._getfullpathname('_sqlite3.dll'), " + + should_pass("WinDLL(nt._path_abspath('_sqlite3.dll'), " + "winmode=nt._LOAD_LIBRARY_SEARCH_SYSTEM32|" + "nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)") diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 64cbfaaaaa0690..a0e5a8e66279be 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -347,6 +347,7 @@ def test_normpath(self): tester("ntpath.normpath('..')", r'..') tester("ntpath.normpath('.')", r'.') + tester("ntpath.normpath('c:.')", 'c:') tester("ntpath.normpath('')", r'.') tester("ntpath.normpath('/')", '\\') tester("ntpath.normpath('c:/')", 'c:\\') @@ -354,6 +355,7 @@ def test_normpath(self): tester("ntpath.normpath('c:/../../..')", 'c:\\') tester("ntpath.normpath('../.././..')", r'..\..\..') tester("ntpath.normpath('K:../.././..')", r'K:..\..\..') + tester("ntpath.normpath('./a/b')", r'a\b') tester("ntpath.normpath('C:////a/b')", r'C:\a\b') tester("ntpath.normpath('//machine/share//a/b')", r'\\machine\share\a\b') @@ -806,6 +808,9 @@ def test_abspath(self): tester('ntpath.abspath("C:\\spam. . .")', "C:\\spam") tester('ntpath.abspath("C:/nul")', "\\\\.\\nul") tester('ntpath.abspath("C:\\nul")', "\\\\.\\nul") + self.assertTrue(ntpath.isabs(ntpath.abspath("C:spam"))) + self.assertTrue(ntpath.isabs(ntpath.abspath("C:\x00"))) + self.assertTrue(ntpath.isabs(ntpath.abspath("\x00:spam"))) tester('ntpath.abspath("//..")', "\\\\") tester('ntpath.abspath("//../")', "\\\\..\\") tester('ntpath.abspath("//../..")', "\\\\..\\") @@ -836,6 +841,20 @@ def test_abspath(self): tester('ntpath.abspath("")', cwd_dir) tester('ntpath.abspath(" ")', cwd_dir + "\\ ") tester('ntpath.abspath("?")', cwd_dir + "\\?") + tester('ntpath.abspath("con")', r"\\.\con") + # bpo-45354: Windows 11 changed MS-DOS device name handling + if sys.getwindowsversion()[:3] < (10, 0, 22000): + tester('ntpath.abspath("./con")', r"\\.\con") + tester('ntpath.abspath("foo/../con")', r"\\.\con") + tester('ntpath.abspath("con/foo/..")', r"\\.\con") + tester('ntpath.abspath("con/.")', r"\\.\con") + else: + tester('ntpath.abspath("./con")', cwd_dir + r"\con") + tester('ntpath.abspath("foo/../con")', cwd_dir + r"\con") + tester('ntpath.abspath("con/foo/..")', cwd_dir + r"\con") + tester('ntpath.abspath("con/.")', cwd_dir + r"\con") + tester('ntpath.abspath("./Z:spam")', cwd_dir + r"\Z:spam") + tester('ntpath.abspath("spam/../Z:eggs")', cwd_dir + r"\Z:eggs") drive, _ = ntpath.splitdrive(cwd_dir) tester('ntpath.abspath("/abc/")', drive + "\\abc") diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 57a24e9c70d5e5..0071114a57dd2b 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -293,6 +293,8 @@ def test_fast_paths_in_use(self): self.assertFalse(inspect.isfunction(os.path.splitroot)) self.assertTrue(os.path.normpath is posix._path_normpath) self.assertFalse(inspect.isfunction(os.path.normpath)) + self.assertTrue(os.path.abspath is posix._path_abspath) + self.assertFalse(inspect.isfunction(os.path.abspath)) def test_expanduser(self): self.assertEqual(posixpath.expanduser("foo"), "foo") @@ -382,6 +384,7 @@ def test_expanduser_pwd2(self): ("///..//./foo/.//bar", "/foo/bar"), (".", "."), (".//.", "."), + ("./foo/bar", "foo/bar"), ("..", ".."), ("../", ".."), ("../foo", "../foo"), @@ -689,19 +692,21 @@ def test_realpath_unreadable_symlink(self): os.chmod(ABSTFN, 0o755, follow_symlinks=False) os.unlink(ABSTFN) + @skip_if_ABSTFN_contains_backslash def test_relpath(self): - (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: - curdir = os.path.split(os.getcwd())[-1] + os.mkdir(ABSTFN) self.assertRaises(TypeError, posixpath.relpath, None) self.assertRaises(ValueError, posixpath.relpath, "") self.assertEqual(posixpath.relpath("a"), "a") self.assertEqual(posixpath.relpath(posixpath.abspath("a")), "a") self.assertEqual(posixpath.relpath("a/b"), "a/b") self.assertEqual(posixpath.relpath("../a/b"), "../a/b") - self.assertEqual(posixpath.relpath("a", "../b"), "../"+curdir+"/a") - self.assertEqual(posixpath.relpath("a/b", "../c"), - "../"+curdir+"/a/b") + with os_helper.change_cwd(ABSTFN): + curdir = basename(ABSTFN) + self.assertEqual(posixpath.relpath("a", "../b"), "../"+curdir+"/a") + self.assertEqual(posixpath.relpath("a/b", "../c"), + "../"+curdir+"/a/b") self.assertEqual(posixpath.relpath("a", "b/c"), "../../a") self.assertEqual(posixpath.relpath("a", "a"), ".") self.assertEqual(posixpath.relpath("/foo/bar/bat", "/x/y/z"), '../../../foo/bar/bat') @@ -714,21 +719,23 @@ def test_relpath(self): self.assertEqual(posixpath.relpath("/a", "/a"), '.') self.assertEqual(posixpath.relpath("/a/b", "/a/b"), '.') finally: - os.getcwd = real_getcwd + safe_rmdir(ABSTFN) + @skip_if_ABSTFN_contains_backslash def test_relpath_bytes(self): - (real_getcwdb, os.getcwdb) = (os.getcwdb, lambda: br"/home/user/bar") try: - curdir = os.path.split(os.getcwdb())[-1] + os.mkdir(ABSTFN) self.assertRaises(ValueError, posixpath.relpath, b"") self.assertEqual(posixpath.relpath(b"a"), b"a") self.assertEqual(posixpath.relpath(posixpath.abspath(b"a")), b"a") self.assertEqual(posixpath.relpath(b"a/b"), b"a/b") self.assertEqual(posixpath.relpath(b"../a/b"), b"../a/b") - self.assertEqual(posixpath.relpath(b"a", b"../b"), - b"../"+curdir+b"/a") - self.assertEqual(posixpath.relpath(b"a/b", b"../c"), - b"../"+curdir+b"/a/b") + with os_helper.change_cwd(ABSTFN): + curdir = os.fsencode(basename(ABSTFN)) + self.assertEqual(posixpath.relpath(b"a", b"../b"), + b"../"+curdir+b"/a") + self.assertEqual(posixpath.relpath(b"a/b", b"../c"), + b"../"+curdir+b"/a/b") self.assertEqual(posixpath.relpath(b"a", b"b/c"), b"../../a") self.assertEqual(posixpath.relpath(b"a", b"a"), b".") self.assertEqual(posixpath.relpath(b"/foo/bar/bat", b"/x/y/z"), b'../../../foo/bar/bat') @@ -744,7 +751,7 @@ def test_relpath_bytes(self): self.assertRaises(TypeError, posixpath.relpath, b"bytes", "str") self.assertRaises(TypeError, posixpath.relpath, "str", b"bytes") finally: - os.getcwdb = real_getcwdb + safe_rmdir(ABSTFN) def test_commonpath(self): def check(paths, expected): diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-04-13-18-00-18.gh-issue-117587.52AB27.rst b/Misc/NEWS.d/next/Core and Builtins/2024-04-13-18-00-18.gh-issue-117587.52AB27.rst new file mode 100644 index 00000000000000..14063a72c0e5d8 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-04-13-18-00-18.gh-issue-117587.52AB27.rst @@ -0,0 +1 @@ +Speedup :func:`os.path.abspath` with a native implementation. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index c7a447b455c594..f37d9086807e06 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -1785,39 +1785,6 @@ os__path_isdevdrive(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P #if defined(MS_WINDOWS) -PyDoc_STRVAR(os__getfullpathname__doc__, -"_getfullpathname($module, path, /)\n" -"--\n" -"\n"); - -#define OS__GETFULLPATHNAME_METHODDEF \ - {"_getfullpathname", (PyCFunction)os__getfullpathname, METH_O, os__getfullpathname__doc__}, - -static PyObject * -os__getfullpathname_impl(PyObject *module, path_t *path); - -static PyObject * -os__getfullpathname(PyObject *module, PyObject *arg) -{ - PyObject *return_value = NULL; - path_t path = PATH_T_INITIALIZE_P("_getfullpathname", "path", 0, 0, 0, 0); - - if (!path_converter(arg, &path)) { - goto exit; - } - return_value = os__getfullpathname_impl(module, &path); - -exit: - /* Cleanup for path */ - path_cleanup(&path); - - return return_value; -} - -#endif /* defined(MS_WINDOWS) */ - -#if defined(MS_WINDOWS) - PyDoc_STRVAR(os__getfinalpathname__doc__, "_getfinalpathname($module, path, /)\n" "--\n" @@ -2488,6 +2455,36 @@ os__path_normpath(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyO return return_value; } +PyDoc_STRVAR(os__path_abspath__doc__, +"_path_abspath($module, path, /)\n" +"--\n" +"\n" +"Return an absolute path."); + +#define OS__PATH_ABSPATH_METHODDEF \ + {"_path_abspath", (PyCFunction)os__path_abspath, METH_O, os__path_abspath__doc__}, + +static PyObject * +os__path_abspath_impl(PyObject *module, path_t *path); + +static PyObject * +os__path_abspath(PyObject *module, PyObject *arg) +{ + PyObject *return_value = NULL; + path_t path = PATH_T_INITIALIZE("_path_abspath", "path", 0, 1, 1, 0, 0); + + if (!path_converter(arg, &path)) { + goto exit; + } + return_value = os__path_abspath_impl(module, &path); + +exit: + /* Cleanup for path */ + path_cleanup(&path); + + return return_value; +} + PyDoc_STRVAR(os_mkdir__doc__, "mkdir($module, /, path, mode=511, *, dir_fd=None)\n" "--\n" @@ -12204,10 +12201,6 @@ os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored)) #define OS__PATH_ISDEVDRIVE_METHODDEF #endif /* !defined(OS__PATH_ISDEVDRIVE_METHODDEF) */ -#ifndef OS__GETFULLPATHNAME_METHODDEF - #define OS__GETFULLPATHNAME_METHODDEF -#endif /* !defined(OS__GETFULLPATHNAME_METHODDEF) */ - #ifndef OS__GETFINALPATHNAME_METHODDEF #define OS__GETFINALPATHNAME_METHODDEF #endif /* !defined(OS__GETFINALPATHNAME_METHODDEF) */ @@ -12795,4 +12788,4 @@ os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */ -/*[clinic end generated code: output=300bd1c54dc43765 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5a851e6712142787 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index bb35cfd9cdb138..8abf7491c85c43 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -24,6 +24,7 @@ #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_signal.h" // Py_NSIG #include "pycore_time.h" // _PyLong_FromTime_t() +#include "osdefs.h" // SEP #ifdef HAVE_UNISTD_H # include // symlink() @@ -4890,40 +4891,6 @@ _PyOS_getfullpathname(const wchar_t *path, wchar_t **abspath_p) } -/* A helper function for abspath on win32 */ -/*[clinic input] -os._getfullpathname - - path: path_t - / - -[clinic start generated code]*/ - -static PyObject * -os__getfullpathname_impl(PyObject *module, path_t *path) -/*[clinic end generated code: output=bb8679d56845bc9b input=332ed537c29d0a3e]*/ -{ - wchar_t *abspath; - - if (_PyOS_getfullpathname(path->wide, &abspath) < 0) { - return win32_error_object("GetFullPathNameW", path->object); - } - if (abspath == NULL) { - return PyErr_NoMemory(); - } - - PyObject *str = PyUnicode_FromWideChar(abspath, wcslen(abspath)); - PyMem_RawFree(abspath); - if (str == NULL) { - return NULL; - } - if (PyBytes_Check(path->object)) { - Py_SETREF(str, PyUnicode_EncodeFSDefault(str)); - } - return str; -} - - /*[clinic input] os._getfinalpathname @@ -5540,7 +5507,7 @@ os__path_normpath_impl(PyObject *module, path_t *path) PyObject *result; Py_ssize_t norm_len; wchar_t *norm_path = _Py_normpath_and_size((wchar_t *)path->wide, - path->length, &norm_len); + path->length, 0, &norm_len, 0); if (!norm_len) { result = PyUnicode_FromOrdinal('.'); } @@ -5553,6 +5520,98 @@ os__path_normpath_impl(PyObject *module, path_t *path) return result; } +/*[clinic input] +os._path_abspath + + path: path_t(make_wide=True, nonstrict=True) + / + +Return an absolute path. +[clinic start generated code]*/ + +static PyObject * +os__path_abspath_impl(PyObject *module, path_t *path) +/*[clinic end generated code: output=bb40fbf3be7251a4 input=8870bbcaa7c920a1]*/ +{ + Py_ssize_t abs_len; + wchar_t *abs, *abs_buf = NULL; + + wchar_t *path_buf = (wchar_t *)path->wide; + Py_ssize_t path_len = path->length; + int use_bytes = PyBytes_Check(path->object); +#ifdef MS_WINDOWS + if (wcslen(path_buf) != path_len) { + return PyErr_Format(PyExc_ValueError, + "_path_abspath: embedded null character in path"); + } + // Preserve `.\` for qualified referencing + abs = _Py_normpath_and_size(path_buf, path_len, 0, &abs_len, 1); + if (abs_len == 0 || (abs_len == 1 && abs[0] == L'.')) { + return posix_getcwd(use_bytes); + } + if (_PyOS_getfullpathname(abs, &abs_buf) < 0) { + return win32_error_object("GetFullPathNameW", path->object); + } + abs = abs_buf; + abs_len = wcslen(abs_buf); +#else + if (path_len == 0 || (path_len == 1 && path_buf[0] == L'.')) { + return posix_getcwd(use_bytes); + } + + if (_Py_isabs(path_buf)) { + abs = _Py_normpath_and_size(path_buf, path_len, 0, &abs_len, 0); + } + else { + PyObject *cwd_obj = posix_getcwd(0); + if (!cwd_obj) { + return NULL; + } + Py_ssize_t cwd_len; + wchar_t *cwd_buf = PyUnicode_AsWideCharString(cwd_obj, &cwd_len); + Py_DECREF(cwd_obj); + if (!cwd_buf) { + return NULL; + } + + int add_sep = cwd_buf[cwd_len - 1] != SEP; + Py_ssize_t prefix_len = cwd_len + add_sep; + abs_len = prefix_len + path_len; + + if ((size_t)abs_len + 1 > (size_t)PY_SSIZE_T_MAX / sizeof(wchar_t)) { + PyMem_Free(cwd_buf); + PyErr_SetString(PyExc_OverflowError, "path is too long"); + return NULL; + } + + abs_buf = PyMem_RawMalloc(((size_t)abs_len + 1) * sizeof(wchar_t)); + if (!abs_buf) { + PyMem_Free(cwd_buf); + return PyErr_NoMemory(); + } + + // Join cwd & path + wchar_t *p = memcpy(abs_buf, cwd_buf, cwd_len * sizeof(wchar_t)); + PyMem_Free(cwd_buf); + p += cwd_len; + if (add_sep) { + *p++ = SEP; + } + memcpy(p, path_buf, path_len * sizeof(wchar_t)); + p[path_len] = '\0'; + abs = _Py_normpath_and_size(abs_buf, abs_len, prefix_len, &abs_len, 0); + } +#endif + + PyObject *result = PyUnicode_FromWideChar(abs, abs_len); + PyMem_RawFree(abs_buf); + if (use_bytes) { + Py_SETREF(result, PyUnicode_EncodeFSDefault(result)); + } + + return result; +} + /*[clinic input] os.mkdir @@ -16884,7 +16943,6 @@ static PyMethodDef posix_methods[] = { OS_FPATHCONF_METHODDEF OS_PATHCONF_METHODDEF OS_ABORT_METHODDEF - OS__GETFULLPATHNAME_METHODDEF OS__GETDISKUSAGE_METHODDEF OS__GETFINALPATHNAME_METHODDEF OS__FINDFIRSTFILE_METHODDEF @@ -16892,6 +16950,7 @@ static PyMethodDef posix_methods[] = { OS__PATH_SPLITROOT_METHODDEF OS__PATH_SPLITROOT_EX_METHODDEF OS__PATH_NORMPATH_METHODDEF + OS__PATH_ABSPATH_METHODDEF OS_GETLOADAVG_METHODDEF OS_URANDOM_METHODDEF OS_SETRESUID_METHODDEF diff --git a/Python/fileutils.c b/Python/fileutils.c index e6a5391a3a28b5..4dcaf8cfefc5cb 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -2480,21 +2480,31 @@ _Py_find_basename(const wchar_t *filename) path, which will be within the original buffer. Guaranteed to not make the path longer, and will not fail. 'size' is the length of the path, if known. If -1, the first null character will be assumed - to be the end of the path. 'normsize' will be set to contain the - length of the resulting normalized path. */ + to be the end of the path. 'start' is the position where to start + normalizing. 'normsize' will be set to contain the length of the + resulting normalized path. If 'explicit_curdir' is set, an explicit + curdir will be used for qualified referencing in the cwd. */ wchar_t * -_Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) +_Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t start, + Py_ssize_t *normsize, int explicit_curdir) { assert(path != NULL); + assert(start >= 0); if ((size < 0 && !path[0]) || size == 0) { *normsize = 0; return path; } + if (size >= 0 && start >= size) { + // Don't normalize path + *normsize = size; + return path; + } wchar_t *pEnd = size >= 0 ? &path[size] : NULL; wchar_t *p1 = path; // sequentially scanned address in the path wchar_t *p2 = path; // destination of a scanned character to be ljusted wchar_t *minP2 = path; // the beginning of the destination range wchar_t lastC = L'\0'; // the last ljusted character, p2[-1] in most cases + int explicit = 0; // uses qualified referencing in the cwd #define IS_END(x) (pEnd ? (x) == pEnd : !*(x)) #ifdef ALTSEP @@ -2504,40 +2514,47 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) #endif #define SEP_OR_END(x) (IS_SEP(x) || IS_END(x)) - if (p1[0] == L'.' && IS_SEP(&p1[1])) { - // Skip leading '.\' - path = &path[2]; - while (IS_SEP(path)) { - path++; - } - p1 = p2 = minP2 = path; - lastC = SEP; - } - else { - Py_ssize_t drvsize, rootsize; - _Py_skiproot(path, size, &drvsize, &rootsize); - if (drvsize || rootsize) { - // Skip past root and update minP2 - p1 = &path[drvsize + rootsize]; + Py_ssize_t drvsize, rootsize; + _Py_skiproot(path, size, &drvsize, &rootsize); + if (drvsize || rootsize) { + // Skip past root and update minP2 + p1 = &path[drvsize + rootsize]; #ifndef ALTSEP - p2 = p1; + p2 = p1; #else - for (; p2 < p1; ++p2) { - if (*p2 == ALTSEP) { - *p2 = SEP; - } + for (; p2 < p1; ++p2) { + if (*p2 == ALTSEP) { + *p2 = SEP; } + } #endif - minP2 = p2 - 1; - lastC = *minP2; + minP2 = p2 - 1; + lastC = *minP2; #ifdef MS_WINDOWS - if (lastC != SEP) { - minP2++; - } + if (lastC != SEP) { + minP2++; + } #endif + } + if (p1[0] == L'.' && SEP_OR_END(&p1[1])) { + // Skip leading '.\' + lastC = *++p1; +#ifdef ALTSEP + if (lastC == ALTSEP) { + lastC = SEP; + } +#endif + while (IS_SEP(p1)) { + p1++; } + explicit = 1; } + // Skip past cwd, not allowed if size is unknown + if (size >= 0 && path + start > p1) { + p1 = p2 = path + start; + lastC = *(p1 - 1); + } /* if pEnd is specified, check that. Else, check for null terminator */ for (; !IS_END(p1); ++p1) { wchar_t c = *p1; @@ -2567,9 +2584,11 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) p2 = p3 + 1; } else { p2 = p3; + explicit = 1; } p1 += 1; } else if (sep_at_1) { + explicit = 1; } else { *p2++ = lastC = c; } @@ -2589,6 +2608,31 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) } else { --p2; } + if (explicit_curdir && !rootsize && explicit) { + // Add explicit curdir + if (p2 == minP2 - 1) { + // Set to '.' + p2++; + assert(p2 < p1); + *p2 = L'.'; + } + else if (minP2[0] != L'.' || minP2[1] != L'.' || + !SEP_OR_END(&minP2[2])) + { + // Add leading '.\' + wchar_t *p3 = p2; + p2 += 2; + assert(p2 < p1); + while (p3 != minP2) { + p3[2] = *p3; + p3--; + } + p3[2] = p3[0]; + p3[1] = SEP; + p3[0] = L'.'; + } + p2[1] = L'\0'; + } *normsize = p2 - path + 1; #undef SEP_OR_END #undef IS_SEP @@ -2605,7 +2649,7 @@ wchar_t * _Py_normpath(wchar_t *path, Py_ssize_t size) { Py_ssize_t norm_length; - return _Py_normpath_and_size(path, size, &norm_length); + return _Py_normpath_and_size(path, size, 0, &norm_length, 0); }