From bb381c9d18d9a11e139903fe5a7ee8d20ae054bc Mon Sep 17 00:00:00 2001 From: Tim Su Date: Sat, 23 Jan 2021 16:42:58 -0800 Subject: [PATCH 1/4] Parse multiple exceptions to rrule --- lib/icalendar/util/deserialize.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/icalendar/util/deserialize.ex b/lib/icalendar/util/deserialize.ex index a6d1443..72cfb54 100644 --- a/lib/icalendar/util/deserialize.ex +++ b/lib/icalendar/util/deserialize.ex @@ -102,12 +102,16 @@ defmodule ICalendar.Util.Deserialize do end def parse_attr( - %Property{key: "EXDATE", value: exdate, params: params}, + %Property{key: "EXDATE", value: new_exdates, params: params}, acc ) do exdates = Map.get(acc, :exdates, []) - {:ok, timestamp} = to_date(exdate, params) - %{acc | exdates: [timestamp | exdates]} + new_dates = String.split(new_exdates, ",") + |> Enum.map(fn exdate -> + {:ok, timestamp} = to_date(exdate, params) + timestamp + end) + %{acc | exdates: Enum.concat(new_dates, exdates)} end def parse_attr( From bc74164bc6fff5f944833741e9d8d48826c605ba Mon Sep 17 00:00:00 2001 From: Tim Su Date: Sat, 23 Jan 2021 19:41:38 -0800 Subject: [PATCH 2/4] Allow passing a flag to ignore event parsing errors or raise. --- lib/icalendar.ex | 2 +- lib/icalendar/deserialize.ex | 34 +++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/icalendar.ex b/lib/icalendar.ex index 3d0dc31..18c4736 100644 --- a/lib/icalendar.ex +++ b/lib/icalendar.ex @@ -5,7 +5,7 @@ defmodule ICalendar do defstruct events: [] defdelegate to_ics(events, options \\ []), to: ICalendar.Serialize - defdelegate from_ics(events), to: ICalendar.Deserialize + defdelegate from_ics(events, options \\ []), to: ICalendar.Deserialize @doc """ To create a Phoenix/Plug controller and view that output ics format: diff --git a/lib/icalendar/deserialize.ex b/lib/icalendar/deserialize.ex index 7619705..e778fb0 100644 --- a/lib/icalendar/deserialize.ex +++ b/lib/icalendar/deserialize.ex @@ -1,40 +1,52 @@ defprotocol ICalendar.Deserialize do - def from_ics(ics) + def from_ics(ics, opts \\ []) end alias ICalendar.Deserialize defimpl ICalendar.Deserialize, for: BitString do alias ICalendar.Util.Deserialize + require Logger - def from_ics(ics) do + def from_ics(ics, opts \\ []) do ics |> String.trim() |> String.split("\n") |> Enum.map(&String.trim_trailing/1) - |> get_events() + |> get_events([], [], opts) end - defp get_events(calendar_data, event_collector \\ [], temp_collector \\ []) + defp get_events(calendar_data, event_collector, temp_collector, opts) - defp get_events([head | calendar_data], event_collector, temp_collector) do + defp get_events([head | calendar_data], event_collector, temp_collector, opts) do case head do "BEGIN:VEVENT" -> # start collecting event - get_events(calendar_data, event_collector, [head]) + get_events(calendar_data, event_collector, [head], opts) "END:VEVENT" -> # finish collecting event - event = Deserialize.build_event(temp_collector ++ [head]) - get_events(calendar_data, [event] ++ event_collector, []) + try do + event = Deserialize.build_event(temp_collector ++ [head]) + get_events(calendar_data, [event] ++ event_collector, [], opts) + rescue + e -> + if Keyword.get(opts, :ignore_errors) do + Logger.info("Error parsing: #{inspect(e)}") + get_events(calendar_data, event_collector, [], opts) + else + Kernel.reraise(e, __STACKTRACE__) + end + end event_property when temp_collector != [] -> - get_events(calendar_data, event_collector, temp_collector ++ [event_property]) + get_events(calendar_data, event_collector, temp_collector ++ [event_property], opts) _unimportant_stuff -> - get_events(calendar_data, event_collector, temp_collector) + get_events(calendar_data, event_collector, temp_collector, opts) end end - defp get_events([], event_collector, _temp_collector), do: event_collector + defp get_events([], event_collector, _temp_collector, _opts), do: event_collector + end From 1a0fff7a5b0d97a7e908c4bc2dc7a8e242cd0da2 Mon Sep 17 00:00:00 2001 From: Tim Su Date: Sat, 23 Jan 2021 20:57:51 -0800 Subject: [PATCH 3/4] Handle unexpected timezones without crashing by falling back to UTC. --- lib/icalendar/util/deserialize.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/icalendar/util/deserialize.ex b/lib/icalendar/util/deserialize.ex index 72cfb54..bd0f576 100644 --- a/lib/icalendar/util/deserialize.ex +++ b/lib/icalendar/util/deserialize.ex @@ -242,8 +242,12 @@ defmodule ICalendar.Util.Deserialize do if Regex.match?(~r/\//, timezone) do timezone else - Timex.Timezone.Utils.to_olson(timezone) - end + try do + Timex.Timezone.Utils.to_olson(timezone) + rescue + _e -> nil # probably a custom timezone defined in this file + end + end || "UTC" date_string = case String.last(date_string) do From c898b6d7152c4aad2ca56f0a32fa40150ad008b3 Mon Sep 17 00:00:00 2001 From: Tim Su Date: Sat, 23 Jan 2021 23:23:00 -0800 Subject: [PATCH 4/4] Add tests to deserialize. Format. --- lib/icalendar/deserialize.ex | 1 - lib/icalendar/util/deserialize.ex | 16 ++++++++----- test/icalendar/deserialize_test.exs | 35 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/icalendar/deserialize.ex b/lib/icalendar/deserialize.ex index e778fb0..aa3fa60 100644 --- a/lib/icalendar/deserialize.ex +++ b/lib/icalendar/deserialize.ex @@ -48,5 +48,4 @@ defimpl ICalendar.Deserialize, for: BitString do end defp get_events([], event_collector, _temp_collector, _opts), do: event_collector - end diff --git a/lib/icalendar/util/deserialize.ex b/lib/icalendar/util/deserialize.ex index bd0f576..a0bebc1 100644 --- a/lib/icalendar/util/deserialize.ex +++ b/lib/icalendar/util/deserialize.ex @@ -106,11 +106,14 @@ defmodule ICalendar.Util.Deserialize do acc ) do exdates = Map.get(acc, :exdates, []) - new_dates = String.split(new_exdates, ",") - |> Enum.map(fn exdate -> - {:ok, timestamp} = to_date(exdate, params) - timestamp - end) + + new_dates = + String.split(new_exdates, ",") + |> Enum.map(fn exdate -> + {:ok, timestamp} = to_date(exdate, params) + timestamp + end) + %{acc | exdates: Enum.concat(new_dates, exdates)} end @@ -245,7 +248,8 @@ defmodule ICalendar.Util.Deserialize do try do Timex.Timezone.Utils.to_olson(timezone) rescue - _e -> nil # probably a custom timezone defined in this file + # probably a custom timezone defined in this file + _e -> nil end end || "UTC" diff --git a/test/icalendar/deserialize_test.exs b/test/icalendar/deserialize_test.exs index b02ba6e..e49c536 100644 --- a/test/icalendar/deserialize_test.exs +++ b/test/icalendar/deserialize_test.exs @@ -49,6 +49,41 @@ defmodule ICalendar.DeserializeTest do assert event.dtend.time_zone == "America/Chicago" end + test "with custom Timezone won't crash" do + ics = """ + BEGIN:VTIMEZONE + TZID:Eastern Standard Time 1 + BEGIN:STANDARD + DTSTART:16010101T000000 + TZOFFSETFROM:-0500 + TZOFFSETTO:-0500 + END:STANDARD + END:VTIMEZONE + BEGIN:VEVENT + DTEND;TZID=Eastern Standard Time 1:22221224T084500 + DTSTART;TZID=Eastern Standard Time 1:22221224T083000 + END:VEVENT + """ + + [event] = ICalendar.from_ics(ics) + assert event.dtstart.time_zone == "Etc/UTC" + assert event.dtend.time_zone == "Etc/UTC" + end + + test "with rrule exceptions" do + ics = """ + BEGIN:VEVENT + RRULE:FREQ=WEEKLY + EXDATE;TZID=Eastern Standard Time:20201126T091500,20201127T091500 + DTSTART;TZID=Eastern Standard Time:20201023T091500 + DTEND;TZID=Eastern Standard Time:20201023T093000 + END:VEVENT + """ + + [event] = ICalendar.from_ics(ics) + assert length(event.exdates) == 2 + end + test "with CR+LF line endings" do ics = """ BEGIN:VEVENT