1+ import re
2+ import urllib .parse
3+
4+ import pytest
5+
16from mdurl import decode
27
38
@@ -14,3 +19,117 @@ def test_decode_invalid_utf8():
1419 decode ("https://host.invalid/%F1%81%d1%45" )
1520 == "https://host.invalid/\ufffd \ufffd \ufffd E"
1621 )
22+
23+
24+ def _percent_encode (binary ):
25+ # Mirror the ``encodeBinary`` helper in ``tests/decode.js``: strip all
26+ # whitespace, then convert each trailing 8-bit chunk to a two-digit
27+ # lowercase hex escape, prepending to the result.
28+ binary = re .sub (r"\s+" , "" , binary )
29+ result = ""
30+ while binary :
31+ chunk = binary [- 8 :]
32+ result = "%" + format (int (chunk , 2 ), "02x" ) + result
33+ binary = binary [:- 8 ]
34+ return result
35+
36+
37+ def _decode_uri_component (percent ):
38+ # Mimic JavaScript ``decodeURIComponent``: strict UTF-8 decoding, which
39+ # raises ``UnicodeDecodeError`` on malformed sequences just like
40+ # ``decodeURIComponent`` throws URIError on them.
41+ return urllib .parse .unquote_to_bytes (percent ).decode ("utf-8" , "strict" )
42+
43+
44+ # Binary UTF-8 sample matrix ported verbatim from tests/decode.js. As in the
45+ # JS test, the True/False marks are documentation only: each sample's
46+ # validity is decided by the strict UTF-8 oracle above (the JS test likewise
47+ # probes decodeURIComponent directly and never reads the marks).
48+ SAMPLES = {
49+ "00000000" : True ,
50+ "01010101" : True ,
51+ "01111111" : True ,
52+ # invalid as 1st byte
53+ "10000000" : True ,
54+ "10111111" : True ,
55+ # invalid sequences, 2nd byte should be >= 0x80
56+ "11000111 01010101" : False ,
57+ "11100011 01010101" : False ,
58+ "11110001 01010101" : False ,
59+ # invalid sequences, 2nd byte should be < 0xc0
60+ "11000111 11000000" : False ,
61+ "11100011 11000000" : False ,
62+ "11110001 11000000" : False ,
63+ # invalid 3rd byte
64+ "11100011 10010101 01010101" : False ,
65+ "11110001 10010101 01010101" : False ,
66+ # invalid 4th byte
67+ "11110001 10010101 10010101 01010101" : False ,
68+ # valid sequences
69+ "11000111 10101010" : True ,
70+ "11100011 10101010 10101010" : True ,
71+ "11110001 10101010 10101010 10101010" : True ,
72+ # minimal chars with given length
73+ "11000010 10000000" : True ,
74+ "11100000 10100000 10000000" : True ,
75+ # impossible sequences
76+ "11000001 10111111" : False ,
77+ "11100000 10011111 10111111" : False ,
78+ "11000001 10000000" : False ,
79+ "11100000 10010000 10000000" : False ,
80+ # maximum chars with given length
81+ "11011111 10111111" : True ,
82+ "11101111 10111111 10111111" : True ,
83+ "11110000 10010000 10000000 10000000" : True ,
84+ "11110000 10010000 10001111 10001111" : True ,
85+ "11110100 10001111 10110000 10000000" : True ,
86+ "11110100 10001111 10111111 10111111" : True ,
87+ # too low
88+ "11110000 10001111 10111111 10111111" : False ,
89+ # too high
90+ "11110100 10010000 10000000 10000000" : False ,
91+ "11110100 10011111 10111111 10111111" : False ,
92+ # surrogate range
93+ "11101101 10011111 10111111" : True ,
94+ "11101101 10100000 10000000" : False ,
95+ "11101101 10111111 10111111" : False ,
96+ "11101110 10000000 10000000" : True ,
97+ }
98+
99+
100+ def test_decode_percent ():
101+ assert decode ("x%20xx%20%2520" ) == "x xx %20"
102+
103+
104+ def test_decode_invalid_sequences ():
105+ assert decode ("%2g%z1%%" ) == "%2g%z1%%"
106+
107+
108+ def test_decode_reserved_set_percent ():
109+ assert decode ("%20%25%20" , "%" ) == " %25 "
110+
111+
112+ def test_decode_reserved_set_space ():
113+ assert decode ("%20%25%20" , " " ) == "%20%%20"
114+
115+
116+ def test_decode_reserved_set_both ():
117+ assert decode ("%20%25%20" , " %" ) == "%20%25%20"
118+
119+
120+ @pytest .mark .parametrize ("binary" , SAMPLES .keys ())
121+ def test_decode_utf8_sample_matrix (binary ):
122+ percent = _percent_encode (binary )
123+ try :
124+ expected = _decode_uri_component (percent )
125+ error = None
126+ except UnicodeDecodeError :
127+ error = True
128+
129+ result = decode (percent )
130+
131+ if error :
132+ assert "\ufffd " in result
133+ else :
134+ assert result == expected
135+ assert "\ufffd " not in result
0 commit comments