diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h index 99dd6bf1b912dd..6c6e2ed21e3761 100644 --- a/Include/internal/pycore_bytesobject.h +++ b/Include/internal/pycore_bytesobject.h @@ -60,93 +60,7 @@ PyAPI_FUNC(void) _PyBytes_Repeat(char* dest, Py_ssize_t len_dest, const char* src, Py_ssize_t len_src); -/* --- _PyBytesWriter ----------------------------------------------------- */ - -/* The _PyBytesWriter structure is big: it contains an embedded "stack buffer". - A _PyBytesWriter variable must be declared at the end of variables in a - function to optimize the memory allocation on the stack. */ -typedef struct { - /* bytes, bytearray or NULL (when the small buffer is used) */ - PyObject *buffer; - - /* Number of allocated size. */ - Py_ssize_t allocated; - - /* Minimum number of allocated bytes, - incremented by _PyBytesWriter_Prepare() */ - Py_ssize_t min_size; - - /* If non-zero, use a bytearray instead of a bytes object for buffer. */ - int use_bytearray; - - /* If non-zero, overallocate the buffer (default: 0). - This flag must be zero if use_bytearray is non-zero. */ - int overallocate; - - /* Stack buffer */ - int use_small_buffer; - char small_buffer[512]; -} _PyBytesWriter; - -/* Initialize a bytes writer - - By default, the overallocation is disabled. Set the overallocate attribute - to control the allocation of the buffer. - - Export _PyBytesWriter API for '_pickle' shared extension. */ -PyAPI_FUNC(void) _PyBytesWriter_Init(_PyBytesWriter *writer); - -/* Get the buffer content and reset the writer. - Return a bytes object, or a bytearray object if use_bytearray is non-zero. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(PyObject *) _PyBytesWriter_Finish(_PyBytesWriter *writer, - void *str); - -/* Deallocate memory of a writer (clear its internal buffer). */ -PyAPI_FUNC(void) _PyBytesWriter_Dealloc(_PyBytesWriter *writer); - -/* Allocate the buffer to write size bytes. - Return the pointer to the beginning of buffer data. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_Alloc(_PyBytesWriter *writer, - Py_ssize_t size); - -/* Ensure that the buffer is large enough to write *size* bytes. - Add size to the writer minimum size (min_size attribute). - - str is the current pointer inside the buffer. - Return the updated current pointer inside the buffer. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_Prepare(_PyBytesWriter *writer, - void *str, - Py_ssize_t size); - -/* Resize the buffer to make it larger. - The new buffer may be larger than size bytes because of overallocation. - Return the updated current pointer inside the buffer. - Raise an exception and return NULL on error. - - Note: size must be greater than the number of allocated bytes in the writer. - - This function doesn't use the writer minimum size (min_size attribute). - - See also _PyBytesWriter_Prepare(). - */ -PyAPI_FUNC(void*) _PyBytesWriter_Resize(_PyBytesWriter *writer, - void *str, - Py_ssize_t size); - -/* Write bytes. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_WriteBytes(_PyBytesWriter *writer, - void *str, - const void *bytes, - Py_ssize_t size); - -// Export for '_testcapi' shared extension. -PyAPI_FUNC(PyBytesWriter*) _PyBytesWriter_CreateByteArray( - Py_ssize_t size); - +/* --- PyBytesWriter ------------------------------------------------------ */ struct PyBytesWriter { char small_buffer[256]; @@ -156,6 +70,9 @@ struct PyBytesWriter { int overallocate; }; +// Export for '_testcapi' shared extension +PyAPI_FUNC(PyBytesWriter*) _PyBytesWriter_CreateByteArray(Py_ssize_t size); + #ifdef __cplusplus } #endif diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index b2daeadc7e7278..e7a12e4d0b6d57 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -464,7 +464,11 @@ def _parse_args(args, **kwargs): if ns.python is None: ns.rerun = True ns.print_slow = True - ns.verbose3 = True + if not ns.verbose: + ns.verbose3 = True + else: + # --verbose has the priority over --verbose3 + pass else: ns._add_python_opts = False diff --git a/Lib/test/test_c_locale_coercion.py b/Lib/test/test_c_locale_coercion.py index 10f8ba2255228b..340bec3c71b68f 100644 --- a/Lib/test/test_c_locale_coercion.py +++ b/Lib/test/test_c_locale_coercion.py @@ -15,7 +15,7 @@ # Set the list of ways we expect to be able to ask for the "C" locale. # 'invalid.ascii' is an invalid LOCALE name and so should get turned in to the # default locale, which is traditionally C. -EXPECTED_C_LOCALE_EQUIVALENTS = ["C", "invalid.ascii"] +EXPECTED_C_LOCALE_EQUIVALENTS = ["C", "POSIX", "invalid.ascii"] # Set our expectation for the default encoding used in the C locale # for the filesystem encoding and the standard streams @@ -55,11 +55,6 @@ # VxWorks defaults to using UTF-8 for all system interfaces EXPECTED_C_LOCALE_STREAM_ENCODING = "utf-8" EXPECTED_C_LOCALE_FS_ENCODING = "utf-8" -if sys.platform.startswith("linux"): - # Linux recognizes POSIX as a synonym for C. Python will always coerce - # if the locale is set to POSIX, but not all platforms will use the - # C locale encodings if POSIX is set, so we'll only test it on linux. - EXPECTED_C_LOCALE_EQUIVALENTS.append("POSIX") # Note that the above expectations are still wrong in some cases, such as: # * Windows when PYTHONLEGACYWINDOWSFSENCODING is set @@ -467,8 +462,9 @@ def test_PYTHONCOERCECLOCALE_set_to_one(self): loc = locale.setlocale(locale.LC_CTYPE, "") except locale.Error as e: self.skipTest(str(e)) - if loc == "C": - self.skipTest("test requires LC_CTYPE locale different than C") + if loc in ("C", "POSIX"): + self.skipTest("test requires LC_CTYPE locale different " + "than C and POSIX") if loc in TARGET_LOCALES : self.skipTest("coerced LC_CTYPE locale: %s" % loc) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 2c28f106ec7cb6..31e58e825be422 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -12,6 +12,10 @@ import _testlimitedcapi except ImportError: _testlimitedcapi = None +try: + import _testinternalcapi +except ImportError: + _testinternalcapi = None import struct import collections import itertools @@ -1037,6 +1041,18 @@ def test_unexpected_keyword_suggestion_via_getargs(self): @cpython_only class TestRecursion(unittest.TestCase): + def test_margin_is_sufficient(self): + + def get_sp(): + return _testinternalcapi.get_stack_pointer() + + this_sp = _testinternalcapi.get_stack_pointer() + lower_sp = _testcapi.pyobject_vectorcall(get_sp, (), ()) + self.assertLess(lower_sp, this_sp) + # Add an (arbitrary) extra 25% for safety + safe_margin = (this_sp - lower_sp) * 5 / 4 + self.assertLess(safe_margin, _testinternalcapi.get_stack_margin()) + @skip_on_s390x @unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack") @skip_if_sanitizer("requires deep stack", thread=True) diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 8e9f1c3204a8bb..9a5ee03e4722c0 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -459,7 +459,12 @@ def test(): error = proc.stderr.read() self.assertIn(b"KeyboardInterrupt", error) retcode = proc.wait() - self.assertEqual(retcode, 0) + # Sometimes we send the SIGINT after the subthread yields the GIL to + # the main thread, which results in the main thread getting the + # KeyboardInterrupt before finalization is reached. There's not + # any great way to protect against that, so we just allow a -2 + # return code as well. + self.assertIn(retcode, (0, -signal.SIGINT)) class TestInterpreterIsRunning(TestBase): diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 31f0a1eb2cbf40..1793ef148703b1 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1299,7 +1299,6 @@ def test_apropos_with_unreadable_dir(self): self.assertEqual(out.getvalue(), '') self.assertEqual(err.getvalue(), '') - @os_helper.skip_unless_working_chmod def test_apropos_empty_doc(self): pkgdir = os.path.join(TESTFN, 'walkpkg') os.mkdir(pkgdir) diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 50e6eb32817700..c27b3c862924d1 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -448,7 +448,8 @@ def create_regrtest(self, args): return regrtest - def check_ci_mode(self, args, use_resources, *, rerun=True, randomize=True): + def check_ci_mode(self, args, use_resources, + *, rerun=True, randomize=True, output_on_failure=True): regrtest = self.create_regrtest(args) self.assertEqual(regrtest.num_workers, -1) self.assertEqual(regrtest.want_rerun, rerun) @@ -457,7 +458,7 @@ def check_ci_mode(self, args, use_resources, *, rerun=True, randomize=True): self.assertIsInstance(regrtest.random_seed, int) self.assertTrue(regrtest.fail_env_changed) self.assertTrue(regrtest.print_slowest) - self.assertTrue(regrtest.output_on_failure) + self.assertEqual(regrtest.output_on_failure, output_on_failure) self.assertEqual(sorted(regrtest.use_resources), sorted(use_resources)) return regrtest @@ -484,6 +485,14 @@ def test_fast_ci_resource(self): use_resources.remove('network') self.check_ci_mode(args, use_resources) + def test_fast_ci_verbose(self): + args = ['--fast-ci', '--verbose'] + use_resources = sorted(cmdline.ALL_RESOURCES) + use_resources.remove('cpu') + regrtest = self.check_ci_mode(args, use_resources, + output_on_failure=False) + self.assertEqual(regrtest.verbose, True) + def test_slow_ci(self): args = ['--slow-ci'] use_resources = sorted(cmdline.ALL_RESOURCES) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-22-15-21-49.gh-issue-74857.5XRQaA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-22-15-21-49.gh-issue-74857.5XRQaA.rst new file mode 100644 index 00000000000000..820b57e920020b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-22-15-21-49.gh-issue-74857.5XRQaA.rst @@ -0,0 +1,2 @@ +:pep:`538`: Coerce the POSIX locale to a UTF-8 based locale. Patch by Victor +Stinner. diff --git a/Misc/NEWS.d/next/Library/2025-09-15-19-29-12.gh-issue-130567.shDEnT.rst b/Misc/NEWS.d/next/Library/2025-09-15-19-29-12.gh-issue-130567.shDEnT.rst new file mode 100644 index 00000000000000..c194b2331e5f4a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-15-19-29-12.gh-issue-130567.shDEnT.rst @@ -0,0 +1,2 @@ +Fix possible crash in :func:`locale.strxfrm` due to a platform bug on +macOS. diff --git a/Misc/NEWS.d/next/Tests/2025-09-22-15-40-09.gh-issue-139208.Tc13dl.rst b/Misc/NEWS.d/next/Tests/2025-09-22-15-40-09.gh-issue-139208.Tc13dl.rst new file mode 100644 index 00000000000000..b8672ac83e1ead --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2025-09-22-15-40-09.gh-issue-139208.Tc13dl.rst @@ -0,0 +1,2 @@ +Fix regrtest ``--fast-ci --verbose``: don't ignore the ``--verbose`` option +anymore. Patch by Victor Stinner. diff --git a/Modules/_localemodule.c b/Modules/_localemodule.c index e86d5b17d1759d..cb448b14d8cd63 100644 --- a/Modules/_localemodule.c +++ b/Modules/_localemodule.c @@ -457,7 +457,9 @@ _locale_strxfrm_impl(PyObject *module, PyObject *str) /* assume no change in size, first */ n1 = n1 + 1; - buf = PyMem_New(wchar_t, n1); + /* Yet another +1 is needed to work around a platform bug in wcsxfrm() + * on macOS. See gh-130567. */ + buf = PyMem_New(wchar_t, n1+1); if (!buf) { PyErr_NoMemory(); goto exit; diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 7aa63f913af335..d680711e5d828a 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -125,6 +125,18 @@ get_c_recursion_remaining(PyObject *self, PyObject *Py_UNUSED(args)) return PyLong_FromLong(remaining); } +static PyObject* +get_stack_pointer(PyObject *self, PyObject *Py_UNUSED(args)) +{ + uintptr_t here_addr = _Py_get_machine_stack_pointer(); + return PyLong_FromSize_t(here_addr); +} + +static PyObject* +get_stack_margin(PyObject *self, PyObject *Py_UNUSED(args)) +{ + return PyLong_FromSize_t(_PyOS_STACK_MARGIN_BYTES); +} static PyObject* test_bswap(PyObject *self, PyObject *Py_UNUSED(args)) @@ -2390,6 +2402,8 @@ static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, {"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS}, + {"get_stack_pointer", get_stack_pointer, METH_NOARGS}, + {"get_stack_margin", get_stack_margin, METH_NOARGS}, {"test_bswap", test_bswap, METH_NOARGS}, {"test_popcount", test_popcount, METH_NOARGS}, {"test_bit_length", test_bit_length, METH_NOARGS}, diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 07237ceaa647e6..91d20cb9afa7ba 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -34,8 +34,6 @@ class bytes "PyBytesObject *" "&PyBytes_Type" #define PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1) /* Forward declaration */ -Py_LOCAL_INLINE(Py_ssize_t) _PyBytesWriter_GetSize(_PyBytesWriter *writer, - char *str); static void* _PyBytesWriter_ResizeAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *data); static Py_ssize_t _PyBytesWriter_GetAllocated(PyBytesWriter *writer); @@ -3453,288 +3451,6 @@ bytes_iter(PyObject *seq) } -/* _PyBytesWriter API */ - -#ifdef MS_WINDOWS - /* On Windows, overallocate by 50% is the best factor */ -# define OVERALLOCATE_FACTOR 2 -#else - /* On Linux, overallocate by 25% is the best factor */ -# define OVERALLOCATE_FACTOR 4 -#endif - -void -_PyBytesWriter_Init(_PyBytesWriter *writer) -{ - /* Set all attributes before small_buffer to 0 */ - memset(writer, 0, offsetof(_PyBytesWriter, small_buffer)); -#ifndef NDEBUG - memset(writer->small_buffer, PYMEM_CLEANBYTE, - sizeof(writer->small_buffer)); -#endif -} - -void -_PyBytesWriter_Dealloc(_PyBytesWriter *writer) -{ - Py_CLEAR(writer->buffer); -} - -Py_LOCAL_INLINE(char*) -_PyBytesWriter_AsString(_PyBytesWriter *writer) -{ - if (writer->use_small_buffer) { - assert(writer->buffer == NULL); - return writer->small_buffer; - } - else if (writer->use_bytearray) { - assert(writer->buffer != NULL); - return PyByteArray_AS_STRING(writer->buffer); - } - else { - assert(writer->buffer != NULL); - return PyBytes_AS_STRING(writer->buffer); - } -} - -Py_LOCAL_INLINE(Py_ssize_t) -_PyBytesWriter_GetSize(_PyBytesWriter *writer, char *str) -{ - const char *start = _PyBytesWriter_AsString(writer); - assert(str != NULL); - assert(str >= start); - assert(str - start <= writer->allocated); - return str - start; -} - -#ifndef NDEBUG -Py_LOCAL_INLINE(int) -_PyBytesWriter_CheckConsistency(_PyBytesWriter *writer, char *str) -{ - const char *start, *end; - - if (writer->use_small_buffer) { - assert(writer->buffer == NULL); - } - else { - assert(writer->buffer != NULL); - if (writer->use_bytearray) - assert(PyByteArray_CheckExact(writer->buffer)); - else - assert(PyBytes_CheckExact(writer->buffer)); - assert(Py_REFCNT(writer->buffer) == 1); - } - - if (writer->use_bytearray) { - /* bytearray has its own overallocation algorithm, - writer overallocation must be disabled */ - assert(!writer->overallocate); - } - - assert(0 <= writer->allocated); - assert(0 <= writer->min_size && writer->min_size <= writer->allocated); - /* the last byte must always be null */ - start = _PyBytesWriter_AsString(writer); - assert(start[writer->allocated] == 0); - - end = start + writer->allocated; - assert(str != NULL); - assert(start <= str && str <= end); - return 1; -} -#endif - -void* -_PyBytesWriter_Resize(_PyBytesWriter *writer, void *str, Py_ssize_t size) -{ - Py_ssize_t allocated, pos; - - assert(_PyBytesWriter_CheckConsistency(writer, str)); - assert(writer->allocated < size); - - allocated = size; - if (writer->overallocate - && allocated <= (PY_SSIZE_T_MAX - allocated / OVERALLOCATE_FACTOR)) { - /* overallocate to limit the number of realloc() */ - allocated += allocated / OVERALLOCATE_FACTOR; - } - - pos = _PyBytesWriter_GetSize(writer, str); - if (!writer->use_small_buffer) { - if (writer->use_bytearray) { - if (PyByteArray_Resize(writer->buffer, allocated)) - goto error; - /* writer->allocated can be smaller than writer->buffer->ob_alloc, - but we cannot use ob_alloc because bytes may need to be moved - to use the whole buffer. bytearray uses an internal optimization - to avoid moving or copying bytes when bytes are removed at the - beginning (ex: del bytearray[:1]). */ - } - else { - if (_PyBytes_Resize(&writer->buffer, allocated)) - goto error; - } - } - else { - /* convert from stack buffer to bytes object buffer */ - assert(writer->buffer == NULL); - - if (writer->use_bytearray) - writer->buffer = PyByteArray_FromStringAndSize(NULL, allocated); - else - writer->buffer = PyBytes_FromStringAndSize(NULL, allocated); - if (writer->buffer == NULL) - goto error; - - if (pos != 0) { - char *dest; - if (writer->use_bytearray) - dest = PyByteArray_AS_STRING(writer->buffer); - else - dest = PyBytes_AS_STRING(writer->buffer); - memcpy(dest, - writer->small_buffer, - pos); - } - - writer->use_small_buffer = 0; -#ifndef NDEBUG - memset(writer->small_buffer, PYMEM_CLEANBYTE, - sizeof(writer->small_buffer)); -#endif - } - writer->allocated = allocated; - - str = _PyBytesWriter_AsString(writer) + pos; - assert(_PyBytesWriter_CheckConsistency(writer, str)); - return str; - -error: - _PyBytesWriter_Dealloc(writer); - return NULL; -} - -void* -_PyBytesWriter_Prepare(_PyBytesWriter *writer, void *str, Py_ssize_t size) -{ - Py_ssize_t new_min_size; - - assert(_PyBytesWriter_CheckConsistency(writer, str)); - assert(size >= 0); - - if (size == 0) { - /* nothing to do */ - return str; - } - - if (writer->min_size > PY_SSIZE_T_MAX - size) { - PyErr_NoMemory(); - _PyBytesWriter_Dealloc(writer); - return NULL; - } - new_min_size = writer->min_size + size; - - if (new_min_size > writer->allocated) - str = _PyBytesWriter_Resize(writer, str, new_min_size); - - writer->min_size = new_min_size; - return str; -} - -/* Allocate the buffer to write size bytes. - Return the pointer to the beginning of buffer data. - Raise an exception and return NULL on error. */ -void* -_PyBytesWriter_Alloc(_PyBytesWriter *writer, Py_ssize_t size) -{ - /* ensure that _PyBytesWriter_Alloc() is only called once */ - assert(writer->min_size == 0 && writer->buffer == NULL); - assert(size >= 0); - - writer->use_small_buffer = 1; -#ifndef NDEBUG - writer->allocated = sizeof(writer->small_buffer) - 1; - /* In debug mode, don't use the full small buffer because it is less - efficient than bytes and bytearray objects to detect buffer underflow - and buffer overflow. Use 10 bytes of the small buffer to test also - code using the smaller buffer in debug mode. - - Don't modify the _PyBytesWriter structure (use a shorter small buffer) - in debug mode to also be able to detect stack overflow when running - tests in debug mode. The _PyBytesWriter is large (more than 512 bytes), - if _Py_EnterRecursiveCall() is not used in deep C callback, we may hit a - stack overflow. */ - writer->allocated = Py_MIN(writer->allocated, 10); - /* _PyBytesWriter_CheckConsistency() requires the last byte to be 0, - to detect buffer overflow */ - writer->small_buffer[writer->allocated] = 0; -#else - writer->allocated = sizeof(writer->small_buffer); -#endif - return _PyBytesWriter_Prepare(writer, writer->small_buffer, size); -} - -PyObject * -_PyBytesWriter_Finish(_PyBytesWriter *writer, void *str) -{ - Py_ssize_t size; - PyObject *result; - - assert(_PyBytesWriter_CheckConsistency(writer, str)); - - size = _PyBytesWriter_GetSize(writer, str); - if (size == 0 && !writer->use_bytearray) { - Py_CLEAR(writer->buffer); - /* Get the empty byte string singleton */ - result = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - } - else if (writer->use_small_buffer) { - if (writer->use_bytearray) { - result = PyByteArray_FromStringAndSize(writer->small_buffer, size); - } - else { - result = PyBytes_FromStringAndSize(writer->small_buffer, size); - } - } - else { - result = writer->buffer; - writer->buffer = NULL; - - if (size != writer->allocated) { - if (writer->use_bytearray) { - if (PyByteArray_Resize(result, size)) { - Py_DECREF(result); - return NULL; - } - } - else { - if (_PyBytes_Resize(&result, size)) { - assert(result == NULL); - return NULL; - } - } - } - } - return result; -} - -void* -_PyBytesWriter_WriteBytes(_PyBytesWriter *writer, void *ptr, - const void *bytes, Py_ssize_t size) -{ - char *str = (char *)ptr; - - str = _PyBytesWriter_Prepare(writer, str, size); - if (str == NULL) - return NULL; - - memcpy(str, bytes, size); - str += size; - - return str; -} - - void _PyBytes_Repeat(char* dest, Py_ssize_t len_dest, const char* src, Py_ssize_t len_src) @@ -3799,7 +3515,6 @@ byteswriter_allocated(PyBytesWriter *writer) # define OVERALLOCATE_FACTOR 4 #endif - static inline int byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize) { diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 37af58a68d7883..185c9ae752819a 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -209,7 +209,10 @@ _Py_LegacyLocaleDetected(int warn) * we may also want to check for that explicitly. */ const char *ctype_loc = setlocale(LC_CTYPE, NULL); - return ctype_loc != NULL && strcmp(ctype_loc, "C") == 0; + if (ctype_loc == NULL) { + return 0; + } + return (strcmp(ctype_loc, "C") == 0 || strcmp(ctype_loc, "POSIX") == 0); #else /* Windows uses code pages instead of locales, so no locale is legacy */ return 0;