Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-42658: Using LCMapStringEx in ntpath.normcase #32010

Merged
merged 17 commits into from
Jun 6, 2022
42 changes: 34 additions & 8 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import genericpath
from genericpath import *


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

def normcase(s):
"""Normalize case of pathname.

Makes all characters lowercase and all slashes into backslashes."""
s = os.fspath(s)
if isinstance(s, bytes):
return s.replace(b'/', b'\\').lower()
else:
try:
from _winapi import (
LCMapStringEx as _LCMapStringEx,
LOCALE_NAME_INVARIANT as _LOCALE_NAME_INVARIANT,
LCMAP_LOWERCASE as _LCMAP_LOWERCASE)

def normcase(s):
"""Normalize case of pathname.

Makes all characters lowercase and all slashes into backslashes.
"""
s = os.fspath(s)
if not s:
return s
if isinstance(s, bytes):
encoding = sys.getfilesystemencoding()
s = s.decode(encoding, 'surrogateescape').replace('/', '\\')
s = _LCMapStringEx(_LOCALE_NAME_INVARIANT,
_LCMAP_LOWERCASE, s)
return s.encode(encoding, 'surrogateescape')
else:
return _LCMapStringEx(_LOCALE_NAME_INVARIANT,
_LCMAP_LOWERCASE,
s.replace('/', '\\'))
except ImportError:
def normcase(s):
"""Normalize case of pathname.

Makes all characters lowercase and all slashes into backslashes.
"""
s = os.fspath(s)
if isinstance(s, bytes):
aisk marked this conversation as resolved.
Show resolved Hide resolved
return os.fsencode(os.fsdecode(s).replace('/', '\\').lower())
return s.replace('/', '\\').lower()


Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,8 @@ def _check_function(self, func):

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

def test_path_isabs(self):
self._check_function(self.path.isabs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Support native Windows case-insensitive path comparisons by using
``LCMapStringEx`` instead of :func:`str.lower` in :func:`ntpath.normcase`.
Add ``LCMapStringEx`` to the :mod:`_winapi` module.
61 changes: 61 additions & 0 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,50 @@ _winapi_PeekNamedPipe_impl(PyObject *module, HANDLE handle, int size)
}
}

/*[clinic input]
_winapi.LCMapStringEx

locale: LPCWSTR
flags: DWORD
src: LPCWSTR

[clinic start generated code]*/

static PyObject *
_winapi_LCMapStringEx_impl(PyObject *module, LPCWSTR locale, DWORD flags,
LPCWSTR src)
/*[clinic end generated code: output=cf4713d80e2b47c9 input=9fe26f95d5ab0001]*/
{
if (flags & (LCMAP_SORTHANDLE | LCMAP_HASH | LCMAP_BYTEREV |
LCMAP_SORTKEY)) {
return PyErr_Format(PyExc_ValueError, "unsupported flags");
}

int dest_size = LCMapStringEx(locale, flags, src, -1, NULL, 0,
NULL, NULL, 0);
if (dest_size == 0) {
return PyErr_SetFromWindowsErr(0);
}

wchar_t* dest = PyMem_NEW(wchar_t, dest_size);
aisk marked this conversation as resolved.
Show resolved Hide resolved
if (dest == NULL) {
return PyErr_NoMemory();
}

int nmapped = LCMapStringEx(locale, flags, src, -1, dest, dest_size,
NULL, NULL, 0);
if (nmapped == 0) {
DWORD error = GetLastError();
PyMem_DEL(dest);
return PyErr_SetFromWindowsErr(error);
}

PyObject *ret = PyUnicode_FromWideChar(dest, dest_size - 1);
PyMem_DEL(dest);

return ret;
}

/*[clinic input]
_winapi.ReadFile

Expand Down Expand Up @@ -2023,6 +2067,7 @@ static PyMethodDef winapi_functions[] = {
_WINAPI_OPENFILEMAPPING_METHODDEF
_WINAPI_OPENPROCESS_METHODDEF
_WINAPI_PEEKNAMEDPIPE_METHODDEF
_WINAPI_LCMAPSTRINGEX_METHODDEF
_WINAPI_READFILE_METHODDEF
_WINAPI_SETNAMEDPIPEHANDLESTATE_METHODDEF
_WINAPI_TERMINATEPROCESS_METHODDEF
Expand Down Expand Up @@ -2160,6 +2205,22 @@ static int winapi_exec(PyObject *m)
WINAPI_CONSTANT(F_DWORD, FILE_TYPE_PIPE);
WINAPI_CONSTANT(F_DWORD, FILE_TYPE_REMOTE);

WINAPI_CONSTANT("u", LOCALE_NAME_INVARIANT);
WINAPI_CONSTANT(F_DWORD, LOCALE_NAME_MAX_LENGTH);
WINAPI_CONSTANT("u", LOCALE_NAME_SYSTEM_DEFAULT);
WINAPI_CONSTANT("u", LOCALE_NAME_USER_DEFAULT);

WINAPI_CONSTANT(F_DWORD, LCMAP_FULLWIDTH);
WINAPI_CONSTANT(F_DWORD, LCMAP_HALFWIDTH);
WINAPI_CONSTANT(F_DWORD, LCMAP_HIRAGANA);
WINAPI_CONSTANT(F_DWORD, LCMAP_KATAKANA);
WINAPI_CONSTANT(F_DWORD, LCMAP_LINGUISTIC_CASING);
WINAPI_CONSTANT(F_DWORD, LCMAP_LOWERCASE);
WINAPI_CONSTANT(F_DWORD, LCMAP_SIMPLIFIED_CHINESE);
WINAPI_CONSTANT(F_DWORD, LCMAP_TITLECASE);
WINAPI_CONSTANT(F_DWORD, LCMAP_TRADITIONAL_CHINESE);
WINAPI_CONSTANT(F_DWORD, LCMAP_UPPERCASE);

WINAPI_CONSTANT("i", NULL);

return 0;
Expand Down
39 changes: 38 additions & 1 deletion Modules/clinic/_winapi.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.