Skip to content

Commit 28bf6ab

Browse files
[3.9] bpo-42318: Fix support of non-BMP characters in Tkinter on macOS (GH-23281). (GH-23784)
(cherry picked from commit a26215d)
1 parent 99d37a0 commit 28bf6ab

File tree

3 files changed

+94
-7
lines changed

3 files changed

+94
-7
lines changed

Lib/test/test_tcl.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import locale
23
import re
34
import subprocess
45
import sys
@@ -59,6 +60,10 @@ def test_eval_null_in_result(self):
5960
tcl = self.interp
6061
self.assertEqual(tcl.eval('set a "a\\0b"'), 'a\x00b')
6162

63+
def test_eval_surrogates_in_result(self):
64+
tcl = self.interp
65+
self.assertIn(tcl.eval(r'set a "<\ud83d\udcbb>"'), '<\U0001f4bb>')
66+
6267
def testEvalException(self):
6368
tcl = self.interp
6469
self.assertRaises(TclError,tcl.eval,'set a')
@@ -191,29 +196,48 @@ def test_getboolean(self):
191196

192197
def testEvalFile(self):
193198
tcl = self.interp
194-
with open(support.TESTFN, 'w') as f:
195-
self.addCleanup(support.unlink, support.TESTFN)
199+
filename = support.TESTFN_ASCII
200+
self.addCleanup(support.unlink, filename)
201+
with open(filename, 'w') as f:
196202
f.write("""set a 1
197203
set b 2
198204
set c [ expr $a + $b ]
199205
""")
200-
tcl.evalfile(support.TESTFN)
206+
tcl.evalfile(filename)
201207
self.assertEqual(tcl.eval('set a'),'1')
202208
self.assertEqual(tcl.eval('set b'),'2')
203209
self.assertEqual(tcl.eval('set c'),'3')
204210

205211
def test_evalfile_null_in_result(self):
206212
tcl = self.interp
207-
with open(support.TESTFN, 'w') as f:
208-
self.addCleanup(support.unlink, support.TESTFN)
213+
filename = support.TESTFN_ASCII
214+
self.addCleanup(support.unlink, filename)
215+
with open(filename, 'w') as f:
209216
f.write("""
210217
set a "a\0b"
211218
set b "a\\0b"
212219
""")
213-
tcl.evalfile(support.TESTFN)
220+
tcl.evalfile(filename)
214221
self.assertEqual(tcl.eval('set a'), 'a\x00b')
215222
self.assertEqual(tcl.eval('set b'), 'a\x00b')
216223

224+
def test_evalfile_surrogates_in_result(self):
225+
tcl = self.interp
226+
encoding = tcl.call('encoding', 'system')
227+
self.addCleanup(tcl.call, 'encoding', 'system', encoding)
228+
tcl.call('encoding', 'system', 'utf-8')
229+
230+
filename = support.TESTFN_ASCII
231+
self.addCleanup(support.unlink, filename)
232+
with open(filename, 'wb') as f:
233+
f.write(b"""
234+
set a "<\xed\xa0\xbd\xed\xb2\xbb>"
235+
set b "<\\ud83d\\udcbb>"
236+
""")
237+
tcl.evalfile(filename)
238+
self.assertEqual(tcl.eval('set a'), '<\U0001f4bb>')
239+
self.assertEqual(tcl.eval('set b'), '<\U0001f4bb>')
240+
217241
def testEvalFileException(self):
218242
tcl = self.interp
219243
filename = "doesnotexists"
@@ -436,6 +460,11 @@ def passValue(value):
436460
self.assertEqual(passValue('str\x00ing\u20ac'), 'str\x00ing\u20ac')
437461
self.assertEqual(passValue('str\x00ing\U0001f4bb'),
438462
'str\x00ing\U0001f4bb')
463+
if sys.platform != 'win32':
464+
self.assertEqual(passValue('<\udce2\udc82\udcac>'),
465+
'<\u20ac>')
466+
self.assertEqual(passValue('<\udced\udca0\udcbd\udced\udcb2\udcbb>'),
467+
'<\U0001f4bb>')
439468
self.assertEqual(passValue(b'str\x00ing'),
440469
b'str\x00ing' if self.wantobjects else 'str\x00ing')
441470
self.assertEqual(passValue(b'str\xc0\x80ing'),
@@ -495,6 +524,9 @@ def float_eq(actual, expected):
495524
check('string\xbd')
496525
check('string\u20ac')
497526
check('string\U0001f4bb')
527+
if sys.platform != 'win32':
528+
check('<\udce2\udc82\udcac>', '<\u20ac>')
529+
check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>')
498530
check('')
499531
check(b'string', 'string')
500532
check(b'string\xe2\x82\xac', 'string\xe2\x82\xac')
@@ -538,6 +570,8 @@ def test_splitlist(self):
538570
('a \u20ac', ('a', '\u20ac')),
539571
('a \U0001f4bb', ('a', '\U0001f4bb')),
540572
(b'a \xe2\x82\xac', ('a', '\u20ac')),
573+
(b'a \xf0\x9f\x92\xbb', ('a', '\U0001f4bb')),
574+
(b'a \xed\xa0\xbd\xed\xb2\xbb', ('a', '\U0001f4bb')),
541575
(b'a\xc0\x80b c\xc0\x80d', ('a\x00b', 'c\x00d')),
542576
('a {b c}', ('a', 'b c')),
543577
(r'a b\ c', ('a', 'b c')),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed support of non-BMP characters in :mod:`tkinter` on macOS.

Modules/_tkinter.c

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ unicodeFromTclStringAndSize(const char *s, Py_ssize_t size)
395395

396396
char *buf = NULL;
397397
PyErr_Clear();
398-
/* Tcl encodes null character as \xc0\x80 */
398+
/* Tcl encodes null character as \xc0\x80.
399+
https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 */
399400
if (memchr(s, '\xc0', size)) {
400401
char *q;
401402
const char *e = s + size;
@@ -419,6 +420,57 @@ unicodeFromTclStringAndSize(const char *s, Py_ssize_t size)
419420
if (buf != NULL) {
420421
PyMem_Free(buf);
421422
}
423+
if (r == NULL || PyUnicode_KIND(r) == PyUnicode_1BYTE_KIND) {
424+
return r;
425+
}
426+
427+
/* In CESU-8 non-BMP characters are represented as a surrogate pair,
428+
like in UTF-16, and then each surrogate code point is encoded in UTF-8.
429+
https://en.wikipedia.org/wiki/CESU-8 */
430+
Py_ssize_t len = PyUnicode_GET_LENGTH(r);
431+
Py_ssize_t i, j;
432+
/* All encoded surrogate characters start with \xED. */
433+
i = PyUnicode_FindChar(r, 0xdcED, 0, len, 1);
434+
if (i == -2) {
435+
Py_DECREF(r);
436+
return NULL;
437+
}
438+
if (i == -1) {
439+
return r;
440+
}
441+
Py_UCS4 *u = PyUnicode_AsUCS4Copy(r);
442+
Py_DECREF(r);
443+
if (u == NULL) {
444+
return NULL;
445+
}
446+
Py_UCS4 ch;
447+
for (j = i; i < len; i++, u[j++] = ch) {
448+
Py_UCS4 ch1, ch2, ch3, high, low;
449+
/* Low surrogates U+D800 - U+DBFF are encoded as
450+
\xED\xA0\x80 - \xED\xAF\xBF. */
451+
ch1 = ch = u[i];
452+
if (ch1 != 0xdcED) continue;
453+
ch2 = u[i + 1];
454+
if (!(0xdcA0 <= ch2 && ch2 <= 0xdcAF)) continue;
455+
ch3 = u[i + 2];
456+
if (!(0xdc80 <= ch3 && ch3 <= 0xdcBF)) continue;
457+
high = 0xD000 | ((ch2 & 0x3F) << 6) | (ch3 & 0x3F);
458+
assert(Py_UNICODE_IS_HIGH_SURROGATE(high));
459+
/* High surrogates U+DC00 - U+DFFF are encoded as
460+
\xED\xB0\x80 - \xED\xBF\xBF. */
461+
ch1 = u[i + 3];
462+
if (ch1 != 0xdcED) continue;
463+
ch2 = u[i + 4];
464+
if (!(0xdcB0 <= ch2 && ch2 <= 0xdcBF)) continue;
465+
ch3 = u[i + 5];
466+
if (!(0xdc80 <= ch3 && ch3 <= 0xdcBF)) continue;
467+
low = 0xD000 | ((ch2 & 0x3F) << 6) | (ch3 & 0x3F);
468+
assert(Py_UNICODE_IS_HIGH_SURROGATE(high));
469+
ch = Py_UNICODE_JOIN_SURROGATES(high, low);
470+
i += 5;
471+
}
472+
r = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, u, j);
473+
PyMem_Free(u);
422474
return r;
423475
}
424476

0 commit comments

Comments
 (0)