Skip to content

Commit ec6c89c

Browse files
[3.13] gh-150880: Normalize paths on Windows before appending wildcard (GH-152906) (GH-152965)
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. (cherry picked from commit 1b4135a) Co-authored-by: Zain Nadeem <zainnadeemzainnadeem80@gmail.com>
1 parent 857acd3 commit ec6c89c

3 files changed

Lines changed: 78 additions & 29 deletions

File tree

Lib/test/test_os.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5118,6 +5118,30 @@ def test_current_directory(self):
51185118
finally:
51195119
os.chdir(old_dir)
51205120

5121+
@unittest.skipIf(sys.platform != 'win32', "Win32 specific test")
5122+
def test_windows_trailing_space_path(self):
5123+
import pathlib
5124+
5125+
filename = self.create_file("file.txt")
5126+
path = self.path + " "
5127+
5128+
self.assertTrue(os.path.exists(path))
5129+
os.stat(path)
5130+
with open(filename + " ", "rb") as file:
5131+
self.assertEqual(file.read(), b"python")
5132+
5133+
self.assertEqual(os.listdir(path), ["file.txt"])
5134+
with os.scandir(path) as entries:
5135+
self.assertEqual([entry.name for entry in entries], ["file.txt"])
5136+
pathlib_entries = list(pathlib.Path(path).iterdir())
5137+
self.assertEqual([entry.name for entry in pathlib_entries], ["file.txt"])
5138+
del pathlib_entries
5139+
5140+
extended_path = "\\\\?\\" + path
5141+
self.assertFalse(os.path.exists(extended_path))
5142+
self.assertRaises(FileNotFoundError, os.listdir, extended_path)
5143+
self.assertRaises(FileNotFoundError, os.scandir, extended_path)
5144+
51215145
def test_repr(self):
51225146
entry = self.create_file_entry()
51235147
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
@@ -4350,42 +4350,34 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
43504350

43514351

43524352
#if defined(MS_WINDOWS) && !defined(HAVE_OPENDIR)
4353+
static wchar_t *
4354+
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename,
4355+
int normalize);
4356+
43534357
static PyObject *
43544358
_listdir_windows_no_opendir(path_t *path, PyObject *list)
43554359
{
43564360
PyObject *v;
43574361
HANDLE hFindFile = INVALID_HANDLE_VALUE;
43584362
BOOL result, return_bytes;
4359-
wchar_t namebuf[MAX_PATH+4]; /* Overallocate for "\*.*" */
4360-
/* only claim to have space for MAX_PATH */
4361-
Py_ssize_t len = Py_ARRAY_LENGTH(namebuf)-4;
43624363
wchar_t *wnamebuf = NULL;
43634364

43644365
WIN32_FIND_DATAW wFileData;
43654366
const wchar_t *po_wchars;
43664367

43674368
if (!path->wide) { /* Default arg: "." */
43684369
po_wchars = L".";
4369-
len = 1;
43704370
return_bytes = 0;
43714371
} else {
43724372
po_wchars = path->wide;
4373-
len = wcslen(path->wide);
43744373
return_bytes = PyBytes_Check(path->object);
43754374
}
4376-
/* The +5 is so we can append "\\*.*\0" */
4377-
wnamebuf = PyMem_New(wchar_t, len + 5);
4378-
if (!wnamebuf) {
4379-
PyErr_NoMemory();
4375+
4376+
wnamebuf = join_path_filenameW(po_wchars, L"*.*", 1);
4377+
if (wnamebuf == NULL) {
43804378
goto exit;
43814379
}
4382-
wcscpy(wnamebuf, po_wchars);
4383-
if (len > 0) {
4384-
wchar_t wch = wnamebuf[len-1];
4385-
if (wch != SEP && wch != ALTSEP && wch != L':')
4386-
wnamebuf[len++] = SEP;
4387-
wcscpy(wnamebuf + len, L"*.*");
4388-
}
4380+
43894381
if ((list = PyList_New(0)) == NULL) {
43904382
goto exit;
43914383
}
@@ -15933,13 +15925,19 @@ static PyType_Spec DirEntryType_spec = {
1593315925

1593415926
#ifdef MS_WINDOWS
1593515927

15928+
static int
15929+
is_extended_path(const wchar_t *path)
15930+
{
15931+
return wcsncmp(path, L"\\\\?\\", 4) == 0;
15932+
}
15933+
1593615934
static wchar_t *
15937-
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename)
15935+
join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename,
15936+
int normalize)
1593815937
{
1593915938
Py_ssize_t path_len;
15940-
Py_ssize_t size;
1594115939
wchar_t *result;
15942-
wchar_t ch;
15940+
wchar_t *path_allocated = NULL;
1594315941

1594415942
if (!path_wide) { /* Default arg: "." */
1594515943
path_wide = L".";
@@ -15949,20 +15947,44 @@ join_path_filenameW(const wchar_t *path_wide, const wchar_t *filename)
1594915947
path_len = wcslen(path_wide);
1595015948
}
1595115949

15952-
/* The +1's are for the path separator and the NUL */
15953-
size = path_len + 1 + wcslen(filename) + 1;
15950+
if (path_len == 0) {
15951+
result = PyMem_New(wchar_t, 1);
15952+
if (result == NULL) {
15953+
PyErr_NoMemory();
15954+
return NULL;
15955+
}
15956+
result[0] = L'\0';
15957+
return result;
15958+
}
15959+
15960+
if (normalize && !is_extended_path(path_wide)) {
15961+
int err = _PyOS_getfullpathname(path_wide, &path_allocated);
15962+
if (err < 0) {
15963+
PyErr_SetFromWindowsErr(0);
15964+
return NULL;
15965+
}
15966+
if (path_allocated == NULL) {
15967+
PyErr_NoMemory();
15968+
return NULL;
15969+
}
15970+
path_wide = path_allocated;
15971+
path_len = wcslen(path_wide);
15972+
}
15973+
15974+
size_t size = (size_t)path_len + 1 + wcslen(filename) + 1;
1595415975
result = PyMem_New(wchar_t, size);
15955-
if (!result) {
15976+
if (result == NULL) {
15977+
PyMem_RawFree(path_allocated);
1595615978
PyErr_NoMemory();
1595715979
return NULL;
1595815980
}
1595915981
wcscpy(result, path_wide);
15960-
if (path_len > 0) {
15961-
ch = result[path_len - 1];
15962-
if (ch != SEP && ch != ALTSEP && ch != L':')
15963-
result[path_len++] = SEP;
15964-
wcscpy(result + path_len, filename);
15982+
wchar_t ch = result[path_len - 1];
15983+
if (ch != SEP && ch != ALTSEP && ch != L':') {
15984+
result[path_len++] = SEP;
1596515985
}
15986+
wcscpy(result + path_len, filename);
15987+
PyMem_RawFree(path_allocated);
1596615988
return result;
1596715989
}
1596815990

@@ -15994,7 +16016,7 @@ DirEntry_from_find_data(PyObject *module, path_t *path, WIN32_FIND_DATAW *dataW)
1599416016
goto error;
1599516017
}
1599616018

15997-
joined_path = join_path_filenameW(path->wide, dataW->cFileName);
16019+
joined_path = join_path_filenameW(path->wide, dataW->cFileName, 0);
1599816020
if (!joined_path)
1599916021
goto error;
1600016022

@@ -16418,7 +16440,7 @@ os_scandir_impl(PyObject *module, path_t *path)
1641816440
#ifdef MS_WINDOWS
1641916441
iterator->first_time = 1;
1642016442

16421-
path_strW = join_path_filenameW(iterator->path.wide, L"*.*");
16443+
path_strW = join_path_filenameW(iterator->path.wide, L"*.*", 1);
1642216444
if (!path_strW)
1642316445
goto error;
1642416446

0 commit comments

Comments
 (0)