Skip to content

Commit 2bcb1dd

Browse files
committed
Port tests/decode.js to Python (issue #2)
Add the full contents of tests/decode.js to tests/test_decode.py: %xx decoding, invalid-sequence passthrough, the reserved-set variants, and the complete 36-case binary UTF-8 sample matrix (validity judged by a strict UTF-8 oracle that mirrors decodeURIComponent). Keep the pre-existing decode tests unchanged.
1 parent 524d2ed commit 2bcb1dd

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

‎tests/test_decode.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import re
2+
import urllib.parse
3+
4+
import pytest
5+
16
from 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\ufffdE"
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

Comments
 (0)