Skip to content

Commit 7755fce

Browse files
[3.14] gh-87587: Fix os.device_encoding() for any console on Windows (GH-155410) (GH-156596)
It was hard coded to map file descriptors 0, 1 and 2 to the console code page, but a console can be opened as any file descriptor, and other character devices, like NUL, are not consoles. The file handle is now queried. The UTF-8 code page is also now reported as "utf-8" instead of "cp65001". (cherry picked from commit b4aea41)
1 parent 162c47c commit 7755fce

3 files changed

Lines changed: 85 additions & 16 deletions

File tree

Lib/test/test_os.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3575,6 +3575,45 @@ def test_device_encoding(self):
35753575
self.assertTrue(codecs.lookup(encoding))
35763576

35773577

3578+
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
3579+
class Win32DeviceEncodingTests(unittest.TestCase):
3580+
# gh-87587: any console file descriptor is supported, not only 0, 1 and 2,
3581+
# and other character devices are not consoles.
3582+
3583+
@staticmethod
3584+
def expected_encoding(cp):
3585+
return 'utf-8' if cp == 65001 else 'cp%d' % cp
3586+
3587+
def test_console(self):
3588+
import ctypes
3589+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
3590+
try:
3591+
fin = open('CONIN$')
3592+
except OSError:
3593+
self.skipTest('no console')
3594+
with fin:
3595+
self.assertEqual(os.device_encoding(fin.fileno()),
3596+
self.expected_encoding(kernel32.GetConsoleCP()))
3597+
with open('CONOUT$', 'w') as fout:
3598+
self.assertEqual(
3599+
os.device_encoding(fout.fileno()),
3600+
self.expected_encoding(kernel32.GetConsoleOutputCP()))
3601+
3602+
def test_not_a_console(self):
3603+
with open('NUL', 'w') as f:
3604+
self.assertTrue(os.isatty(f.fileno()))
3605+
self.assertIsNone(os.device_encoding(f.fileno()))
3606+
# Not a console even if it is a standard file descriptor.
3607+
saved = os.dup(1)
3608+
try:
3609+
os.dup2(f.fileno(), 1)
3610+
encoding = os.device_encoding(1)
3611+
finally:
3612+
os.dup2(saved, 1)
3613+
os.close(saved)
3614+
self.assertIsNone(encoding)
3615+
3616+
35783617
@support.requires_subprocess()
35793618
class PidTests(unittest.TestCase):
35803619
@unittest.skipUnless(hasattr(os, 'getppid'), "test needs os.getppid")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`os.device_encoding` on Windows now returns the code page of any
2+
console file descriptor, not only 0, 1 and 2, and returns ``None`` for other
3+
character devices like ``NUL``. The UTF-8 code page is now reported as
4+
``"utf-8"`` instead of ``"cp65001"``.

Python/fileutils.c

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,41 +80,67 @@ get_surrogateescape(_Py_error_handler errors, int *surrogateescape)
8080
PyObject *
8181
_Py_device_encoding(int fd)
8282
{
83-
int valid;
84-
Py_BEGIN_ALLOW_THREADS
83+
#if defined(MS_WINDOWS) && defined(HAVE_WINDOWS_CONSOLE_IO)
84+
HANDLE handle;
85+
DWORD temp;
86+
UINT cp = 0;
87+
8588
_Py_BEGIN_SUPPRESS_IPH
86-
valid = isatty(fd);
89+
handle = (HANDLE)_get_osfhandle(fd);
8790
_Py_END_SUPPRESS_IPH
88-
Py_END_ALLOW_THREADS
89-
if (!valid)
91+
if (handle == INVALID_HANDLE_VALUE) {
9092
Py_RETURN_NONE;
93+
}
94+
95+
Py_BEGIN_ALLOW_THREADS
96+
if (GetFileType(handle) == FILE_TYPE_CHAR) {
97+
/* GetConsoleMode() only succeeds for a console handle. */
98+
if (!GetConsoleMode(handle, &temp)) {
99+
/* Assume that access denied implies an output handle. */
100+
if (GetLastError() == ERROR_ACCESS_DENIED) {
101+
cp = GetConsoleOutputCP();
102+
}
103+
}
104+
else if (GetNumberOfConsoleInputEvents(handle, &temp)) {
105+
cp = GetConsoleCP();
106+
}
107+
else {
108+
cp = GetConsoleOutputCP();
109+
}
110+
}
111+
Py_END_ALLOW_THREADS
91112

92-
#ifdef MS_WINDOWS
93-
#ifdef HAVE_WINDOWS_CONSOLE_IO
94-
UINT cp;
95-
if (fd == 0)
96-
cp = GetConsoleCP();
97-
else if (fd == 1 || fd == 2)
98-
cp = GetConsoleOutputCP();
99-
else
100-
cp = 0;
101113
/* GetConsoleCP() and GetConsoleOutputCP() return 0 if the application
102114
has no console */
115+
if (cp == CP_UTF8) {
116+
_Py_DECLARE_STR(utf_8, "utf-8");
117+
return &_Py_STR(utf_8);
118+
}
103119
if (cp == 0) {
104120
Py_RETURN_NONE;
105121
}
106-
107122
return PyUnicode_FromFormat("cp%u", (unsigned int)cp);
108123
#else
124+
int valid;
125+
Py_BEGIN_ALLOW_THREADS
126+
_Py_BEGIN_SUPPRESS_IPH
127+
valid = isatty(fd);
128+
_Py_END_SUPPRESS_IPH
129+
Py_END_ALLOW_THREADS
130+
if (!valid) {
131+
Py_RETURN_NONE;
132+
}
133+
134+
#ifdef MS_WINDOWS
109135
Py_RETURN_NONE;
110-
#endif /* HAVE_WINDOWS_CONSOLE_IO */
111136
#else
112137
if (_PyRuntime.preconfig.utf8_mode) {
113138
_Py_DECLARE_STR(utf_8, "utf-8");
114139
return &_Py_STR(utf_8);
115140
}
116141
return _Py_GetLocaleEncodingObject();
117142
#endif
143+
#endif /* MS_WINDOWS && HAVE_WINDOWS_CONSOLE_IO */
118144
}
119145

120146

0 commit comments

Comments
 (0)