diff options
-rw-r--r-- | src/ical.c | 341 | ||||
-rw-r--r-- | src/note.c | 7 | ||||
-rw-r--r-- | test/Makefile.am | 2 | ||||
-rw-r--r-- | test/data/ical-009.ical | 13 | ||||
-rw-r--r-- | test/data/ical-012.ical | 93 | ||||
-rwxr-xr-x | test/ical-009.sh | 24 | ||||
-rwxr-xr-x | test/ical-012.sh | 76 |
7 files changed, 479 insertions, 77 deletions
@@ -54,6 +54,15 @@ typedef enum { EVENT } ical_vevent_e; +typedef enum { + NO_PROPERTY, + SUMMARY, + DESCRIPTION, + LOCATION, + COMMENT, + STATUS +} ical_property_e; + typedef struct { enum recur_type type; int freq; @@ -440,14 +449,16 @@ ical_store_apoint(char *mesg, char *note, long start, long dur, } /* - * Returns an allocated string representing the argument string with escaped - * characters decoded, or NULL on error. - * The string is assumed to be the value part of a SUMMARY or DESCRIPTION line. + * Return an allocated string containing the decoded 'line' or NULL on error. + * The last arguments are used to format a note file entry. + * The line is assumed to be the value part of a content line of type TEXT or + * INTEGER (RFC 5545, 3.3.11 and 3.3.8) without list or field separators (3.1.1). */ -static char *ical_unformat_line(char *line) +static char *ical_unformat_line(char *line, int eol, int indentation) { struct string s; char *p; + const char *INDENT = " "; string_init(&s); for (p = line; *p; p++) { @@ -457,6 +468,8 @@ static char *ical_unformat_line(char *line) case 'N': case 'n': string_catf(&s, "%c", '\n'); + if (indentation) + string_catf(&s, "%s", INDENT); p++; break; case '\\': @@ -472,9 +485,7 @@ static char *ical_unformat_line(char *line) break; case ',': case ';': - /* - * No list or field separator allowed. - */ + /* No list or field separator allowed. */ mem_free(s.buf); return NULL; default: @@ -482,6 +493,9 @@ static char *ical_unformat_line(char *line) break; } } + /* Add the final EOL removed by ical_readline(). */ + if (eol) + string_catf(&s, "\n"); return string_buf(&s); } @@ -879,31 +893,54 @@ ical_read_exdate(llist_t * exc, FILE * log, char *exstr, return 1; } -/* Return an allocated string containing the name of the newly created note. */ -static char *ical_read_note(char *line, unsigned *noskipped, +/* + * Return an allocated string containing a property value to be written in a + * note file or NULL on error. + */ +static char *ical_read_note(char *line, ical_property_e property, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { - char *p, *notestr, *note; + const int EOL = 1, + INDENT = (property != DESCRIPTION); + char *p, *pname, *notestr; + + switch (property) { + case DESCRIPTION: + pname = "description"; + break; + case LOCATION: + pname = "location"; + break; + case COMMENT: + pname = "comment"; + break; + case STATUS: + pname = "status"; + break; + default: + pname = "no property"; + } p = ical_get_value(line); if (!p) { - ical_log(log, item_type, itemline, - _("malformed description line.")); + asprintf(&p, _("malformed %s line."), pname); + ical_log(log, item_type, itemline, p); + mem_free(p); (*noskipped)++; - return NULL; + notestr = NULL; + goto leave; } - notestr = ical_unformat_line(p); + notestr = ical_unformat_line(p, EOL, INDENT); if (!notestr) { - ical_log(log, item_type, itemline, _("malformed description.")); + asprintf(&p, _("malformed %s."), pname); + ical_log(log, item_type, itemline, p); + mem_free(p); (*noskipped)++; - return NULL; - } else { - note = generate_note(notestr); - mem_free(notestr); - return note; } + leave: + return notestr; } /* Returns an allocated string containing the ical item summary. */ @@ -911,30 +948,31 @@ static char *ical_read_summary(char *line, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { - char *p, *summary; + const int EOL = 0, INDENT = 0; + char *p, *summary = NULL; p = ical_get_value(line); if (!p) { - ical_log(log, item_type, itemline, _("malformed summary line")); + ical_log(log, item_type, itemline, _("malformed summary line.")); (*noskipped)++; - return NULL; + goto leave; } - summary = ical_unformat_line(p); + summary = ical_unformat_line(p, EOL, INDENT); if (!summary) { ical_log(log, item_type, itemline, _("malformed summary.")); (*noskipped)++; - return NULL; + goto leave; } - /* Event summaries must not contain newlines. */ + /* An event summary is one line only. */ if (strchr(summary, '\n')) { ical_log(log, item_type, itemline, _("line break in summary.")); (*noskipped)++; mem_free(summary); - return NULL; + summary = NULL; } - + leave: return summary; } @@ -946,21 +984,26 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, { const int ITEMLINE = *lineno - !feof(fdi); ical_vevent_e vevent_type; - char *p; + ical_property_e property; + char *p, *note = NULL, *comment; + const char *SEPARATOR = "-- \n"; + struct string s; struct { llist_t exc; ical_rpt_t *rpt; - char *mesg, *note; + char *mesg, *desc, *loc, *comm, *stat, *note; long start, end, dur; int has_alarm; } vevent; - int skip_alarm; + int skip_alarm, has_note, separator; vevent_type = UNDEFINED; memset(&vevent, 0, sizeof vevent); LLIST_INIT(&vevent.exc); - skip_alarm = 0; + skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { + note = NULL; + property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords @@ -970,7 +1013,6 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, skip_alarm = 0; continue; } - if (starts_with_ci(buf, "END:VEVENT")) { if (!vevent.mesg) { ical_log(log, ICAL_VEVENT, ITEMLINE, @@ -982,7 +1024,6 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, _("item start date is not defined.")); goto skip; } - if (vevent_type == APPOINTMENT && vevent.dur == 0) { if (vevent.end != 0) { vevent.dur = vevent.end - vevent.start; @@ -994,13 +1035,38 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, goto skip; } } - if (vevent.rpt && vevent.rpt->count) { vevent.rpt->until = ical_compute_rpt_until(vevent.start, vevent.rpt); } - + if (has_note) { + /* Construct string with note file contents. */ + string_init(&s); + if (vevent.desc) { + string_catf(&s, "%s", vevent.desc); + mem_free(vevent.desc); + if (separator) + string_catf(&s, SEPARATOR); + } + if (vevent.loc) { + string_catf(&s, _("Location: %s"), + vevent.loc); + mem_free(vevent.loc); + } + if (vevent.comm) { + string_catf(&s, _("Comment: %s"), + vevent.comm); + mem_free(vevent.comm); + } + if (vevent.stat) { + string_catf(&s, _("Status: %s"), + vevent.stat); + mem_free(vevent.stat); + } + vevent.note = generate_note(string_buf(&s)); + mem_free(s.buf); + } switch (vevent_type) { case APPOINTMENT: ical_store_apoint(vevent.mesg, vevent.note, @@ -1023,10 +1089,8 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, goto skip; break; } - return; } - if (starts_with_ci(buf, "DTSTART")) { p = ical_get_value(buf); if (!p) { @@ -1079,23 +1143,84 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = vevent.has_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { - vevent.note = ical_read_note(buf, noskipped, - ICAL_VEVENT, ITEMLINE, log); - if (!vevent.note) + property = DESCRIPTION; + } else if (starts_with_ci(buf, "LOCATION")) { + property = LOCATION; + } else if (starts_with_ci(buf, "COMMENT")) { + property = COMMENT; + } else if (starts_with_ci(buf, "STATUS")) { + property = STATUS; + } + if (property) { + note = ical_read_note(buf, property, noskipped, + ICAL_VEVENT, ITEMLINE, log); + if (!note) goto cleanup; + separator = (property != DESCRIPTION); + has_note = 1; + } + switch (property) { + case DESCRIPTION: + if (vevent.desc) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("only one description allowed.")); + goto skip; + } + vevent.desc = note; + break; + case LOCATION: + if (vevent.loc) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("only one location allowed.")); + goto skip; + } + vevent.loc = note; + break; + case COMMENT: + /* There may be more than one. */ + if (vevent.comm) { + asprintf(&comment, "%sComment: %s", + vevent.comm, note); + mem_free(vevent.comm); + vevent.comm = comment; + } else + vevent.comm = note; + break; + case STATUS: + if (vevent.stat) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("only one status allowed.")); + goto skip; + } + if (!(starts_with(note, "TENTATIVE") || + starts_with(note, "CONFIRMED") || + starts_with(note, "CANCELLED"))) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid status value.")); + goto skip; + } + vevent.stat = note; + break; + default: + break; } } - ical_log(log, ICAL_VEVENT, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); - -skip: + skip: (*noskipped)++; - cleanup: - if (vevent.note) - mem_free(vevent.note); + if (note) + mem_free(note); + if (vevent.desc) + mem_free(vevent.desc); + if (vevent.loc) + mem_free(vevent.loc); + if (vevent.comm) + mem_free(vevent.comm); + if (vevent.stat) + mem_free(vevent.stat); if (vevent.mesg) mem_free(vevent.mesg); if (vevent.rpt) @@ -1108,16 +1233,22 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, char *buf, char *lstore, unsigned *lineno, const char *fmt_todo) { const int ITEMLINE = *lineno - !feof(fdi); + ical_property_e property; + char *note = NULL, *comment; + const char *SEPARATOR = "-- \n"; + struct string s; struct { - char *mesg, *note; + char *mesg, *desc, *loc, *comm, *stat, *note; int priority; int completed; } vtodo; - int skip_alarm; + int skip_alarm, has_note, separator; memset(&vtodo, 0, sizeof vtodo); - skip_alarm = 0; + skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { + note = NULL; + property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords @@ -1127,20 +1258,44 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, skip_alarm = 0; continue; } - if (starts_with_ci(buf, "END:VTODO")) { if (!vtodo.mesg) { ical_log(log, ICAL_VTODO, ITEMLINE, _("could not retrieve item summary.")); goto cleanup; } - + if (has_note) { + /* Construct string with note file contents. */ + string_init(&s); + if (vtodo.desc) { + string_catf(&s, "%s", vtodo.desc); + mem_free(vtodo.desc); + if (separator) + string_catf(&s, SEPARATOR); + } + if (vtodo.loc) { + string_catf(&s, _("Location: %s"), + vtodo.loc); + mem_free(vtodo.loc); + } + if (vtodo.comm) { + string_catf(&s, _("Comment: %s"), + vtodo.comm); + mem_free(vtodo.comm); + } + if (vtodo.stat) { + string_catf(&s, _("Status: %s"), + vtodo.stat); + mem_free(vtodo.stat); + } + vtodo.note = generate_note(string_buf(&s)); + mem_free(s.buf); + } ical_store_todo(vtodo.priority, vtodo.completed, vtodo.mesg, vtodo.note, fmt_todo); (*notodos)++; return; } - if (starts_with_ci(buf, "PRIORITY:")) { sscanf(buf, "PRIORITY:%d\n", &vtodo.priority); if (vtodo.priority < 0 || vtodo.priority > 9) { @@ -1151,6 +1306,7 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, } } else if (starts_with_ci(buf, "STATUS:COMPLETED")) { vtodo.completed = 1; + property = STATUS; } else if (starts_with_ci(buf, "SUMMARY")) { vtodo.mesg = ical_read_summary(buf, noskipped, ICAL_VTODO, @@ -1160,22 +1316,85 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { - vtodo.note = ical_read_note(buf, noskipped, ICAL_VTODO, - ITEMLINE, log); - if (!vtodo.note) + property = DESCRIPTION; + } else if (starts_with_ci(buf, "LOCATION")) { + property = LOCATION; + } else if (starts_with_ci(buf, "COMMENT")) { + property = COMMENT; + } else if (starts_with_ci(buf, "STATUS")) { + property = STATUS; + } + if (property) { + note = ical_read_note(buf, property, noskipped, + ICAL_VTODO, ITEMLINE, log); + if (!note) goto cleanup; + separator = (property != DESCRIPTION); + has_note = 1; + } + switch (property) { + case DESCRIPTION: + if (vtodo.desc) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("only one description allowed.")); + goto skip; + } + vtodo.desc = note; + break; + case LOCATION: + if (vtodo.loc) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("only one location allowed.")); + goto skip; + } + vtodo.loc = note; + break; + case COMMENT: + /* There may be more than one. */ + if (vtodo.comm) { + asprintf(&comment, "%sComment: %s", + vtodo.comm, note); + mem_free(vtodo.comm); + vtodo.comm = comment; + } else + vtodo.comm = note; + break; + case STATUS: + if (vtodo.stat) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("only one status allowed.")); + goto skip; + } + if (!(starts_with(note, "NEEDS-ACTION") || + starts_with(note, "COMPLETED") || + starts_with(note, "IN-PROCESS") || + starts_with(note, "CANCELLED"))) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("invalid status value.")); + goto skip; + } + vtodo.stat = note; + break; + default: + break; } } - ical_log(log, ICAL_VTODO, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); - -skip: + skip: (*noskipped)++; cleanup: - if (vtodo.note) - mem_free(vtodo.note); + if (note) + mem_free(note); + if (vtodo.desc) + mem_free(vtodo.desc); + if (vtodo.loc) + mem_free(vtodo.loc); + if (vtodo.comm) + mem_free(vtodo.comm); + if (vtodo.stat) + mem_free(vtodo.stat); if (vtodo.mesg) mem_free(vtodo.mesg); } @@ -59,13 +59,9 @@ HTABLE_PROTOTYPE(htp, note_gc_hash) char *generate_note(const char *str) { char *sha1 = mem_malloc(SHA1_DIGESTLEN * 2 + 1); - char *notepath, *s; + char *notepath; FILE *fp; - /* Temporary hack */ - asprintf(&s, "%s\n", str); - str = s; - sha1_digest(str, sha1); asprintf(¬epath, "%s%s", path_notes, sha1); fp = fopen(notepath, "w"); @@ -74,7 +70,6 @@ char *generate_note(const char *str) fputs(str, fp); file_close(fp, __FILE_POS__); - mem_free(s); mem_free(notepath); return sha1; } diff --git a/test/Makefile.am b/test/Makefile.am index 6b04d86..8a62a0c 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -59,6 +59,7 @@ TESTS = \ ical-009.sh \ ical-010.sh \ ical-011.sh \ + ical-012.sh \ next-001.sh \ next-002.sh \ next-003.sh \ @@ -134,5 +135,6 @@ EXTRA_DIST = \ data/ical-007.ical \ data/ical-008.ical \ data/ical-009.ical \ + data/ical-012.ical \ data/todo \ data/todo-export diff --git a/test/data/ical-009.ical b/test/data/ical-009.ical index 73e9037..39ae422 100644 --- a/test/data/ical-009.ical +++ b/test/data/ical-009.ical @@ -63,6 +63,19 @@ BEGIN:VTODO PRIORITY:1 SUMMARY:an unescaped comma: , END:VTODO +BEGIN:VEVENT +DTSTART:20200406T221300 +DURATION:PT0H15M0S +SUMMARY:Invalid STATUS +STATUS:confirmed +END:VEVENT +BEGIN:VEVENT +DTSTART:20200406T221300 +DURATION:PT0H15M0S +SUMMARY:LOCATION twice +LOCATION:first +LOCATION:second +END:VEVENT BEGIN:VTODO SUMMARY:finally\, missing end of item END:VCALENDAR diff --git a/test/data/ical-012.ical b/test/data/ical-012.ical new file mode 100644 index 0000000..09385fb --- /dev/null +++ b/test/data/ical-012.ical @@ -0,0 +1,93 @@ +BEGIN:VCALENDAR +VERSION:2.0 + +BEGIN:VEVENT +DTSTART:20200404T204500 +DURATION:PT1H30M0S +SUMMARY:event with one-line description +DESCRIPTION:event with one-line description +END:VEVENT + +BEGIN:VEVENT +DTSTART:20200404T204500 +DURATION:PT1H30M0S +SUMMARY:description and location +DESCRIPTION:event with description\nand location +LOCATION: Right here +END:VEVENT + +BEGIN:VEVENT +DTSTART:20200404T084100 +DURATION:PT1H30M0S +SUMMARY:No description. Comment and status +COMMENT:Event without description: a comment\nstreching over\nthree lines +STATUS:CONFIRMED +END:VEVENT + +BEGIN:VEVENT +DTSTART:20200404T084100 +DURATION:PT1H30M0S +SUMMARY:Empty description +DESCRIPTION: +END:VEVENT + +BEGIN:VEVENT +DTSTART:20200404T084100 +DURATION:PT1H30M0S +SUMMARY:Empty description\, but comment +DESCRIPTION: +COMMENT:event with empty description +END:VEVENT + +BEGIN:VEVENT +DTSTART:20200404T204500 +DURATION:PT1H30M0S +SUMMARY:description\, status\, comment and location +DESCRIPTION:event with\ndescription\nstatus\ncomment\nand location +LOCATION: Right here +COMMENT:just a repetition of description:\nevent with\ndescription\nstatus\ncomment\nand location +STATUS:CANCELLED +END:VEVENT + +BEGIN:VTODO +PRIORITY:2 +SUMMARY:todo with one-line description +DESCRIPTION:todo with one-line description +END:VTODO + +BEGIN:VTODO +PRIORITY:3 +SUMMARY:description and location +DESCRIPTION:todo with description\nand location +LOCATION: Right here +END:VTODO + +BEGIN:VTODO +PRIORITY:4 +SUMMARY:Comment and status +COMMENT:Todo with out description. A comment\nstreching over\nthree lines +STATUS:NEEDS-ACTION +END:VTODO + +BEGIN:VTODO +PRIORITY:5 +SUMMARY:Empty description +DESCRIPTION: +END:VTODO + +BEGIN:VTODO +PRIORITY:6 +SUMMARY:Empty description\,but status +DESCRIPTION: +STATUS:COMPLETED +END:VTODO + +BEGIN:VTODO +SUMMARY:todo with description\, status\, comment and location +DESCRIPTION:todo with\ndescription\nstatus\ncomment\nand location\,\nbut no priority +LOCATION: Right here +COMMENT:mostly a repetition of description:\ntodo with\ndescription\nstatus\ncomment\nand location +STATUS:IN-PROCESS +END:VTODO + +END:VCALENDAR diff --git a/test/ical-009.sh b/test/ical-009.sh index 31dc283..d912f5c 100755 --- a/test/ical-009.sh +++ b/test/ical-009.sh @@ -7,27 +7,31 @@ if [ "$1" = 'actual' ]; then mkdir .calcurse || exit 1 cp "$DATA_DIR/conf" .calcurse || exit 1 out=$("$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-009.ical" 2>&1) - echo "$out" | sed -n '4,5p' - log=$(echo "$out" | awk '$1 == "See" {print $2}') - cat "$log" | sed '1,17d' - cat $PWD/.calcurse/notes/* | wc + # Print the import report (stdout). + echo "$out" | awk '$1 == "Import"; $2 == "apps"' + # Find the log file and print the log messages (stderr). + logfile=$(echo "$out" | awk '$1 == "See" { print $2 }') + sed '1,18d' "$logfile" + # One empty note file. + cat "$PWD/.calcurse/notes"/* | wc | awk '{ print $1 $2 $3 }' rm -rf .calcurse || exit 1 elif [ "$1" = 'expected' ]; then cat <<EOD -Import process report: 0068 lines read -2 apps / 0 events / 1 todo / 10 skipped - +Import process report: 0081 lines read +2 apps / 0 events / 1 todo / 12 skipped VEVENT [12]: could not retrieve event start time. VEVENT [17]: recurrence frequency not recognized. -VEVENT [23]: malformed summary line +VEVENT [23]: malformed summary line. VTODO [28]: item priority is invalid (must be between 0 and 9). VEVENT [32]: malformed exceptions line. VEVENT [39]: line break in summary. VEVENT [44]: malformed description line. VEVENT [50]: malformed description. VTODO [62]: malformed summary. -VTODO [66]: The ical file seems to be malformed. The end of item was not found. - 1 0 1 +VEVENT [66]: invalid status value. +VEVENT [72]: only one location allowed. +VTODO [79]: The ical file seems to be malformed. The end of item was not found. +101 EOD else ./run-test "$0" diff --git a/test/ical-012.sh b/test/ical-012.sh new file mode 100755 index 0000000..9175db9 --- /dev/null +++ b/test/ical-012.sh @@ -0,0 +1,76 @@ +#!/bin/sh +# Note file creation. Eleven note files are created for 6 apps and 6 todos. +# To produce a fixed, predictable directory listing it is necessary that the +# notes are of different sizes (except for the vevent and vtodo empty note which +# is shared). + +. "${TEST_INIT:-./test-init.sh}" + +if [ "$1" = 'actual' ]; then + mkdir .calcurse && + cp "$DATA_DIR/conf" .calcurse || exit 1 + "$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-012.ical" + (cd "$PWD/.calcurse/notes/"; cat $(ls -S1)) + rm -rf .calcurse || exit 1 +elif [ "$1" = 'expected' ]; then + cat <<EOD +Import process report: 0093 lines read +6 apps / 0 events / 6 todos / 0 skipped +todo with +description +status +comment +and location, +but no priority +-- +Location: Right here +Comment: mostly a repetition of description: + todo with + description + status + comment + and location +Status: IN-PROCESS +event with +description +status +comment +and location +-- +Location: Right here +Comment: just a repetition of description: + event with + description + status + comment + and location +Status: CANCELLED +Comment: Todo with out description. A comment + streching over + three lines +Status: NEEDS-ACTION +Comment: Event without description: a comment + streching over + three lines +Status: CONFIRMED +event with description +and location +-- +Location: Right here +todo with description +and location +-- +Location: Right here + +-- +Comment: event with empty description +event with one-line description +todo with one-line description + +-- +Status: COMPLETED + +EOD +else + ./run-test "$0" +fi |