diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 9facf47b4166597..88b215be49c5caa 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -5118,6 +5118,30 @@ def test_current_directory(self): finally: os.chdir(old_dir) + @unittest.skipIf(sys.platform != 'win32', "Win32 specific test") + def test_windows_trailing_space_path(self): + import pathlib + + filename = self.create_file("file.txt") + path = self.path + " " + + self.assertTrue(os.path.exists(path)) + os.stat(path) + with open(filename + " ", "rb") as file: + self.assertEqual(file.read(), b"python") + + self.assertEqual(os.listdir(path), ["file.txt"]) + with os.scandir(path) as entries: + self.assertEqual([entry.name for entry in entries], ["file.txt"]) + pathlib_entries = list(pathlib.Path(path).iterdir()) + self.assertEqual([entry.name for entry in pathlib_entries], ["file.txt"]) + del pathlib_entries + + extended_path = "\\\\?\\" + path + self.assertFalse(os.path.exists(extended_path)) + self.assertRaises(FileNotFoundError, os.listdir, extended_path) + self.assertRaises(FileNotFoundError, os.scandir, extended_path) + def test_repr(self): entry = self.create_file_entry() self.assertEqual(repr(entry), "") diff --git a/Misc/NEWS.d/next/Library/2026-07-02-22-19-53.gh-issue-150880.vPZ7jK.rst b/Misc/NEWS.d/next/Library/2026-07-02-22-19-53.gh-issue-150880.vPZ7jK.rst new file mode 100644 index 000000000000000..a7bf210f8e1ca9c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-02-22-19-53.gh-issue-150880.vPZ7jK.rst @@ -0,0 +1,3 @@ +Normalize non-extended Windows paths before appending the wildcard used by +``os.listdir()`` and ``os.scandir()``, making paths with trailing spaces +behave consistently with other filesystem APIs. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 54718f07379caf8..be14e81898e1f15 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4350,15 +4350,16 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #if defined(MS_WINDOWS) && !defined(HAVE_OPENDIR) +static wchar_t * +join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename, + int normalize); + static PyObject * _listdir_windows_no_opendir(path_t *path, PyObject *list) { PyObject *v; HANDLE hFindFile = INVALID_HANDLE_VALUE; BOOL result, return_bytes; - wchar_t namebuf[MAX_PATH+4]; /* Overallocate for "\*.*" */ - /* only claim to have space for MAX_PATH */ - Py_ssize_t len = Py_ARRAY_LENGTH(namebuf)-4; wchar_t *wnamebuf = NULL; WIN32_FIND_DATAW wFileData; @@ -4366,26 +4367,17 @@ _listdir_windows_no_opendir(path_t *path, PyObject *list) if (!path->wide) { /* Default arg: "." */ po_wchars = L"."; - len = 1; return_bytes = 0; } else { po_wchars = path->wide; - len = wcslen(path->wide); return_bytes = PyBytes_Check(path->object); } - /* The +5 is so we can append "\\*.*\0" */ - wnamebuf = PyMem_New(wchar_t, len + 5); - if (!wnamebuf) { - PyErr_NoMemory(); + + wnamebuf = join_path_filenameW(po_wchars, L"*.*", 1); + if (wnamebuf == NULL) { goto exit; } - wcscpy(wnamebuf, po_wchars); - if (len > 0) { - wchar_t wch = wnamebuf[len-1]; - if (wch != SEP && wch != ALTSEP && wch != L':') - wnamebuf[len++] = SEP; - wcscpy(wnamebuf + len, L"*.*"); - } + if ((list = PyList_New(0)) == NULL) { goto exit; } @@ -15933,13 +15925,19 @@ static PyType_Spec DirEntryType_spec = { #ifdef MS_WINDOWS +static int +is_extended_path(const wchar_t *path) +{ + return wcsncmp(path, L"\\\\?\\", 4) == 0; +} + static wchar_t * -join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename) +join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename, + int normalize) { Py_ssize_t path_len; - Py_ssize_t size; wchar_t *result; - wchar_t ch; + wchar_t *path_allocated = NULL; if (!path_wide) { /* Default arg: "." */ path_wide = L"."; @@ -15949,20 +15947,44 @@ join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename) path_len = wcslen(path_wide); } - /* The +1's are for the path separator and the NUL */ - size = path_len + 1 + wcslen(filename) + 1; + if (path_len == 0) { + result = PyMem_New(wchar_t, 1); + if (result == NULL) { + PyErr_NoMemory(); + return NULL; + } + result[0] = L'\0'; + return result; + } + + if (normalize && !is_extended_path(path_wide)) { + int err = _PyOS_getfullpathname(path_wide, &path_allocated); + if (err < 0) { + PyErr_SetFromWindowsErr(0); + return NULL; + } + if (path_allocated == NULL) { + PyErr_NoMemory(); + return NULL; + } + path_wide = path_allocated; + path_len = wcslen(path_wide); + } + + size_t size = (size_t)path_len + 1 + wcslen(filename) + 1; result = PyMem_New(wchar_t, size); - if (!result) { + if (result == NULL) { + PyMem_RawFree(path_allocated); PyErr_NoMemory(); return NULL; } wcscpy(result, path_wide); - if (path_len > 0) { - ch = result[path_len - 1]; - if (ch != SEP && ch != ALTSEP && ch != L':') - result[path_len++] = SEP; - wcscpy(result + path_len, filename); + wchar_t ch = result[path_len - 1]; + if (ch != SEP && ch != ALTSEP && ch != L':') { + result[path_len++] = SEP; } + wcscpy(result + path_len, filename); + PyMem_RawFree(path_allocated); return result; } @@ -15994,7 +16016,7 @@ DirEntry_from_find_data(PyObject *module, path_t *path, WIN32_FIND_DATAW *dataW) goto error; } - joined_path = join_path_filenameW(path->wide, dataW->cFileName); + joined_path = join_path_filenameW(path->wide, dataW->cFileName, 0); if (!joined_path) goto error; @@ -16418,7 +16440,7 @@ os_scandir_impl(PyObject *module, path_t *path) #ifdef MS_WINDOWS iterator->first_time = 1; - path_strW = join_path_filenameW(iterator->path.wide, L"*.*"); + path_strW = join_path_filenameW(iterator->path.wide, L"*.*", 1); if (!path_strW) goto error;