Skip to content

Commit 6aeb041

Browse files
Don't consider Unicode codepoint attributes outside RFC 3454
Due to a bug, some Unicode codepoint attributes were considered for characters not yet defined in Unicode 3.2.0 or attributes which changed in later Unicode versions. RFC 3454 (StringPrep) requires using Unicode 3.2.0 strictly. Co-authored-by: Stan Ulbrych <89152624+stanfromireland@users.noreply.github.com>
1 parent c3aefdb commit 6aeb041

7 files changed

Lines changed: 1907 additions & 1348 deletions

File tree

Lib/stringprep.py

Lines changed: 258 additions & 66 deletions
Large diffs are not rendered by default.

Lib/test/test_codecs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,12 @@ def test_builtin_encode(self):
16951695
self.assertEqual("pyth\xf6n.org".encode("idna"), b"xn--pythn-mua.org")
16961696
self.assertEqual("pyth\xf6n.org.".encode("idna"), b"xn--pythn-mua.org.")
16971697

1698+
def test_new_unicode_case_folding(self):
1699+
self.assertEqual("ᎠᎠ".encode("idna"), b"xn--58da")
1700+
self.assertEqual("Ⴀ.".encode("idna"), b"xn--7md.")
1701+
self.assertEqual("Ӏ.example".encode("idna"), b"xn--d5a.example")
1702+
self.assertEqual("Ↄ.example.".encode("idna"), b"xn--q5g.example.")
1703+
16981704
def test_builtin_encode_invalid(self):
16991705
for case, expected in self.invalid_encode_testcases:
17001706
with self.subTest(case=case, expected=expected):

Lib/test/test_unicodedata.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def test_category(self):
321321
self.assertRaises(TypeError, self.db.category, 'xx')
322322

323323
def test_bidirectional(self):
324-
self.assertEqual(self.db.bidirectional('\uFFFE'), 'BN')
324+
self.assertEqual(self.db.bidirectional('\uFFFE'), '' if self.old else 'BN')
325325
self.assertEqual(self.db.bidirectional(' '), 'WS')
326326
self.assertEqual(self.db.bidirectional('A'), 'L')
327327
self.assertEqual(self.db.bidirectional('\U00020000'), 'L')
@@ -350,15 +350,13 @@ def test_bidirectional(self):
350350
self.assertRaises(TypeError, self.db.bidirectional, 'xx')
351351

352352
def test_bidirectional_unassigned(self):
353-
if self.old:
354-
return
355-
self.assertEqual(self.db.bidirectional('\u0378'), 'L')
356-
self.assertEqual(self.db.bidirectional('\u077F'), 'AL')
357-
self.assertEqual(self.db.bidirectional('\u20CF'), 'ET')
358-
self.assertEqual(self.db.bidirectional('\u0590'), 'R')
359-
self.assertEqual(self.db.bidirectional('\uFFFF'), 'BN')
360-
self.assertEqual(self.db.bidirectional('\U0001FFFE'), 'BN')
361-
self.assertEqual(self.db.bidirectional('\U00010D01'), 'AL')
353+
self.assertEqual(self.db.bidirectional('\u0378'), '' if self.old else 'L')
354+
self.assertEqual(self.db.bidirectional('\u077F'), '' if self.old else 'AL')
355+
self.assertEqual(self.db.bidirectional('\u20CF'), '' if self.old else 'ET')
356+
self.assertEqual(self.db.bidirectional('\u0590'), '' if self.old else 'R')
357+
self.assertEqual(self.db.bidirectional('\uFFFF'), '' if self.old else 'BN')
358+
self.assertEqual(self.db.bidirectional('\U0001FFFE'), '' if self.old else 'BN')
359+
self.assertEqual(self.db.bidirectional('\U00010D01'), '' if self.old else 'AL')
362360

363361
def test_decomposition(self):
364362
self.assertEqual(self.db.decomposition('\uFFFE'),'')
@@ -1104,9 +1102,9 @@ def test_block_invalid_input(self):
11041102
class Unicode_3_2_0_FunctionsTest(unittest.TestCase, BaseUnicodeFunctionsTest):
11051103
db = unicodedata.ucd_3_2_0
11061104
old = True
1107-
expectedchecksum = ('cb5bbbd1f55b67371e18222b90a8e21c87f16b72'
1105+
expectedchecksum = ('883824cb6c0ccf994e4451ebf281e2d6d479af47'
11081106
if quicktest else
1109-
'74936dffe949d99203a47e6a66565b2fc337bae7')
1107+
'68cd01e2c680b851c1fcab012efb5635b2229c2b')
11101108

11111109

11121110
class UnicodeMiscTest(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Change the :mod:`stringprep` module and :mod:`encoding.idna` codec to not
2+
consider Unicode codepoint attributes beyond those defined in :rfc:`3454`.

Modules/unicodedata_db.h

Lines changed: 1564 additions & 1243 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/unicode/makeunicodedata.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import dataclasses
3030
import os
31+
import subprocess
3132
import sys
3233
import re
3334
import zipfile
@@ -169,6 +170,7 @@ def maketables(trace=0):
169170
makeunicodename(unicode, trace)
170171
makeunicodedata(unicode, trace)
171172
makeunicodetype(unicode, trace)
173+
makestringprep()
172174

173175

174176
# --------------------------------------------------------------------
@@ -810,6 +812,19 @@ def makeunicodename(unicode, trace):
810812
fprint(' "%s",' % prefix)
811813
fprint('};')
812814

815+
816+
def makestringprep():
817+
FILE = "Lib/stringprep.py"
818+
819+
print("--- Preparing", FILE, "...")
820+
821+
MKSTRINGPREP = "Tools/unicode/mkstringprep.py"
822+
823+
with open(FILE, "w") as f:
824+
f.truncate()
825+
subprocess.check_call([sys.executable, MKSTRINGPREP], stdout=f)
826+
827+
813828
def merge_old_version(version, new, old):
814829
# Changes to exclusion file not implemented yet
815830
if old.exclusions != new.exclusions:
@@ -831,6 +846,10 @@ def merge_old_version(version, new, old):
831846
# Characters unassigned in the new version ought to
832847
# be unassigned in the old one
833848
assert old.table[i] is None
849+
# Without a change record the old view would fall
850+
# through to the new default, so record the old value
851+
if old.bidi_classes[i] != new.bidi_classes[i]:
852+
bidir_changes[i] = BIDIRECTIONAL_NAMES.index(old.bidi_classes[i] or '')
834853
continue
835854
# check characters unassigned in the old version
836855
if old.table[i] is None:

Tools/unicode/mkstringprep.py

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import re
2-
from unicodedata import ucd_3_2_0 as unicodedata
2+
import unicodedata as unicodedata_current
3+
from unicodedata import ucd_3_2_0 as unicodedata_320
34

45
def gen_category(cats):
56
for i in range(0, 0x110000):
6-
if unicodedata.category(chr(i)) in cats:
7-
yield(i)
7+
if unicodedata_320.category(chr(i)) in cats:
8+
yield i
89

910
def gen_bidirectional(cats):
1011
for i in range(0, 0x110000):
11-
if unicodedata.bidirectional(chr(i)) in cats:
12-
yield(i)
12+
if unicodedata_320.bidirectional(chr(i)) in cats:
13+
yield i
1314

1415
def compact_set(l):
1516
single = []
@@ -116,10 +117,18 @@ def compact_set(l):
116117
and mappings, for which a mapping function is provided.
117118
\"\"\"
118119
119-
from unicodedata import ucd_3_2_0 as unicodedata
120+
# This check asserts that mkstringprep.py has been run
121+
# when unicodedata is modified to ensure conformant behavior.
122+
import unicodedata
120123
""")
121124

122-
print("assert unicodedata.unidata_version == %r" % (unicodedata.unidata_version,))
125+
print("assert unicodedata.unidata_version == %r" % (unicodedata_current.unidata_version,))
126+
127+
print("""
128+
from unicodedata import ucd_3_2_0 as unicodedata_320
129+
""")
130+
131+
print("assert unicodedata_320.unidata_version == %r" % (unicodedata_320.unidata_version,))
123132

124133
# A.1 is the table of unassigned characters
125134
# XXX Plane 15 PUA is listed as unassigned in Python.
@@ -139,7 +148,7 @@ def compact_set(l):
139148

140149
print("""
141150
def in_table_a1(code):
142-
if unicodedata.category(code) != 'Cn': return False
151+
if unicodedata_320.category(code) != 'Cn': return False
143152
c = ord(code)
144153
if 0xFDD0 <= c < 0xFDF0: return False
145154
return (c & 0xFFFF) not in (0xFFFE, 0xFFFF)
@@ -172,21 +181,33 @@ def in_table_b1(code):
172181

173182
# B.3 is mostly Python's .lower, except for a number
174183
# of special cases, e.g. considering canonical forms.
184+
# To enforce Unicode 3.2.0 behavior of .lower instead of
185+
# whatever Unicode version is included with Python we
186+
# add unassigned or newly case-folding codepoints to
187+
# the exception map, too.
175188

176189
b3_exceptions = {}
177190

178191
for k,v in table_b2.items():
179192
if list(map(ord, chr(k).lower())) != v:
180193
b3_exceptions[k] = "".join(map(chr,v))
194+
for cp in range(0x110000):
195+
ch = chr(cp)
196+
# Assigned in current Unicode version
197+
# and supports case folding, but not
198+
# explicitly in B.2 or B.3 tables.
199+
if (unicodedata_current.category(ch) != "Cn"
200+
and ch.lower() != ch
201+
and cp not in table_b2
202+
and cp not in table_b3):
203+
b3_exceptions[cp] = ch # Identity.
181204

182205
b3 = sorted(b3_exceptions.items())
183206

184207
print("""
185208
b3_exceptions = {""")
186209
for i, kv in enumerate(b3):
187-
print("0x%x:%a," % kv, end=' ')
188-
if i % 4 == 3:
189-
print()
210+
print("0x%x:%a," % kv, end='\n' if i % 4 == 3 else ' ')
190211
print("}")
191212

192213
print("""
@@ -207,9 +228,9 @@ def map_table_b3(code):
207228

208229
def map_table_b2(a):
209230
al = map_table_b3(a)
210-
b = unicodedata.normalize("NFKC", al)
231+
b = unicodedata_320.normalize("NFKC", al)
211232
bl = "".join([map_table_b3(ch) for ch in b])
212-
c = unicodedata.normalize("NFKC", bl)
233+
c = unicodedata_320.normalize("NFKC", bl)
213234
if b != c:
214235
return c
215236
else:
@@ -226,9 +247,9 @@ def map_table_b2(a):
226247
print("""
227248
def map_table_b2(a):
228249
al = map_table_b3(a)
229-
b = unicodedata.normalize("NFKC", al)
250+
b = unicodedata_320.normalize("NFKC", al)
230251
bl = "".join([map_table_b3(ch) for ch in b])
231-
c = unicodedata.normalize("NFKC", bl)
252+
c = unicodedata_320.normalize("NFKC", bl)
232253
if b != c:
233254
return c
234255
else:
@@ -251,16 +272,16 @@ def in_table_c11(code):
251272
del tables[0]
252273
assert name == "C.1.2"
253274

254-
# table = set(table.keys())
255-
# Zs = set(gen_category(["Zs"])) - {0x20}
256-
# assert Zs == table
275+
table = set(table.keys())
276+
Zs = set(gen_category(["Zs"])) - {0x20}
277+
assert Zs == table
257278

258279
print("""
259280
def in_table_c12(code):
260-
return unicodedata.category(code) == "Zs" and code != " "
281+
return unicodedata_320.category(code) == "Zs" and code != " "
261282
262283
def in_table_c11_c12(code):
263-
return unicodedata.category(code) == "Zs"
284+
return unicodedata_320.category(code) == "Zs"
264285
""")
265286

266287
# C.2.1 ASCII control characters
@@ -275,7 +296,7 @@ def in_table_c11_c12(code):
275296

276297
print("""
277298
def in_table_c21(code):
278-
return ord(code) < 128 and unicodedata.category(code) == "Cc"
299+
return ord(code) < 128 and unicodedata_320.category(code) == "Cc"
279300
""")
280301

281302
# C.2.2 Non-ASCII control characters. It also includes
@@ -295,11 +316,11 @@ def in_table_c21(code):
295316
def in_table_c22(code):
296317
c = ord(code)
297318
if c < 128: return False
298-
if unicodedata.category(code) == "Cc": return True
319+
if unicodedata_320.category(code) == "Cc": return True
299320
return c in c22_specials
300321
301322
def in_table_c21_c22(code):
302-
return unicodedata.category(code) == "Cc" or \\
323+
return unicodedata_320.category(code) == "Cc" or \\
303324
ord(code) in c22_specials
304325
""")
305326

@@ -313,7 +334,7 @@ def in_table_c21_c22(code):
313334

314335
print("""
315336
def in_table_c3(code):
316-
return unicodedata.category(code) == "Co"
337+
return unicodedata_320.category(code) == "Co"
317338
""")
318339

319340
# C.4 Non-character code points, xFFFE, xFFFF
@@ -346,7 +367,7 @@ def in_table_c4(code):
346367

347368
print("""
348369
def in_table_c5(code):
349-
return unicodedata.category(code) == "Cs"
370+
return unicodedata_320.category(code) == "Cs"
350371
""")
351372

352373
# C.6 Inappropriate for plain text
@@ -411,7 +432,7 @@ def in_table_c9(code):
411432

412433
print("""
413434
def in_table_d1(code):
414-
return unicodedata.bidirectional(code) in ("R","AL")
435+
return unicodedata_320.bidirectional(code) in ("R","AL")
415436
""")
416437

417438
# D.2 Characters with bidirectional property "L"
@@ -424,5 +445,5 @@ def in_table_d1(code):
424445

425446
print("""
426447
def in_table_d2(code):
427-
return unicodedata.bidirectional(code) == "L"
448+
return unicodedata_320.bidirectional(code) == "L"
428449
""")

0 commit comments

Comments
 (0)