https://github.com/python/cpython/commit/1b4135a2c69f5320dbdcb37dcc295a5fd36580b5
commit: 1b4135a2c69f5320dbdcb37dcc295a5fd36580b5
branch: main
author: Zain Nadeem <[email protected]>
committer: zooba <[email protected]>
date: 2026-07-03T16:30:02+01:00
summary:
gh-150880: Normalize paths on Windows before appending wildcard (GH-152906)
This ensures that we don't turn a "valid" path with trailing spaces into an
invalid path with embedded spaces.
files:
A Misc/NEWS.d/next/Library/2026-07-02-22-19-53.gh-issue-150880.vPZ7jK.rst
M Lib/test/test_os/test_os.py
M Modules/posixmodule.c
diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py
index 97b2475f20a2fbe..5c258ff89ba735c 100644
--- a/Lib/test/test_os/test_os.py
+++ b/Lib/test/test_os/test_os.py
@@ -5058,6 +5058,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), "<DirEntry 'file.txt'>")
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 e455f532a5bbf41..57db175336702e2 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -4905,15 +4905,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;
@@ -4921,26 +4922,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;
}
@@ -16608,13 +16600,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".";
@@ -16624,20 +16622,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;
}
@@ -16669,7 +16691,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;
@@ -17105,7 +17127,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;
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]