Skip to content

Commit

Permalink
Merge branch 'master' into feat/fuzzing-corpus
Browse files Browse the repository at this point in the history
  • Loading branch information
ennamarie19 committed Nov 7, 2023
2 parents 44edb19 + 5c42e8e commit 3523143
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Minor changes:

- Added corpus to fuzzing directory
- Added exclusion of fuzzing corpus in MANIFEST.in
- Augmented fuzzer to optionally convert multiple calendars from a source string

Breaking changes:

Expand All @@ -20,6 +21,9 @@ New features:
Bug fixes:

- ...
- Fixed index error in cal.py when attempting to pop from an empty stack
- Fixed type error in prop.py when attempting to join strings into a byte-string
- Caught Wrong Date Format in ical_fuzzer to resolve fuzzing coverage blocker

5.0.11 (2023-11-03)
-------------------
Expand Down
4 changes: 4 additions & 0 deletions src/icalendar/cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ def from_ical(cls, st, multiple=False):
elif uname == 'END':
# we are done adding properties to this component
# so pop it from the stack and add it to the new top.
if not stack:
# The stack is currently empty, the input must be invalid
raise ValueError('END encountered without an accompanying BEGIN!')

component = stack.pop()
if not stack: # we are at the end
comps.append(component)
Expand Down
37 changes: 28 additions & 9 deletions src/icalendar/fuzzing/ical_fuzzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,50 @@
import atheris
import sys

with atheris.instrument_imports(include=['icalendar']):
from icalendar import Calendar
with atheris.instrument_imports(
include=['icalendar'],
exclude=['pytz', 'six', 'site_packages', 'pkg_resources', 'dateutil']):
import icalendar

_value_error_matches = [
"component", "parse", "Expected", "Wrong date format", "END encountered",
"vDDD", 'recurrence', 'Wrong datetime', 'Offset must', 'Invalid iCalendar'
]


def _fuzz_calendar(cal: icalendar.Calendar, should_walk: bool):
if should_walk:
for event in cal.walk('VEVENT'):
event.to_ical()
else:
cal.to_ical()


@atheris.instrument_func
def TestOneInput(data):
fdp = atheris.FuzzedDataProvider(data)
try:
b = fdp.ConsumeBool()
multiple = fdp.ConsumeBool()
should_walk = fdp.ConsumeBool()

cal = Calendar.from_ical(fdp.ConsumeString(fdp.remaining_bytes()))
cal = icalendar.Calendar.from_ical(fdp.ConsumeString(fdp.remaining_bytes()), multiple=multiple)

if b:
for event in cal.walk('VEVENT'):
event.to_ical().decode('utf-8')
if multiple:
for c in cal:
_fuzz_calendar(c, should_walk)
else:
cal.to_ical()
_fuzz_calendar(cal, should_walk)
except ValueError as e:
if "component" in str(e) or "parse" in str(e) or "Expected" in str(e):
if any(m in str(e) for m in _value_error_matches):
return -1
raise e


def main():
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()


if __name__ == "__main__":
main()

19 changes: 19 additions & 0 deletions src/icalendar/parser_tools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
from typing import Any

SEQUENCE_TYPES = (list, tuple)
DEFAULT_ENCODING = 'utf-8'


def from_unicode(value: Any, encoding='utf-8') -> bytes:
"""
Converts a value to bytes, even if it already is bytes
:param value: The value to convert
:param encoding: The encoding to use in the conversion
:return: The bytes representation of the value
"""
if isinstance(value, bytes):
value = value
elif isinstance(value, str):
try:
value = value.encode(encoding)
except UnicodeEncodeError:
value = value.encode('utf-8', 'replace')
return value


def to_unicode(value, encoding='utf-8'):
"""Converts a value to unicode, even if it is already a unicode string.
"""
Expand Down
17 changes: 13 additions & 4 deletions src/icalendar/prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from icalendar.parser_tools import DEFAULT_ENCODING
from icalendar.parser_tools import SEQUENCE_TYPES
from icalendar.parser_tools import to_unicode
from icalendar.parser_tools import from_unicode
from icalendar.timezone_cache import _timezone_cache
from icalendar.windows_to_olson import WINDOWS_TO_OLSON

Expand All @@ -62,14 +63,12 @@
import re
import time as _time


DURATION_REGEX = re.compile(r'([-+]?)P(?:(\d+)W)?(?:(\d+)D)?'
r'(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$')

WEEKDAY_RULE = re.compile(r'(?P<signal>[+-]?)(?P<relative>[\d]{0,2})'
r'(?P<weekday>[\w]{2})$')


####################################################
# handy tzinfo classes you can use.
#
Expand Down Expand Up @@ -174,6 +173,7 @@ def from_ical(cls, ical):
class vCalAddress(str):
"""This just returns an unquoted string.
"""

def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super().__new__(cls, value)
Expand All @@ -194,6 +194,7 @@ def from_ical(cls, ical):
class vFloat(float):
"""Just a float.
"""

def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
self.params = Parameters()
Expand All @@ -213,6 +214,7 @@ def from_ical(cls, ical):
class vInt(int):
"""Just an int.
"""

def __new__(cls, *args, **kwargs):
self = super().__new__(cls, *args, **kwargs)
self.params = Parameters()
Expand Down Expand Up @@ -250,7 +252,7 @@ def __init__(self, dt_list):
self.dts = vDDD

def to_ical(self):
dts_ical = (dt.to_ical() for dt in self.dts)
dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts)
return b",".join(dts_ical)

@staticmethod
Expand Down Expand Up @@ -288,6 +290,7 @@ def __eq__(self, other):
"""self == other"""
return isinstance(other, vCategory) and self.cats == other.cats


class TimeBase:
"""Make classes with a datetime/date comparable."""

Expand Down Expand Up @@ -366,6 +369,7 @@ def __repr__(self):
"""repr(self)"""
return f"{self.__class__.__name__}({self.dt}, {self.params})"


class vDate(TimeBase):
"""Render and generates iCalendar date format.
"""
Expand Down Expand Up @@ -516,6 +520,7 @@ def dt(self):
"""The time delta for compatibility."""
return self.td


class vPeriod(TimeBase):
"""A precise period of time.
"""
Expand Down Expand Up @@ -588,6 +593,7 @@ def dt(self):
"""Make this cooperate with the other vDDDTypes."""
return (self.start, (self.duration if self.by_duration else self.end))


class vWeekday(str):
"""This returns an unquoted weekday abbrevation.
"""
Expand Down Expand Up @@ -830,11 +836,13 @@ def from_ical(ical):
def __eq__(self, other):
return self.to_ical() == other.to_ical()


class vUTCOffset:
"""Renders itself as a utc offset.
"""

ignore_exceptions = False # if True, and we cannot parse this
ignore_exceptions = False # if True, and we cannot parse this

# component, we will silently ignore
# it, rather than let the exception
# propagate upwards
Expand Down Expand Up @@ -896,6 +904,7 @@ class vInline(str):
has parameters. Conversion of inline values are handled by the Component
class, so no further processing is needed.
"""

def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super().__new__(cls, value)
Expand Down
18 changes: 18 additions & 0 deletions src/icalendar/tests/test_oss_fuzz_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""This file collects errors that the OSS FUZZ build has found."""
from datetime import time
from icalendar import Calendar
from icalendar.prop import vDDDLists

import pytest


def test_stack_is_empty():
"""If we get passed an invalid string, we expect to get a ValueError."""
with pytest.raises(ValueError):
Calendar.from_ical("END:CALENDAR")


def test_vdd_list_type_mismatch():
"""If we pass in a string type, we expect it to be converted to bytes"""
vddd_list = vDDDLists([time(hour=6, minute=6, second=6)])
assert vddd_list.to_ical() == b'060606'

0 comments on commit 3523143

Please sign in to comment.