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