Skip to content

Commit 9041b00

Browse files
zoobaaisk
andauthored
bpo-42658: Use LCMapStringEx in ntpath.normcase to match OS behaviour for case-folding (GH-93674)
Co-authored-by: AN Long <aisk@users.noreply.github.com>
1 parent a2695be commit 9041b00

File tree

5 files changed

+157
-9
lines changed

5 files changed

+157
-9
lines changed

Lib/ntpath.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import genericpath
2424
from genericpath import *
2525

26+
2627
__all__ = ["normcase","isabs","join","splitdrive","split","splitext",
2728
"basename","dirname","commonprefix","getsize","getmtime",
2829
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
@@ -41,14 +42,39 @@ def _get_bothseps(path):
4142
# Other normalizations (such as optimizing '../' away) are not done
4243
# (this is done by normpath).
4344

44-
def normcase(s):
45-
"""Normalize case of pathname.
46-
47-
Makes all characters lowercase and all slashes into backslashes."""
48-
s = os.fspath(s)
49-
if isinstance(s, bytes):
50-
return s.replace(b'/', b'\\').lower()
51-
else:
45+
try:
46+
from _winapi import (
47+
LCMapStringEx as _LCMapStringEx,
48+
LOCALE_NAME_INVARIANT as _LOCALE_NAME_INVARIANT,
49+
LCMAP_LOWERCASE as _LCMAP_LOWERCASE)
50+
51+
def normcase(s):
52+
"""Normalize case of pathname.
53+
54+
Makes all characters lowercase and all slashes into backslashes.
55+
"""
56+
s = os.fspath(s)
57+
if not s:
58+
return s
59+
if isinstance(s, bytes):
60+
encoding = sys.getfilesystemencoding()
61+
s = s.decode(encoding, 'surrogateescape').replace('/', '\\')
62+
s = _LCMapStringEx(_LOCALE_NAME_INVARIANT,
63+
_LCMAP_LOWERCASE, s)
64+
return s.encode(encoding, 'surrogateescape')
65+
else:
66+
return _LCMapStringEx(_LOCALE_NAME_INVARIANT,
67+
_LCMAP_LOWERCASE,
68+
s.replace('/', '\\'))
69+
except ImportError:
70+
def normcase(s):
71+
"""Normalize case of pathname.
72+
73+
Makes all characters lowercase and all slashes into backslashes.
74+
"""
75+
s = os.fspath(s)
76+
if isinstance(s, bytes):
77+
return os.fsencode(os.fsdecode(s).replace('/', '\\').lower())
5278
return s.replace('/', '\\').lower()
5379

5480

Lib/test/test_ntpath.py

+2
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,8 @@ def _check_function(self, func):
807807

808808
def test_path_normcase(self):
809809
self._check_function(self.path.normcase)
810+
if sys.platform == 'win32':
811+
self.assertEqual(ntpath.normcase('\u03a9\u2126'), 'ωΩ')
810812

811813
def test_path_isabs(self):
812814
self._check_function(self.path.isabs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Support native Windows case-insensitive path comparisons by using
2+
``LCMapStringEx`` instead of :func:`str.lower` in :func:`ntpath.normcase`.
3+
Add ``LCMapStringEx`` to the :mod:`_winapi` module.

Modules/_winapi.c

+85
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,74 @@ _winapi_PeekNamedPipe_impl(PyObject *module, HANDLE handle, int size)
15121512
}
15131513
}
15141514

1515+
/*[clinic input]
1516+
_winapi.LCMapStringEx
1517+
1518+
locale: unicode
1519+
flags: DWORD
1520+
src: unicode
1521+
1522+
[clinic start generated code]*/
1523+
1524+
static PyObject *
1525+
_winapi_LCMapStringEx_impl(PyObject *module, PyObject *locale, DWORD flags,
1526+
PyObject *src)
1527+
/*[clinic end generated code: output=8ea4c9d85a4a1f23 input=2fa6ebc92591731b]*/
1528+
{
1529+
if (flags & (LCMAP_SORTHANDLE | LCMAP_HASH | LCMAP_BYTEREV |
1530+
LCMAP_SORTKEY)) {
1531+
return PyErr_Format(PyExc_ValueError, "unsupported flags");
1532+
}
1533+
1534+
wchar_t *locale_ = PyUnicode_AsWideCharString(locale, NULL);
1535+
if (!locale_) {
1536+
return NULL;
1537+
}
1538+
Py_ssize_t srcLenAsSsize;
1539+
int srcLen;
1540+
wchar_t *src_ = PyUnicode_AsWideCharString(src, &srcLenAsSsize);
1541+
if (!src_) {
1542+
PyMem_Free(locale_);
1543+
return NULL;
1544+
}
1545+
srcLen = (int)srcLenAsSsize;
1546+
if (srcLen != srcLenAsSsize) {
1547+
srcLen = -1;
1548+
}
1549+
1550+
int dest_size = LCMapStringEx(locale_, flags, src_, srcLen, NULL, 0,
1551+
NULL, NULL, 0);
1552+
if (dest_size == 0) {
1553+
PyMem_Free(locale_);
1554+
PyMem_Free(src_);
1555+
return PyErr_SetFromWindowsErr(0);
1556+
}
1557+
1558+
wchar_t* dest = PyMem_NEW(wchar_t, dest_size);
1559+
if (dest == NULL) {
1560+
PyMem_Free(locale_);
1561+
PyMem_Free(src_);
1562+
return PyErr_NoMemory();
1563+
}
1564+
1565+
int nmapped = LCMapStringEx(locale_, flags, src_, srcLen, dest, dest_size,
1566+
NULL, NULL, 0);
1567+
if (nmapped == 0) {
1568+
DWORD error = GetLastError();
1569+
PyMem_Free(locale_);
1570+
PyMem_Free(src_);
1571+
PyMem_DEL(dest);
1572+
return PyErr_SetFromWindowsErr(error);
1573+
}
1574+
1575+
PyObject *ret = PyUnicode_FromWideChar(dest, dest_size);
1576+
PyMem_Free(locale_);
1577+
PyMem_Free(src_);
1578+
PyMem_DEL(dest);
1579+
1580+
return ret;
1581+
}
1582+
15151583
/*[clinic input]
15161584
_winapi.ReadFile
15171585
@@ -2023,6 +2091,7 @@ static PyMethodDef winapi_functions[] = {
20232091
_WINAPI_OPENFILEMAPPING_METHODDEF
20242092
_WINAPI_OPENPROCESS_METHODDEF
20252093
_WINAPI_PEEKNAMEDPIPE_METHODDEF
2094+
_WINAPI_LCMAPSTRINGEX_METHODDEF
20262095
_WINAPI_READFILE_METHODDEF
20272096
_WINAPI_SETNAMEDPIPEHANDLESTATE_METHODDEF
20282097
_WINAPI_TERMINATEPROCESS_METHODDEF
@@ -2160,6 +2229,22 @@ static int winapi_exec(PyObject *m)
21602229
WINAPI_CONSTANT(F_DWORD, FILE_TYPE_PIPE);
21612230
WINAPI_CONSTANT(F_DWORD, FILE_TYPE_REMOTE);
21622231

2232+
WINAPI_CONSTANT("u", LOCALE_NAME_INVARIANT);
2233+
WINAPI_CONSTANT(F_DWORD, LOCALE_NAME_MAX_LENGTH);
2234+
WINAPI_CONSTANT("u", LOCALE_NAME_SYSTEM_DEFAULT);
2235+
WINAPI_CONSTANT("u", LOCALE_NAME_USER_DEFAULT);
2236+
2237+
WINAPI_CONSTANT(F_DWORD, LCMAP_FULLWIDTH);
2238+
WINAPI_CONSTANT(F_DWORD, LCMAP_HALFWIDTH);
2239+
WINAPI_CONSTANT(F_DWORD, LCMAP_HIRAGANA);
2240+
WINAPI_CONSTANT(F_DWORD, LCMAP_KATAKANA);
2241+
WINAPI_CONSTANT(F_DWORD, LCMAP_LINGUISTIC_CASING);
2242+
WINAPI_CONSTANT(F_DWORD, LCMAP_LOWERCASE);
2243+
WINAPI_CONSTANT(F_DWORD, LCMAP_SIMPLIFIED_CHINESE);
2244+
WINAPI_CONSTANT(F_DWORD, LCMAP_TITLECASE);
2245+
WINAPI_CONSTANT(F_DWORD, LCMAP_TRADITIONAL_CHINESE);
2246+
WINAPI_CONSTANT(F_DWORD, LCMAP_UPPERCASE);
2247+
21632248
WINAPI_CONSTANT("i", NULL);
21642249

21652250
return 0;

Modules/clinic/_winapi.c.h

+33-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)