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 c12e242d560bde..b5d45a7b883b10 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 dfd9f2b799ec8e..5b44b8ceb1ccdc 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 b631382cae058a..becb57f504cf34 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 24cec3a4fded7a..bf2fb2cbff8f20 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 6715071af8c752..05820b57477638 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -844,6 +844,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 cd0c4faeac83d1..8fdb5b3239f505 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=7ee14f5e880092f5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b20f794f9f34bc08 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 6eb7054b566e3f..4cd7660905fd38 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -5598,21 +5598,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('.'); } @@ -5625,6 +5629,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 @@ -17006,6 +17027,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 9529b14d377c60..41e5b838e8c107 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 @@ -2539,6 +2543,7 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) while (IS_SEP(p1)) { p1++; } + explicit = 1; } /* if pEnd is specified, check that. Else, check for null terminator */ @@ -2554,6 +2559,7 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) int sep_at_1 = SEP_OR_END(&p1[1]); int sep_at_2 = !sep_at_1 && SEP_OR_END(&p1[2]); if (sep_at_2 && p1[1] == L'.') { + // Parent directory wchar_t *p3 = p2; while (p3 != minP2 && *--p3 == SEP) { } while (p3 != minP2 && *(p3 - 1) != SEP) { --p3; } @@ -2569,10 +2575,16 @@ _Py_normpath_and_size(wchar_t *path, Py_ssize_t size, Py_ssize_t *normsize) // Absolute path, so absorb segment p2 = p3 + 1; } else { + // Alternative path for foo: foo/bar/.. + // Alternative path for bar: foo/../bar p2 = p3; + explicit = 1; } p1 += 1; } else if (sep_at_1) { + // Current directory + // Alternative path for foo: foo/. + explicit = 1; } else { *p2++ = lastC = c; } @@ -2592,6 +2604,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 @@ -2608,7 +2645,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); }