Skip to content

Commit 5a9aaeb

Browse files
committed
gh-150880: Normalize Windows scandir wildcard paths
1 parent 369ce43 commit 5a9aaeb

3 files changed

Lines changed: 78 additions & 29 deletions

File tree

Lib/test/test_os/test_os.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5058,6 +5058,30 @@ def test_current_directory(self):
50585058
finally:
50595059
os.chdir(old_dir)
50605060

5061+
@unittest.skipIf(sys.platform != 'win32', "Win32 specific test")
5062+
def test_windows_trailing_space_path(self):
5063+
import pathlib
5064+
5065+
filename = self.create_file("file.txt")
5066+
path = self.path + " "
5067+
5068+
self.assertTrue(os.path.exists(path))
5069+
os.stat(path)
5070+
with open(filename + " ", "rb") as file:
5071+
self.assertEqual(file.read(), b"python")
5072+
5073+
self.assertEqual(os.listdir(path), ["file.txt"])
5074+
with os.scandir(path) as entries:
5075+
self.assertEqual([entry.name for entry in entries], ["file.txt"])
5076+
pathlib_entries = list(pathlib.Path(path).iterdir())
5077+
self.assertEqual([entry.name for entry in pathlib_entries], ["file.txt"])
5078+
del pathlib_entries
5079+
5080+
extended_path = "\\\\?\\" + path
5081+
self.assertFalse(os.path.exists(extended_path))
5082+
self.assertRaises(FileNotFoundError, os.listdir, extended_path)
5083+
self.assertRaises(FileNotFoundError, os.scandir, extended_path)
5084+
50615085
def test_repr(self):
50625086
entry = self.create_file_entry()
50635087
self.assertEqual(repr(entry), "<DirEntry 'file.txt'>")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Normalize non-extended Windows paths before appending the wildcard used by
2+
``os.listdir()`` and ``os.scandir()``, making paths with trailing spaces
3+
behave consistently with other filesystem APIs.

Modules/posixmodule.c

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4905,42 +4905,34 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
49054905

49064906

49074907
#if defined(MS_WINDOWS) && !defined(HAVE_OPENDIR)
4908+
static wchar_t *
4909+
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename,
4910+
int normalize);
4911+
49084912
static PyObject *
49094913
_listdir_windows_no_opendir(path_t *path, PyObject *list)
49104914
{
49114915
PyObject *v;
49124916
HANDLE hFindFile = INVALID_HANDLE_VALUE;
49134917
BOOL result, return_bytes;
4914-
wchar_t namebuf[MAX_PATH+4]; /* Overallocate for "\*.*" */
4915-
/* only claim to have space for MAX_PATH */
4916-
Py_ssize_t len = Py_ARRAY_LENGTH(namebuf)-4;
49174918
wchar_t *wnamebuf = NULL;
49184919

49194920
WIN32_FIND_DATAW wFileData;
49204921
const wchar_t *po_wchars;
49214922

49224923
if (!path->wide) { /* Default arg: "." */
49234924
po_wchars = L".";
4924-
len = 1;
49254925
return_bytes = 0;
49264926
} else {
49274927
po_wchars = path->wide;
4928-
len = wcslen(path->wide);
49294928
return_bytes = PyBytes_Check(path->object);
49304929
}
4931-
/* The +5 is so we can append "\\*.*\0" */
4932-
wnamebuf = PyMem_New(wchar_t, len + 5);
4933-
if (!wnamebuf) {
4934-
PyErr_NoMemory();
4930+
4931+
wnamebuf = join_path_filenameW(po_wchars, L"*.*", 1);
4932+
if (wnamebuf == NULL) {
49354933
goto exit;
49364934
}
4937-
wcscpy(wnamebuf, po_wchars);
4938-
if (len > 0) {
4939-
wchar_t wch = wnamebuf[len-1];
4940-
if (wch != SEP && wch != ALTSEP && wch != L':')
4941-
wnamebuf[len++] = SEP;
4942-
wcscpy(wnamebuf + len, L"*.*");
4943-
}
4935+
49444936
if ((list = PyList_New(0)) == NULL) {
49454937
goto exit;
49464938
}
@@ -16608,13 +16600,19 @@ static PyType_Spec DirEntryType_spec = {
1660816600

1660916601
#ifdef MS_WINDOWS
1661016602

16603+
static int
16604+
is_extended_path(const wchar_t *path)
16605+
{
16606+
return wcsncmp(path, L"\\\\?\\", 4) == 0;
16607+
}
16608+
1661116609
static wchar_t *
16612-
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename)
16610+
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename,
16611+
int normalize)
1661316612
{
1661416613
Py_ssize_t path_len;
16615-
Py_ssize_t size;
1661616614
wchar_t *result;
16617-
wchar_t ch;
16615+
wchar_t *path_allocated = NULL;
1661816616

1661916617
if (!path_wide) { /* Default arg: "." */
1662016618
path_wide = L".";
@@ -16624,20 +16622,44 @@ join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename)
1662416622
path_len = wcslen(path_wide);
1662516623
}
1662616624

16627-
/* The +1's are for the path separator and the NUL */
16628-
size = path_len + 1 + wcslen(filename) + 1;
16625+
if (path_len == 0) {
16626+
result = PyMem_New(wchar_t, 1);
16627+
if (result == NULL) {
16628+
PyErr_NoMemory();
16629+
return NULL;
16630+
}
16631+
result[0] = L'\0';
16632+
return result;
16633+
}
16634+
16635+
if (normalize && !is_extended_path(path_wide)) {
16636+
int err = _PyOS_getfullpathname(path_wide, &path_allocated);
16637+
if (err < 0) {
16638+
PyErr_SetFromWindowsErr(0);
16639+
return NULL;
16640+
}
16641+
if (path_allocated == NULL) {
16642+
PyErr_NoMemory();
16643+
return NULL;
16644+
}
16645+
path_wide = path_allocated;
16646+
path_len = wcslen(path_wide);
16647+
}
16648+
16649+
size_t size = (size_t)path_len + 1 + wcslen(filename) + 1;
1662916650
result = PyMem_New(wchar_t, size);
16630-
if (!result) {
16651+
if (result == NULL) {
16652+
PyMem_RawFree(path_allocated);
1663116653
PyErr_NoMemory();
1663216654
return NULL;
1663316655
}
1663416656
wcscpy(result, path_wide);
16635-
if (path_len > 0) {
16636-
ch = result[path_len - 1];
16637-
if (ch != SEP && ch != ALTSEP && ch != L':')
16638-
result[path_len++] = SEP;
16639-
wcscpy(result + path_len, filename);
16657+
wchar_t ch = result[path_len - 1];
16658+
if (ch != SEP && ch != ALTSEP && ch != L':') {
16659+
result[path_len++] = SEP;
1664016660
}
16661+
wcscpy(result + path_len, filename);
16662+
PyMem_RawFree(path_allocated);
1664116663
return result;
1664216664
}
1664316665

@@ -16669,7 +16691,7 @@ DirEntry_from_find_data(PyObject *module, path_t *path, WIN32_FIND_DATAW *dataW)
1666916691
goto error;
1667016692
}
1667116693

16672-
joined_path = join_path_filenameW(path->wide, dataW->cFileName);
16694+
joined_path = join_path_filenameW(path->wide, dataW->cFileName, 0);
1667316695
if (!joined_path)
1667416696
goto error;
1667516697

@@ -17105,7 +17127,7 @@ os_scandir_impl(PyObject *module, path_t *path)
1710517127
#ifdef MS_WINDOWS
1710617128
iterator->first_time = 1;
1710717129

17108-
path_strW = join_path_filenameW(iterator->path.wide, L"*.*");
17130+
path_strW = join_path_filenameW(iterator->path.wide, L"*.*", 1);
1710917131
if (!path_strW)
1711017132
goto error;
1711117133

0 commit comments

Comments
 (0)