Coverage for /pythoncovmergedfiles/medio/medio/src/idna/tests/fuzz_idna_api.py: 64%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1###### Coverage stub
2import atexit
3import coverage
4cov = coverage.coverage(data_file='.coverage', cover_pylib=True)
5cov.start()
6# Register an exist handler that will print coverage
7def exit_handler():
8 cov.stop()
9 cov.save()
10atexit.register(exit_handler)
11####### End of coverage stub
12#!/usr/bin/env python3
13"""OSS-Fuzz target for the whole-domain and per-label API.
15Each input selects an operation and flag combination from its leading bytes
16and feeds the remainder as the domain or label. The harness asserts the same
17invariants as ``tests/test_idna_properties.py``: only :class:`idna.IDNAError`
18may escape, successful output is bounded ASCII, encoding then decoding then
19encoding again is stable, and UTS #46 remapping is NFC and idempotent.
21OSS-Fuzz builds every ``fuzz_*.py`` it finds in the checkout, so this file
22needs no registration there. To run it locally::
24 pip install atheris .
25 python tests/fuzz_idna_api.py -max_total_time=60
27Any libFuzzer flag is accepted; a crash writes a ``crash-*`` file which can be
28passed back as an argument to reproduce. ``tests/test_idna_fuzz_targets.py``
29smoke-tests this harness in the ordinary test suite without atheris.
30"""
32import sys
33import unicodedata
35import atheris # ty: ignore[unresolved-import]
37with atheris.instrument_imports():
38 import idna
40MAX_INPUT = 1100 # just past idna's 1024-character input cap
41STD3_ASCII = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-.")
44def _only_idnaerror(fn, *args, **kwargs):
45 try:
46 return fn(*args, **kwargs)
47 except idna.IDNAError:
48 return None
51def fuzz_encode(fdp):
52 strict, uts46, std3 = fdp.ConsumeBool(), fdp.ConsumeBool(), fdp.ConsumeBool()
53 s = fdp.ConsumeUnicode(MAX_INPUT)
54 encoded = _only_idnaerror(idna.encode, s, strict=strict, uts46=uts46, std3_rules=std3)
55 if encoded is None:
56 return
57 encoded.decode("ascii")
58 assert len(encoded) <= 254, encoded
59 for label in encoded.rstrip(b".").split(b"."):
60 assert 0 < len(label) <= 63, encoded
61 # Anything encode() produced must decode, and re-encode to itself
62 # (up to ASCII case, since ulabel() lowercases while alabel() does not).
63 decoded = idna.decode(encoded)
64 assert idna.encode(decoded) == encoded.lower(), (encoded, decoded)
65 assert idna.decode(encoded, display=True) == decoded
68def fuzz_decode(fdp):
69 strict, uts46, std3, display = fdp.ConsumeBool(), fdp.ConsumeBool(), fdp.ConsumeBool(), fdp.ConsumeBool()
70 data = fdp.ConsumeUnicode(MAX_INPUT) if fdp.ConsumeBool() else fdp.ConsumeBytes(MAX_INPUT)
71 decoded = _only_idnaerror(idna.decode, data, strict=strict, uts46=uts46, std3_rules=std3, display=display)
72 if decoded is None or not strict or uts46 or display:
73 return
74 # RFC 5891 §5.3: an A-label that decodes must re-encode to itself, so
75 # under strict non-UTS46 processing any ASCII input that decodes is (up
76 # to case) its own encoding. decode()'s length checks are deliberately
77 # lenient (no 63-octet label limit, and the whole-domain limit allows
78 # the trailing-dot octet whether or not one is present, as UTS #46
79 # ToUnicode checks no lengths at all) while encode() enforces both
80 # exactly, so skip inputs past either limit.
81 if isinstance(data, str):
82 if not data.isascii():
83 return
84 data = data.encode("ascii")
85 if idna.valid_string_length(data, data.endswith(b".")) and all(len(label) <= 63 for label in data.split(b".")):
86 assert idna.encode(decoded, strict=True) == data.lower(), (data, decoded)
89def fuzz_uts46_remap(fdp):
90 std3 = fdp.ConsumeBool()
91 s = fdp.ConsumeUnicode(MAX_INPUT)
92 out = _only_idnaerror(idna.uts46_remap, s, std3_rules=std3)
93 if out is None:
94 return
95 assert unicodedata.is_normalized("NFC", out), out
96 # Mapping can expand the input (U+FDFA maps to 18 characters), so the
97 # output may exceed the defensive input-length cap that a second pass
98 # would reject; idempotency only holds for output within the cap.
99 if len(out) <= idna.core._max_input_length:
100 assert idna.uts46_remap(out, std3_rules=std3) == out, out
101 if std3: # UTS #46 §4.1 UseSTD3ASCIIRules
102 assert all(c in STD3_ASCII for c in out if c.isascii()), out
105def fuzz_labels(fdp):
106 label = fdp.ConsumeUnicode(MAX_INPUT)
107 for fn in (
108 idna.alabel,
109 idna.ulabel,
110 idna.check_label,
111 idna.check_bidi,
112 idna.check_hyphen_ok,
113 idna.check_initial_combiner,
114 idna.check_nfc,
115 idna.valid_label_length,
116 ):
117 _only_idnaerror(fn, label)
118 _only_idnaerror(idna.ulabel, label.encode("utf-8", "surrogatepass"))
119 _only_idnaerror(idna.check_label, label.encode("utf-8", "surrogatepass"))
120 encoded = _only_idnaerror(idna.alabel, label)
121 if encoded is not None:
122 assert idna.alabel(idna.ulabel(encoded)) == encoded.lower(), (label, encoded)
125OPERATIONS = (fuzz_encode, fuzz_decode, fuzz_uts46_remap, fuzz_labels)
128def TestOneInput(data):
129 fdp = atheris.FuzzedDataProvider(data)
130 OPERATIONS[fdp.ConsumeIntInRange(0, len(OPERATIONS) - 1)](fdp)
133def main():
134 atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
135 atheris.Fuzz()
138if __name__ == "__main__":
139 main()