Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/tests/fuzzed/__init__.py: 88%

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

51 statements  

1"""This is a collection of test files that are generated from the fuzzer. 

2 

3The fuzzer finds the cases in which the icalendar module breaks. 

4These test cases reproduce the failure. 

5Some more tests can be added to make sure that the behavior works properly. 

6""" 

7 

8from __future__ import annotations 

9 

10import inspect 

11from typing import TYPE_CHECKING 

12 

13if TYPE_CHECKING: 

14 from collections.abc import Callable 

15 

16 from icalendar.compatibility import Self 

17 

18 

19class Predicate: 

20 """Check if an exception is expected during fuzzing. 

21 

22 Predicates can be combined with ``&``, ``|`` and ``~``. 

23 

24 Parameters: 

25 func: Returns ``True`` if the exception is expected. 

26 """ 

27 

28 def __init__(self, func: Callable[[BaseException], bool]) -> None: 

29 self.func: Callable[[BaseException], bool] = func 

30 

31 def __call__(self, exception: BaseException) -> bool: 

32 return self.func(exception) 

33 

34 def __and__(self, other: Callable[[BaseException], bool]) -> Self: 

35 return self.__class__(lambda ex: self(ex) and other(ex)) 

36 

37 def __or__(self, other: Callable[[BaseException], bool]) -> Self: 

38 return self.__class__(lambda ex: self(ex) or other(ex)) 

39 

40 def __invert__(self) -> Self: 

41 return self.__class__(lambda ex: not self(ex)) 

42 

43 

44_value_error_matches = [ 

45 "component", 

46 "parse", 

47 "Expected", 

48 "Wrong date format", 

49 "END encountered", 

50 "vDDD", 

51 "recurrence", 

52 "Offset must", 

53 "Invalid iCalendar", 

54 "alue MUST", 

55 "Key name", 

56 "Invalid content line", 

57 "Content line could not be parsed", 

58 "does not exist", 

59 "base 64", 

60 "must use datetime", 

61 "Unknown date type", 

62 "Wrong", 

63 "Start time", 

64 "iCalendar", 

65 "recurrence", 

66 "float, float", 

67 "utc offset", 

68 "parent", 

69 "MUST be a datetime", 

70 "Invalid month:", 

71 "must have exactly", # vCard field count validation (ADR, N) 

72 "must have at least", # vCard ORG minimum field validation 

73 "not enough values to unpack", # dateutil rejects a malformed VTIMEZONE content line 

74 "Content line can not contain unescaped new line characters.", # Contentline after #1559 

75] 

76 

77# The current check: a ValueError or TypeError with one of the substrings above. 

78# See https://github.com/collective/icalendar/issues/1759 

79_value_error_message = Predicate( 

80 lambda e: ( 

81 isinstance(e, (ValueError, TypeError)) 

82 and any(m in str(e) for m in _value_error_matches) 

83 ) 

84) 

85 

86# An exception is ignored during fuzzing if any of these predicates matches. 

87_expected_exceptions: list[Predicate] = [ 

88 _value_error_message, 

89] 

90 

91 

92def is_expected_exception(exception: BaseException) -> bool: 

93 """Check if the fuzzer can ignore this exception. 

94 

95 Parameters: 

96 exception: The exception raised for a fuzzed input. 

97 

98 Returns: 

99 ``True`` if any predicate in the list of expected exceptions matches. 

100 """ 

101 return any(predicate(exception) for predicate in _expected_exceptions) 

102 

103 

104def format_fuzz_log( 

105 from_ical, multiple: bool, should_walk: bool, calendar_string: str | bytes 

106) -> str: 

107 """Format the log entry for fuzzed test case extraction. 

108 

109 Outputs entrypoint name, parameter values, and base64-encoded calendar content. 

110 """ 

111 import base64 

112 

113 try: 

114 # print the ICS file for the test case extraction 

115 # see https://stackoverflow.com/a/27367173/1320237 

116 encoded = base64.b64encode( 

117 calendar_string.encode("UTF-8", "surrogateescape") 

118 if isinstance(calendar_string, str) 

119 else calendar_string 

120 ).decode("ASCII") 

121 except UnicodeEncodeError as e: 

122 encoded = str(e) 

123 return ( 

124 f"{_get_name_of_caller()} {from_ical.__qualname__} multiple={multiple} " 

125 f"should_walk={should_walk} {encoded}" 

126 ) 

127 

128 

129def _get_name_of_caller(skip: int = 0) -> str: 

130 stack = inspect.stack() 

131 relevant_index = skip + 2 # ignore own and caller frame 

132 if relevant_index < len(stack) and (frame := stack[relevant_index]): 

133 return frame.function 

134 return "<unknown source>" 

135 

136 

137def fuzz_v1_calendar( 

138 from_ical, calendar_string: str, multiple: bool, should_walk: bool 

139): 

140 """Take a from_ical function and reproduce the error. 

141 

142 The calendar_string is a fuzzed input. 

143 """ 

144 print(format_fuzz_log(from_ical, multiple, should_walk, calendar_string)) 

145 try: 

146 cal = from_ical(calendar_string, multiple=multiple) 

147 

148 if not multiple: 

149 cal = [cal] 

150 for c in cal: 

151 if should_walk: 

152 for event in c.walk("VEVENT"): 

153 event.to_ical() 

154 else: 

155 c.to_ical() 

156 except Exception as e: 

157 if is_expected_exception(e): 

158 return -1 

159 raise