diff --git a/Include/internal/pycore_fileutils.h b/Include/internal/pycore_fileutils.h index 13f86b01bbfe8f..93c4118e5f7c20 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 *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/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e4f0138e17edfa..891086c1b5e7b4 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -924,6 +924,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exception)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(existing_file_name)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exp)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(explicit_curdir)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extend)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extra_tokens)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(facility)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index e70f11e2a26cd5..9cb3a110c8169a 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -413,6 +413,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(exception) STRUCT_FOR_ID(existing_file_name) STRUCT_FOR_ID(exp) + STRUCT_FOR_ID(explicit_curdir) STRUCT_FOR_ID(extend) STRUCT_FOR_ID(extra_tokens) STRUCT_FOR_ID(facility) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 5d404c8fd91ca6..3cedd4cf5a44a3 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -922,6 +922,7 @@ extern "C" { INIT_ID(exception), \ INIT_ID(existing_file_name), \ INIT_ID(exp), \ + INIT_ID(explicit_curdir), \ INIT_ID(extend), \ INIT_ID(extra_tokens), \ INIT_ID(facility), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index d0bc8d7186c053..3e8ea073593e95 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1448,6 +1448,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(explicit_curdir); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(extend); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 5481bb8888ef59..4c2348be1b1893 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -555,6 +555,7 @@ def normpath(path): # Return an absolute path. try: + from nt import _path_normpath_ex as _normpath from nt import _getfullpathname except ImportError: # not running on Windows - mock up something sensible @@ -573,7 +574,7 @@ def abspath(path): def abspath(path): """Return the absolute version of a path.""" try: - return _getfullpathname(normpath(path)) + return _getfullpathname(_normpath(path, explicit_curdir=True)) except (OSError, ValueError): # See gh-75230, handle outside for cleaner traceback pass diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 4f59184dfcfdc7..0c65a8ee0836e7 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -839,6 +839,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/Misc/NEWS.d/next/Library/2024-05-31-12-44-38.gh-issue-126782.Atm9ol.rst b/Misc/NEWS.d/next/Library/2024-05-31-12-44-38.gh-issue-126782.Atm9ol.rst new file mode 100644 index 00000000000000..e57fdcfc3b7aa8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-05-31-12-44-38.gh-issue-126782.Atm9ol.rst @@ -0,0 +1 @@ +Support qualified referencing for :func:`os.path.abspath` on Windows. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index dce0ea100ec435..e972e9304b5f54 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -2517,6 +2517,78 @@ os__path_splitroot_ex(PyObject *module, PyObject *const *args, Py_ssize_t nargs, return return_value; } +PyDoc_STRVAR(os__path_normpath_ex__doc__, +"_path_normpath_ex($module, /, path, *, explicit_curdir=False)\n" +"--\n" +"\n" +"Normalize path, eliminating double slashes, etc."); + +#define OS__PATH_NORMPATH_EX_METHODDEF \ + {"_path_normpath_ex", _PyCFunction_CAST(os__path_normpath_ex), METH_FASTCALL|METH_KEYWORDS, os__path_normpath_ex__doc__}, + +static PyObject * +os__path_normpath_ex_impl(PyObject *module, path_t *path, + int explicit_curdir); + +static PyObject * +os__path_normpath_ex(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(path), &_Py_ID(explicit_curdir), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"path", "explicit_curdir", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_path_normpath_ex", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + path_t path = PATH_T_INITIALIZE("_path_normpath_ex", "path", 0, 1, 1, 0, 0); + int explicit_curdir = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!path_converter(args[0], &path)) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + explicit_curdir = PyObject_IsTrue(args[1]); + if (explicit_curdir < 0) { + goto exit; + } +skip_optional_kwonly: + return_value = os__path_normpath_ex_impl(module, &path, explicit_curdir); + +exit: + /* Cleanup for path */ + path_cleanup(&path); + + return return_value; +} + PyDoc_STRVAR(os__path_normpath__doc__, "_path_normpath($module, /, path)\n" "--\n" @@ -13114,4 +13186,4 @@ os__create_environ(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=5358a13b4ce6148b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=455d5808247079f4 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index da7399de86f213..a7b7432ba32f4d 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -5594,21 +5594,25 @@ os__path_splitroot_ex_impl(PyObject *module, path_t *path) /*[clinic input] -os._path_normpath +os._path_normpath_ex path: path_t(make_wide=True, nonstrict=True) + * + explicit_curdir: bool = False Normalize path, eliminating double slashes, etc. [clinic start generated code]*/ static PyObject * -os__path_normpath_impl(PyObject *module, path_t *path) -/*[clinic end generated code: output=d353e7ed9410c044 input=3d4ac23b06332dcb]*/ +os__path_normpath_ex_impl(PyObject *module, path_t *path, + int explicit_curdir) +/*[clinic end generated code: output=4c4c3bf33a70fe57 input=90fe0dfc4b3a751b]*/ { 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, &norm_len, + explicit_curdir); if (!norm_len) { result = PyUnicode_FromOrdinal('.'); } @@ -5621,6 +5625,23 @@ os__path_normpath_impl(PyObject *module, path_t *path) return result; } + +/*[clinic input] +os._path_normpath + + path: path_t(make_wide=True, nonstrict=True) + +Normalize path, eliminating double slashes, etc. +[clinic start generated code]*/ + +static PyObject * +os__path_normpath_impl(PyObject *module, path_t *path) +/*[clinic end generated code: output=d353e7ed9410c044 input=3d4ac23b06332dcb]*/ +{ + + return os__path_normpath_ex_impl(module, path, 0); +} + /*[clinic input] os.mkdir @@ -17059,6 +17080,7 @@ static PyMethodDef posix_methods[] = { OS__GETVOLUMEPATHNAME_METHODDEF OS__PATH_SPLITROOT_METHODDEF OS__PATH_SPLITROOT_EX_METHODDEF + OS__PATH_NORMPATH_EX_METHODDEF OS__PATH_NORMPATH_METHODDEF OS_GETLOADAVG_METHODDEF OS_URANDOM_METHODDEF diff --git a/Python/fileutils.c b/Python/fileutils.c index c9ae1b3f54e167..4403adcf9fa3e2 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -2483,9 +2483,12 @@ _Py_find_basename(const wchar_t *filename) 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. */ + 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 *normsize, + int explicit_curdir) { assert(path != NULL); if ((size < 0 && !path[0]) || size == 0) { @@ -2497,6 +2500,7 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) 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 @@ -2506,38 +2510,36 @@ _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)) + Py_ssize_t drvsize, rootsize; + _Py_skiproot(path, size, &drvsize, &rootsize); if (p1[0] == L'.' && IS_SEP(&p1[1])) { // Skip leading '.\' - path = &path[2]; - while (IS_SEP(path)) { - path++; + p1 = &path[2]; + while (IS_SEP(p1)) { + p1++; } - p1 = p2 = minP2 = path; lastC = SEP; + explicit = 1; } - 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]; + else 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++; - } -#endif + if (lastC != SEP) { + minP2++; } +#endif } /* if pEnd is specified, check that. Else, check for null terminator */ @@ -2569,9 +2571,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; } @@ -2591,6 +2595,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 @@ -2607,7 +2636,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, &norm_length, 0); }