Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1362.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid implicit file reads from string calendar input in :meth:`~icalendar.cal.component.Component.from_ical` by removing support for passing file paths as strings. Use :class:`pathlib.Path` instead to read from a file. @uwezkhan
16 changes: 5 additions & 11 deletions src/icalendar/cal/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,12 +490,14 @@ def property_items(
@overload
@classmethod
def from_ical(
cls, st: str | bytes, multiple: Literal[False] = False
cls, st: str | bytes | Path, multiple: Literal[False] = False
) -> Component: ...

@overload
@classmethod
def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ...
def from_ical(
cls, st: str | bytes | Path, multiple: Literal[True]
) -> list[Component]: ...

@classmethod
def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
Expand All @@ -512,7 +514,7 @@ def from_ical(

Parameters:
st: iCalendar data as bytes or string, or a path to an iCalendar file as
:class:`pathlib.Path` or string.
:class:`pathlib.Path`.
multiple: If ``True``, returns list. If ``False``, returns single component.

Returns:
Expand All @@ -523,14 +525,6 @@ def from_ical(
"""
if isinstance(st, Path):
st = st.read_bytes()
elif isinstance(st, str) and "\n" not in st and "\r" not in st:
path = Path(st)
try:
is_file = path.is_file()
except OSError:
is_file = False
if is_file:
st = path.read_bytes()
parser = cls._get_ical_parser(st)
components = parser.parse()
if multiple:
Expand Down
23 changes: 18 additions & 5 deletions src/icalendar/tests/test_issue_756_read_calendar_from_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,25 @@ def test_reading_cal_from_path(dummy_cal):
assert actual_cal.to_ical() == expected_cal.to_ical()


def test_reading_cal_from_string_path(dummy_cal):
"""Test reading a calendar from a valid string path."""
expected_cal, path = dummy_cal
actual_cal = Calendar.from_ical(str(path))
def test_string_path_is_parsed_as_calendar_data(dummy_cal):
"""Test that string input is not interpreted as a filesystem path."""
_, path = dummy_cal

assert actual_cal.to_ical() == expected_cal.to_ical()
with pytest.raises(ValueError) as exc_info:
Calendar.from_ical(str(path))

assert "BEGIN:VCALENDAR" not in str(exc_info.value)


def test_string_path_does_not_disclose_file_contents(tmp_path: Path):
"""Test that parsing a string path does not read from the filesystem."""
path = tmp_path / "secret.txt"
path.write_text("SECRET-CONTENT-WITHOUT-NEWLINE", encoding="utf-8")

with pytest.raises(ValueError) as exc_info:
Calendar.from_ical(str(path))

assert "SECRET-CONTENT-WITHOUT-NEWLINE" not in str(exc_info.value)


def test_reading_cal_from_long_string(dummy_cal):
Expand Down