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

76 statements  

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. 

14 

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. 

20 

21OSS-Fuzz builds every ``fuzz_*.py`` it finds in the checkout, so this file 

22needs no registration there. To run it locally:: 

23 

24 pip install atheris . 

25 python tests/fuzz_idna_api.py -max_total_time=60 

26 

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""" 

31 

32import sys 

33import unicodedata 

34 

35import atheris # ty: ignore[unresolved-import] 

36 

37with atheris.instrument_imports(): 

38 import idna 

39 

40MAX_INPUT = 1100 # just past idna's 1024-character input cap 

41STD3_ASCII = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-.") 

42 

43 

44def _only_idnaerror(fn, *args, **kwargs): 

45 try: 

46 return fn(*args, **kwargs) 

47 except idna.IDNAError: 

48 return None 

49 

50 

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 

66 

67 

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) 

87 

88 

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 

103 

104 

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) 

123 

124 

125OPERATIONS = (fuzz_encode, fuzz_decode, fuzz_uts46_remap, fuzz_labels) 

126 

127 

128def TestOneInput(data): 

129 fdp = atheris.FuzzedDataProvider(data) 

130 OPERATIONS[fdp.ConsumeIntInRange(0, len(OPERATIONS) - 1)](fdp) 

131 

132 

133def main(): 

134 atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) 

135 atheris.Fuzz() 

136 

137 

138if __name__ == "__main__": 

139 main()