Files
MuditaOS/module-utils/ical/ParserICS.cpp
Tomas Rogala 7dee85f011 [EGD-4711] Change calendar events endpoints
-Create providers object
-Change endpoints data message structure to json object
-Extend ical format validation
-Extend service desktop tests for calendar endpoints
2021-01-13 21:20:50 +01:00

784 lines
22 KiB
C++

// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
#include "ParserICS.hpp"
#include <cmath>
#include <module-utils/time/time_date_validation.hpp>
namespace ical
{
constexpr inline auto begin = "BEGIN:VCALENDAR";
namespace with_linefeed
{
constexpr inline auto begin = "BEGIN:VCALENDAR\r\n";
constexpr inline auto version = "VERSION:2.0\r\n";
constexpr inline auto end = "END:VCALENDAR\r\n";
} // namespace with_linefeed
namespace event
{
namespace with_linefeed
{
constexpr inline auto begin = "BEGIN:VEVENT\r\n";
constexpr inline auto end = "END:VEVENT\r\n";
} // namespace with_linefeed
constexpr inline auto begin = "BEGIN:VEVENT";
constexpr inline auto end = "END:VEVENT";
constexpr inline auto uid = "UID:";
constexpr inline auto summary = "SUMMARY:";
constexpr inline auto dtstart = "DTSTART:";
constexpr inline auto dtend = "DTEND:";
} // namespace event
namespace alarm
{
namespace with_linefeed
{
constexpr inline auto begin = "BEGIN:VALARM\r\n";
constexpr inline auto end = "END:VALARM\r\n";
} // namespace with_linefeed
constexpr inline auto begin = "BEGIN:VALARM";
constexpr inline auto end = "END:VALARM";
constexpr inline auto trigger = "TRIGGER:";
constexpr inline auto action = "ACTION:";
namespace value
{
// Time, 0, Minutes
namespace action
{
constexpr inline auto display = "DISPLAY";
constexpr inline auto procedure = "PROCEDURE";
constexpr inline auto audio = "AUDIO";
} // namespace action
constexpr inline auto trigger_invalid = "T0M";
constexpr inline auto before = "-P";
constexpr inline auto after = "P";
} // namespace value
} // namespace alarm
namespace rrule
{
constexpr inline auto rrule = "RRULE:";
constexpr inline auto freq = "FREQ=";
namespace frequency
{
constexpr inline auto daily = "DAILY";
constexpr inline auto weekly = "WEEKLY";
constexpr inline auto monthly = "MONTHLY";
constexpr inline auto yearly = "YEARLY";
} // namespace frequency
constexpr inline auto count = "COUNT=";
constexpr inline auto interval = "INTERVAL=";
constexpr inline auto max_interval = 2;
} // namespace rrule
namespace duration
{
constexpr inline auto minutesInHour = 60;
constexpr inline auto minutesInDay = 24 * minutesInHour;
constexpr inline auto minutesInWeek = 7 * minutesInDay;
namespace minutes
{
constexpr inline auto never_happens_value = 0xFFFF;
}
} // namespace duration
constexpr inline auto date_char_length = yearDigitsNumb + monthDigitsNumb + dayDigitsNumb;
constexpr inline auto time_char_length = HourDigitsNumb + MinDigitsNumb + SecDigitsNumb;
constexpr inline auto dt_char_length = date_char_length + 1 + time_char_length;
} // namespace ical
Duration::Duration(uint32_t week, uint32_t day, uint32_t hour, uint32_t minute)
{
if (minute == ical::duration::minutes::never_happens_value) {
this->minute = ical::duration::minutes::never_happens_value;
return;
}
this->week = week;
this->day = day;
this->hour = hour;
this->minute = minute;
if (!utils::time::validateTime(hour, minute, false)) {
isValid = false;
}
}
Duration::Duration(const std::string &property)
{
if (property.empty()) {
LOG_DEBUG("Duration is empty. Event with no Alarm!");
this->minute = ical::duration::minutes::never_happens_value;
return;
}
uint32_t i = 0;
if ((property[0] != 'P' && property[1] != 'P')) {
LOG_ERROR("Duration constructor: Invalid format provided: %s", property.c_str());
isValid = false;
return;
}
while (property[i] != '\0' && i < property.length()) {
if (isdigit(property[i])) {
std::string value;
while (isdigit(property[i])) {
value += property[i];
++i;
}
try {
switch (property[i]) {
case 'W':
this->week = stoi(value);
break;
case 'D':
this->day = stoi(value);
break;
case 'H':
this->hour = stoi(value);
break;
case 'M':
this->minute = stoi(value);
break;
default:
LOG_ERROR("Wrong duration unit value format");
isValid = false;
break;
}
}
catch (std::exception &e) {
LOG_DEBUG("Duration conversion from string to int failed with exception:%s", e.what());
isValid = false;
}
}
++i;
}
if (!utils::time::validateTime(hour, minute, false)) {
LOG_ERROR("Duration time value is invalid");
isValid = false;
}
}
auto Duration::getDurationInMinutes() const -> uint32_t
{
auto weeksMinutes = week * ical::duration::minutesInWeek;
auto daysMinutes = day * ical::duration::minutesInDay;
auto hoursMinutes = hour * ical::duration::minutesInHour;
return weeksMinutes + daysMinutes + hoursMinutes + minute;
}
auto Duration::getDurationString() const -> std::string
{
std::string durationString;
if (minute == ical::duration::minutes::never_happens_value) {
return durationString;
}
if (week) {
durationString += utils::to_string(week) + 'W';
}
if (day) {
durationString += utils::to_string(day) + 'D';
}
if (hour || minute) {
durationString += 'T';
}
if (hour) {
durationString += utils::to_string(hour) + 'H';
}
if (minute) {
durationString += utils::to_string(minute) + 'M';
}
if (week == 0 && day == 0 && hour == 0 && minute == 0) {
durationString += "T0M";
}
return durationString;
}
Alarm::Alarm(Duration &timeBeforeEvent, Action action)
{
if (!timeBeforeEvent.isValid) {
LOG_ERROR("Duration format is invalid");
isValid = false;
return;
}
this->trigger = timeBeforeEvent;
this->action = action;
}
void Alarm::setAction(const std::string &action)
{
if (action == ical::alarm::value::action::display) {
this->action = Action::display;
}
else if (action == ical::alarm::value::action::procedure) {
this->action = Action::procedure;
}
else if (action == ical::alarm::value::action::audio) {
this->action = Action::audio;
}
else if (action.empty()) {
LOG_DEBUG("Alarm with no action");
this->action = Action::none;
}
else {
LOG_ERROR("Alarm action invalid");
isValid = false;
}
}
void Alarm::setTrigger(const std::string &duration)
{
auto timeBeforeEvent = Duration(duration);
if (!timeBeforeEvent.isValid) {
LOG_ERROR("Duration format is invalid");
isValid = false;
return;
}
this->trigger = timeBeforeEvent;
}
auto Alarm::getTriggerValue() const -> Duration
{
return this->trigger;
}
auto Alarm::getActionValue() const -> Action
{
return this->action;
}
auto Alarm::getTriggerString() const -> std::string
{
auto durationString = trigger.getDurationString();
if (durationString.empty()) {
return durationString;
}
return ical::alarm::value::before + durationString;
}
auto Alarm::getActionString() const -> std::string
{
std::string actionStr = ical::alarm::action;
switch (this->action) {
case Action::audio: {
return ical::alarm::value::action::audio;
}
case Action::display: {
return ical::alarm::value::action::display;
}
case Action::procedure: {
return ical::alarm::value::action::procedure;
}
case Action::none: {
LOG_DEBUG("Alarm with no action");
return "";
}
}
return "";
}
RecurrenceRule::RecurrenceRule(Frequency freq, uint32_t count, uint32_t interval)
{
if ((count == 0 || interval == 0) && freq != Frequency::never) {
LOG_ERROR("Invalid rrule format!");
isValid = false;
return;
}
if (interval > ical::rrule::max_interval) {
LOG_ERROR("Invalid interval value!");
isValid = false;
}
this->frequency = freq;
this->count = count;
this->interval = interval;
}
void RecurrenceRule::setFrequency(const std::string &property)
{
if (property == ical::rrule::frequency::daily) {
this->frequency = Frequency::daily;
}
else if (property == ical::rrule::frequency::weekly) {
this->frequency = Frequency::weekly;
}
else if (property == ical::rrule::frequency::monthly) {
this->frequency = Frequency::monthly;
}
else if (property == ical::rrule::frequency::yearly) {
this->frequency = Frequency::yearly;
}
else if (property.empty()) {
this->frequency = Frequency::never;
}
else {
LOG_ERROR("Invalid frequency!");
isValid = false;
}
}
void RecurrenceRule::setCount(const std::string &property)
{
try {
this->count = stoi(property);
}
catch (...) {
LOG_ERROR("Count value is not an integer!");
isValid = false;
}
}
void RecurrenceRule::setInterval(const std::string &property)
{
try {
this->interval = stoi(property);
}
catch (...) {
LOG_ERROR("Interval value not conversionable to int!");
isValid = false;
}
}
auto RecurrenceRule::getFrequencyValue() const -> Frequency
{
return this->frequency;
}
auto RecurrenceRule::getCountValue() const -> uint32_t
{
return this->count;
}
auto RecurrenceRule::getIntervalValue() const -> uint32_t
{
return this->interval;
}
auto RecurrenceRule::getFrequencyString() const -> std::string
{
std::string frequencyStr = ical::rrule::freq;
switch (this->frequency) {
case Frequency::daily: {
frequencyStr = ical::rrule::frequency::daily;
break;
}
case Frequency::weekly: {
frequencyStr = ical::rrule::frequency::weekly;
break;
}
case Frequency::monthly: {
frequencyStr = ical::rrule::frequency::monthly;
break;
}
case Frequency::yearly: {
frequencyStr = ical::rrule::frequency::yearly;
break;
}
case Frequency::never: {
LOG_DEBUG("Frequency never");
return "";
}
}
return frequencyStr;
}
auto RecurrenceRule::getCountString() const -> std::string
{
return utils::to_string(this->count);
}
auto RecurrenceRule::getIntervalString() const -> std::string
{
return utils::to_string(this->interval);
}
auto Event::getDateFromIcalFormat(const std::string &icalDateTime) const -> std::string
{
return icalDateTime.substr(0, icalDateTime.find_first_of('T'));
}
auto Event::getYearFromIcalDate(const std::string &icalDate) const -> std::string
{
return icalDate.substr(0, yearDigitsNumb);
}
auto Event::getMonthFromIcalDate(const std::string &icalDate) const -> std::string
{
return icalDate.substr(yearDigitsNumb, monthDigitsNumb);
}
auto Event::getDayFromIcalDate(const std::string &icalDate) const -> std::string
{
return icalDate.substr(yearDigitsNumb + monthDigitsNumb, dayDigitsNumb);
}
auto Event::getTimeFromIcalFormat(const std::string &icalDateTime) const -> std::string
{
return icalDateTime.substr(icalDateTime.find_first_of('T') + 1);
}
auto Event::getHourFromIcalTime(const std::string &icalTime) const -> std::string
{
return icalTime.substr(0, HourDigitsNumb);
}
auto Event::getMinutesFromIcalTime(const std::string &icalTime) const -> std::string
{
return icalTime.substr(HourDigitsNumb, MinDigitsNumb);
}
auto Event::getSecondsFromIcalTime(const std::string &icalTime) const -> std::string
{
return icalTime.substr(HourDigitsNumb + MinDigitsNumb, SecDigitsNumb);
}
auto Event::dateStringFrom(const std::string &icalDate) const -> std::string
{
return getYearFromIcalDate(icalDate) + "-" + getMonthFromIcalDate(icalDate) + "-" + getDayFromIcalDate(icalDate);
}
auto Event::timeStringFrom(const std::string &icalTime) const -> std::string
{
return getHourFromIcalTime(icalTime) + ":" + getMinutesFromIcalTime(icalTime) + ":" +
getSecondsFromIcalTime(icalTime);
}
auto Event::TimePointFromIcalDate(const std::string &icalDateTime) const -> TimePoint
{
std::string icalDate(getDateFromIcalFormat(icalDateTime));
std::string icalTime(getTimeFromIcalFormat(icalDateTime));
std::string date(dateStringFrom(icalDate));
std::string time(timeStringFrom(icalTime));
std::string dateTime(date + " " + time);
return TimePointFromString(dateTime.c_str());
}
auto Event::TimePointToIcalDate(const TimePoint &tp) const -> std::string
{
constexpr uint32_t bufferLimit = 16;
auto time = TimePointToTimeT(tp);
auto utcTime = gmtime(&time);
char Buffer[bufferLimit];
strftime(Buffer, bufferLimit, "%Y%m%dT%H%M%S", utcTime);
std::string IcalDate = Buffer;
return IcalDate;
}
auto Event::isDate(const std::string &date) -> bool
{
return utils::time::validateDate(getDayFromIcalDate(date), getMonthFromIcalDate(date), getYearFromIcalDate(date));
}
auto Event::isTime(const std::string &time) -> bool
{
[[maybe_unused]] uint32_t seconds;
try {
seconds = stoi(getSecondsFromIcalTime(time));
}
catch (...) {
LOG_ERROR("Seconds value is not an integer!");
return false;
}
return utils::time::validateTime(getHourFromIcalTime(time), getMinutesFromIcalTime(time), false);
}
auto Event::validateDT(const std::string &dt) -> bool
{
uint32_t dateTimeSize;
if (dt.find_first_of('Z') == ical::date_char_length + ical::time_char_length) {
dateTimeSize = ical::dt_char_length + 1;
}
else {
dateTimeSize = ical::dt_char_length;
}
if (dt.size() != dateTimeSize) {
LOG_ERROR("Date time length is invalid");
return false;
}
auto separatorIndex = dt.find_first_of('T');
LOG_DEBUG("Separator index = %d", (int)separatorIndex);
if (separatorIndex != (yearDigitsNumb + monthDigitsNumb + dayDigitsNumb)) {
LOG_ERROR("Date time separator is invalid");
return false;
}
auto date = getDateFromIcalFormat(dt);
auto time = getTimeFromIcalFormat(dt);
if (!isDate(date)) {
LOG_ERROR("Date is invalid");
return false;
}
if (!isTime(time)) {
LOG_ERROR("Time is invalid");
return false;
}
return true;
}
auto Event::validateUID(const std::string &UID) -> bool
{
auto DTimestamp = UID.substr(0, UID.find_first_of('-'));
auto id = UID.substr(UID.find_first_of('-') + 1);
try {
stoi(id);
}
catch (...) {
LOG_ERROR("UID value is not an integer!");
return false;
}
return validateDT(DTimestamp);
}
Event::Event(const std::string &summary, const TimePoint from, TimePoint till, const std::string &uid)
{
if (summary.empty()) {
isValid = false;
}
if (!validateUID(uid) && !uid.empty()) {
isValid = false;
}
this->uid = uid;
this->summary = summary;
this->dtstart = from;
this->dtend = till;
}
void Event::setUID(const std::string &property)
{
if (!validateUID(property) && !property.empty()) {
LOG_ERROR("UID invalid format");
isValid = false;
}
this->uid = property;
}
void Event::setSummary(const std::string &property)
{
if (property.empty()) {
isValid = false;
}
this->summary = property;
}
void Event::setDTStart(const std::string &property)
{
if (!validateDT(property)) {
isValid = false;
}
this->dtstart = TimePointFromIcalDate(property);
}
void Event::setDTEnd(const std::string &property)
{
if (!validateDT(property)) {
isValid = false;
}
this->dtend = TimePointFromIcalDate(property);
}
auto Event::getUID() const -> std::string
{
return uid;
}
auto Event::getSummary() const -> std::string
{
return summary;
}
auto Event::getDTStartTimePoint() const -> TimePoint
{
return dtstart;
}
auto Event::getDTEndTimePoint() const -> TimePoint
{
return dtend;
}
auto Event::getDTStartString() const -> std::string
{
return TimePointToIcalDate(dtstart);
}
auto Event::getDTEndString() const -> std::string
{
return TimePointToIcalDate(dtend);
}
void ParserICS::parseFrom(const Event &event)
{
icsData += ical::event::uid + event.getUID() + "\r\n";
icsData += ical::event::summary + event.getSummary() + "\r\n";
icsData += ical::event::dtstart + event.getDTStartString() + "\r\n";
icsData += ical::event::dtend + event.getDTEndString() + "\r\n";
}
void ParserICS::parseFrom(const Alarm &alarm)
{
auto triggerValue = alarm.getTriggerString();
std::string prefix = ical::alarm::value::before;
if (triggerValue == (prefix + ical::alarm::value::trigger_invalid)) {
LOG_DEBUG("Got Alarm with empty trigger");
return;
}
icsData += ical::alarm::with_linefeed::begin;
icsData += ical::alarm::trigger + triggerValue + "\r\n";
icsData += ical::alarm::action + alarm.getActionString() + "\r\n";
icsData += ical::alarm::with_linefeed::end;
}
void ParserICS::parseFrom(const RecurrenceRule &rrule)
{
auto freq = rrule.getFrequencyString();
if (!freq.empty()) {
icsData += std::string(ical::rrule::rrule) + ical::rrule::freq + freq + ";" + ical::rrule::count +
rrule.getCountString() + ";" + ical::rrule::interval + rrule.getIntervalString() + "\r\n";
}
else {
LOG_DEBUG("Empty Alarm provided");
}
}
void ParserICS::parseFrom(const ICalEvent &icalEvent)
{
if (icalEvent.event.getUID() == "") {
LOG_ERROR("Empty event provided to ical parser");
return;
}
icsData += ical::event::with_linefeed::begin;
parseFrom(icalEvent.event);
parseFrom(icalEvent.rrule);
parseFrom(icalEvent.alarm);
icsData += ical::event::with_linefeed::end;
}
void ParserICS::eventPropertiesFromLine(ICalEvent &icalEvent, const std::string &line) const
{
if (startsWith(line, ical::event::begin)) {
icalEvent.event = Event();
}
else if (startsWith(line, ical::event::uid)) {
icalEvent.event.setUID(getProperty(line));
}
else if (startsWith(line, ical::event::summary)) {
icalEvent.event.setSummary(getProperty(line));
}
else if (startsWith(line, ical::event::dtstart)) {
icalEvent.event.setDTStart(getProperty(line));
}
else if (startsWith(line, ical::event::dtend)) {
icalEvent.event.setDTEnd(getProperty(line));
}
}
void ParserICS::rrulePropertiesFromLine(ICalEvent &icalEvent, const std::string &line) const
{
icalEvent.rrule = RecurrenceRule();
auto formattedLine = getProperty(line);
icalEvent.rrule.setFrequency(getSubProperty(formattedLine, ical::rrule::freq));
icalEvent.rrule.setCount(getSubProperty(formattedLine, ical::rrule::count));
icalEvent.rrule.setInterval(getSubProperty(formattedLine, ical::rrule::interval));
}
void ParserICS::alarmPropertiesFromLine(ICalEvent &icalEvent, std::istringstream &input, std::string &line) const
{
icalEvent.alarm = Alarm();
while (getline(input, line)) {
if (startsWith(line, ical::alarm::trigger)) {
icalEvent.alarm.setTrigger(getProperty(line));
}
else if (startsWith(line, ical::alarm::action)) {
icalEvent.alarm.setAction(getProperty(line));
}
else if (startsWith(line, ical::alarm::end)) {
break;
}
}
}
auto ParserICS::startsWith(const std::string &line, const char *text) const -> bool
{
return (line.find(text) == 0);
}
auto ParserICS::getProperty(const std::string &line) const -> std::string
{
std::string Temp = line.substr(line.find_first_of(':') + 1);
unsigned int Length = Temp.length();
if (Length > 0 && Temp[Length - 1] == '\r')
Temp.resize(Length - 1);
return Temp;
}
auto ParserICS::getSubProperty(const std::string &Line, const char *propertyName) const -> std::string
{
size_t pos = Line.find(propertyName);
if (pos == std::string::npos)
return "";
pos += strlen(propertyName);
return Line.substr(pos, Line.find_first_of(';', pos) - pos);
}
void ParserICS::importEvents(std::vector<ICalEvent> events)
{
this->icsData = ical::with_linefeed::begin;
this->icsData += +ical::with_linefeed::version;
for (auto event : events) {
parseFrom(event);
}
this->icsData += ical::with_linefeed::end;
}
auto ParserICS::exportEvents() const -> std::vector<ICalEvent>
{
std::vector<ICalEvent> events;
ICalEvent icalEvent;
std::istringstream input;
input.str(icsData);
std::string line;
while (getline(input, line)) {
eventPropertiesFromLine(icalEvent, line);
if (startsWith(line, ical::rrule::rrule)) {
rrulePropertiesFromLine(icalEvent, line);
}
else if (startsWith(line, ical::alarm::begin)) {
alarmPropertiesFromLine(icalEvent, input, line);
}
else if (startsWith(line, ical::event::end)) {
events.push_back(icalEvent);
}
}
return events;
}
void ParserICS::loadData(const std::string &data)
{
std::istringstream input;
input.str(data);
std::string line;
getline(input, line);
if (!startsWith(line, ical::begin)) {
LOG_ERROR("Wrong format");
return;
}
this->icsData = data;
}