diff --git a/Include/internal/pycore_exceptions.h b/Include/internal/pycore_exceptions.h index 26456d1966bbb0..b1964117c8c7e5 100644 --- a/Include/internal/pycore_exceptions.h +++ b/Include/internal/pycore_exceptions.h @@ -22,6 +22,10 @@ extern void _PyExc_Fini(PyInterpreterState *); struct _Py_exc_state { // The dict mapping from errno codes to OSError subclasses PyObject *errnomap; + // Tuple of the locale's identity and its _Py_strerror() messages, or None + // if not built yet, see strerror_ensure_cache() + PyObject *strerror_cache; + PyMutex strerror_lock; PyBaseExceptionObject *memerrors_freelist; int memerrors_numfree; #ifdef Py_GIL_DISABLED diff --git a/Lib/test/test_exception_hierarchy.py b/Lib/test/test_exception_hierarchy.py index 3318fa8e7746f7..39c0210960db6f 100644 --- a/Lib/test/test_exception_hierarchy.py +++ b/Lib/test/test_exception_hierarchy.py @@ -1,10 +1,12 @@ import builtins +import locale import os import select import socket import unittest import errno from errno import EEXIST +from test.support import cpython_only class SubOSError(OSError): @@ -136,6 +138,27 @@ def test_posix_error(self): if os.name == "nt": self.assertEqual(e.winerror, None) + @cpython_only + @unittest.skipIf(os.name == "nt", "POSIX-specific test") + def test_strerror_cached(self): + def strerror(): + with self.assertRaises(FileNotFoundError) as cm: + open("some_hopefully_non_existing_file") + return cm.exception.strerror + message = strerror() + self.assertIs(strerror(), message) + self.assertIs(os.strerror(errno.ENOENT), message) + if hasattr(locale, "LC_MESSAGES"): + old = locale.setlocale(locale.LC_MESSAGES) + self.addCleanup(locale.setlocale, locale.LC_MESSAGES, old) + locale.setlocale(locale.LC_MESSAGES, "C") + message = strerror() + try: + locale.setlocale(locale.LC_MESSAGES, "C.UTF-8") + self.assertIsNot(strerror(), message) + except locale.Error: + pass + @unittest.skipUnless(os.name == "nt", "Windows-specific test") def test_errno_translation(self): # ERROR_ALREADY_EXISTS (183) -> EEXIST diff --git a/Objects/exceptions.c b/Objects/exceptions.c index e2f757c5ee9c0b..9568be7aed6cff 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -4593,6 +4593,7 @@ PyStatus _PyExc_InitState(PyInterpreterState *interp) { struct _Py_exc_state *state = &interp->exc_state; + state->strerror_cache = Py_None; #define ADD_ERRNO(TYPE, CODE) \ do { \ @@ -4699,6 +4700,8 @@ _PyExc_Fini(PyInterpreterState *interp) struct _Py_exc_state *state = &interp->exc_state; free_preallocated_memerrors(state); Py_CLEAR(state->errnomap); + // Not NULL: _Py_strerror() can still run after this. + Py_XSETREF(state->strerror_cache, Py_None); _PyExc_FiniTypes(interp); } diff --git a/Python/fileutils.c b/Python/fileutils.c index 3ea0f59f69ad1e..f43d0160cc8d88 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -1,5 +1,6 @@ #include "Python.h" #include "pycore_fileutils.h" // fileutils definitions +#include "pycore_long.h" // _PyLong_FromUnsignedChar() #include "pycore_runtime.h" // _PyRuntime #include "pycore_pystate.h" // _Py_AssertHoldsTstate() #include "pycore_unicodeobject.h" // _Py_MAX_UNICODE @@ -3304,13 +3305,8 @@ _Py_IsValidFD(int fd) } -// Call strerror_r(code) if available, or use strerror() otherwise. Decode the -// result from the locale encoding using surrogateescape error handler. -// -// On success, return a Unicode string. On error, set an exception and return -// NULL. -PyObject* -_Py_strerror(int code) +static PyObject * +decode_strerror(int code) /*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/ { const char *errors = "surrogateescape"; @@ -3396,3 +3392,88 @@ _Py_strerror(int code) return PyUnicode_DecodeLocale(message, errors); #endif } + + +// Return a strong reference to the tuple (codeset, LC_MESSAGES name, +// {code: message}) of the current locale, replacing it if the locale changed. +static PyObject * +strerror_ensure_cache(struct _Py_exc_state *state) +{ + // C code can change the locale without locale.setlocale(). Messages depend + // on LC_MESSAGES (language) and the LC_CTYPE codeset (decoding). Windows + // has neither, and its messages are ASCII. +#if defined(HAVE_LANGINFO_H) && defined(CODESET) + const char *codeset = nl_langinfo(CODESET); + const char *messages = setlocale(LC_MESSAGES, NULL); +#else + const char *codeset = ""; + const char *messages = ""; +#endif + // Py_SETREF() isn't atomic. + PyObject *cache; + Py_BEGIN_CRITICAL_SECTION_MUTEX(&state->strerror_lock); + cache = state->strerror_cache; + Py_IncRef(cache); + Py_END_CRITICAL_SECTION(); + if (cache != Py_None + && PyUnicode_CompareWithASCIIString( + PyTuple_GET_ITEM(cache, 0), codeset) == 0 + && PyUnicode_CompareWithASCIIString( + PyTuple_GET_ITEM(cache, 1), messages) == 0) + { + return cache; + } + Py_DECREF(cache); + cache = PyTuple_New(3); + if (cache == NULL) { + return NULL; + } + // Latin-1, which is how the comparison above reads a non-ASCII name. + PyTuple_SET_ITEM(cache, 0, + PyUnicode_DecodeLatin1(codeset, strlen(codeset), NULL)); + PyTuple_SET_ITEM(cache, 1, + PyUnicode_DecodeLatin1(messages, strlen(messages), NULL)); + PyTuple_SET_ITEM(cache, 2, PyDict_New()); + if (PyTuple_GET_ITEM(cache, 0) == NULL + || PyTuple_GET_ITEM(cache, 1) == NULL + || PyTuple_GET_ITEM(cache, 2) == NULL) + { + Py_DECREF(cache); + return NULL; + } + Py_BEGIN_CRITICAL_SECTION_MUTEX(&state->strerror_lock); + Py_SETREF(state->strerror_cache, Py_NewRef(cache)); + Py_END_CRITICAL_SECTION(); + return cache; +} + +// Call strerror_r(code) if available, or use strerror() otherwise. Decode the +// result from the locale encoding using surrogateescape error handler. +// Messages of codes 0-255 are cached per locale: decoding is a large part of +// the cost of raising OSError. +// +// On success, return a Unicode string. On error, set an exception and return +// NULL. +PyObject* +_Py_strerror(int code) +{ + if (code < 0 || code >= 256) { + return decode_strerror(code); + } + struct _Py_exc_state *state = &_PyInterpreterState_GET()->exc_state; + PyObject *cache = strerror_ensure_cache(state); + if (cache == NULL) { + return NULL; + } + PyObject *dict = PyTuple_GET_ITEM(cache, 2); + PyObject *key = _PyLong_FromUnsignedChar((unsigned char)code); + PyObject *message; + if (PyDict_GetItemRef(dict, key, &message) == 0) { + message = decode_strerror(code); + if (message != NULL && PyDict_SetItem(dict, key, message) < 0) { + Py_CLEAR(message); + } + } + Py_DECREF(cache); + return message; +}