diff --git a/.gitignore b/.gitignore index 3ff478d..9a61f13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ a.out +ics_analyzer .gitconfig +*.o diff --git a/README.md b/README.md index 7a71ee4..e2a722d 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,17 @@ sudo make install the default path is for evolution ics files at `~/.local/share/evolution/calendar/system/calendar.ics` ``` -ics_analyzer +icscli ``` for a custom path ``` -ics_analyzer -f path/to/ics/file.ics +icscli -f path/to/ics/file.ics ``` +``` +icscli -h +``` #### uninstall @@ -39,9 +42,6 @@ sudo make uninstall #### TODO -- option to add events (evolution does not care about duplicate uuids) -(but use libuuid to create uuids) - add option to print upcoming events only until a certain date -- improve makefile -- rename executable to ics_cli +- show end date for events that span over multiple days - tests diff --git a/docs/ics_format.txt b/docs/ics_format.txt new file mode 100644 index 0000000..33db92b --- /dev/null +++ b/docs/ics_format.txt @@ -0,0 +1,33 @@ +ICS files from Microsoft Outlook differ from those that were created by Gnome Evolution. +(semicolons are used instead of colons at some position) + +Evolution +--------- +all line endings are \r\n + +all day event: +DTSTART;VALUE=DATE:20230908 +DTEND;VALUE=DATE:20230909 + +an all day event can span over multiple days and look like this: +DTSTART;VALUE=DATE:20230911^M$ +DTEND;VALUE=DATE:20230916^M$ + +appointment: +DTSTART;TZID=/freeassociation.sourceforge.net/Continent/City: + 20230909T090000 +DTEND;TZID=/freeassociation.sourceforge.net/Continent/City: + 20230909T093000 + +appointments can also span over multiple days: +DTSTART;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20230913T230000 +DTEND;TZID=/freeassociation.sourceforge.net/Europe/Berlin: + 20230914T040000 + +SEQUENCE is the number of times the event was modified. +Evolution sets this to 2 after it has been generated. + +Appointments include the continent and city of the used time zone. + +"DESCRIPTION:" is an optional field. diff --git a/makefile b/makefile deleted file mode 100644 index dc2b1e2..0000000 --- a/makefile +++ /dev/null @@ -1,32 +0,0 @@ -all: - gcc -Wall ./src/*.c - -.PHONY: debug -debug: - gcc -Wall -g ./src/*.c - gdb a.out - -.PHONY: run -run: - gcc -Wall ./src/*.c - ./a.out - -.PHONY: install -install: a.out - cp a.out /usr/local/bin/ics_analyzer - -.PHONY: clean -clean: - rm a.out - -.PHONY: uninstall -uninstall: - rm /usr/local/bin/ics_analyzer - -.PHONY: test -test: - ./a.out - @echo - ./a.out -h - @echo - ./a.out -f tests/calendar.ics diff --git a/src/Makefile b/src/Makefile index 5df0b27..7985077 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,33 +1,42 @@ -.PHONY:all -all: - gcc -Wall *.c +CC = gcc +CFLAGS = -Wall +LDFLAGS = -luuid -.PHONY:debug -debug: - gcc -Wall -g *.c - gdb a.out +# List of all source files (assuming they're all in the same directory) +SRC_FILES = $(wildcard *.c) -.PHONY:run -run: - gcc -Wall *.c - ./a.out +# Generate a list of object files by replacing the .c extension with .o +OBJ_FILES = $(SRC_FILES:.c=.o) + +EXECUTABLE = "icscli" + +# linking +$(EXECUTABLE): $(OBJ_FILES) + gcc -Wall $(OBJ_FILES) -o $(EXECUTABLE) $(LDFLAGS) + +main.o: main.c + $(CC) $(CFLAGS) -c $< + +# use implicit rule to compile C source files to object files +%.o: %.c %.h + $(CC) $(CFLAGS) -c $< .PHONY:install -install: a.out - cp a.out /usr/local/bin/ics_analyzer +install: $(EXECUTABLE) + cp $(EXECUTABLE) /usr/local/bin/$(EXECUTABLE) .PHONY:clean clean: - -rm a.out + -rm $(EXECUTABLE) *.o .PHONY:uninstall uninstall: - -rm /usr/local/bin/ics_analyzer + -rm /usr/local/bin/$(EXECUTABLE) .PHONY:test test: - ./a.out + ./$(EXECUTABLE) @echo - ./a.out -h + ./$(EXECUTABLE) -h @echo - ./a.out -f ../tests/calendar.ics + ./$(EXECUTABLE) -f ../tests/calendar.ics diff --git a/src/cli_arg_parsing.c b/src/cli_arg_parsing.c index 167ca6e..c85ab56 100644 --- a/src/cli_arg_parsing.c +++ b/src/cli_arg_parsing.c @@ -1,3 +1,4 @@ +#include "insert_event.h" #include "cli_arg_parsing.h" #include #include @@ -5,37 +6,38 @@ #include void usage() { - printf ("-h\tprint this help\n"); - printf ("-f\tspecify ics file path\n"); - exit(0); + printf ("-f [FILE PATH]\t\tspecify ics file path\n"); + printf ("-h\t\t\tprint this help\n"); + printf ("-i\t\t\tinsert an event\n"); + exit(0); } void get_cli_args(int argc, char **argv, char **file_name) { - int opt = 0; + int opt = 0; - memset(file_name, '\0', strlen(*file_name)); + memset(file_name, '\0', strlen(*file_name)); - if (argc < 2) { - char *home = getenv("HOME"); - *file_name = home; + char *home = getenv("HOME"); + *file_name = home; - if (home != NULL) { - strcat(*file_name, "/.local/share/evolution/calendar/system/calendar.ics"); - } else { - printf ("Environment variable HOME is not set.\n"); - exit(1); - } - return; - } + if (home != NULL) { + strcat(*file_name, "/.local/share/evolution/calendar/system/calendar.ics"); + } else { + printf ("Environment variable HOME is not set.\n"); + exit(1); + } - while ((opt = getopt(argc, argv, "f:h")) != -1) { - switch(opt) { - case 'f': - *file_name = optarg; - break; - case 'h': - usage(); - } - } + while ((opt = getopt(argc, argv, "f:hi")) != -1) { + switch(opt) { + case 'f': + *file_name = optarg; + break; + case 'h': + usage(); + break; + case 'i': + insert_event(*file_name); + break; + } + } } - diff --git a/src/cut_string.c b/src/cut_string.c deleted file mode 100644 index 7d546cc..0000000 --- a/src/cut_string.c +++ /dev/null @@ -1,41 +0,0 @@ -// cut a string into two parts by the first occurence of delimiter -// and choose the first part (side 0) or the second part (side 1) -// the chosen part will overwrite the original string - -// cut a string into two parts by delimiter -// and choose the first part (side 0) or the second part (side 1) -// the chosen part will overwrite the original string - -#include - -void cut_string(char my_string[], char delimiter, int side) { - char part1[256] = ""; - char part2[256] = ""; - - int split = 0; - - int j = 0; - for (int i = 0; i < strlen(my_string); i++) { - if (my_string[i] == delimiter) { - if (split == 0) { - split = 1; - continue; - } - } - - if (split == 0) { - part1[i] = my_string[i]; - } else { - part2[j] = my_string[i]; - j++; - } - } - - memset(my_string, '\0', strlen(my_string)); - if (side == 0) { - strcpy(my_string, part1); - } else { - strcpy(my_string, part2); - } -} - diff --git a/src/cut_string.h b/src/cut_string.h deleted file mode 100644 index 9b8803d..0000000 --- a/src/cut_string.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -// cut string into two parts and choose one part -// side is 0 or 1 -void cut_string(char my_string[], char delimiter, int side); diff --git a/src/date_time_handling.c b/src/date_time_handling.c index e7e6a17..e122540 100644 --- a/src/date_time_handling.c +++ b/src/date_time_handling.c @@ -1,15 +1,23 @@ #include "date_time_handling.h" +#include "string_handling.h" #include #include -#include #include +#include +#include +// buffer needs to contain a string with a strlen of 15 (format: "xxxxxxxxTxxxxxx") +// or a strlen of 16 (format: "YYYYmmddTHHMMSSZ") void get_date(char buffer[]) { // add 1 because strlen does not include the null character size_t buffer_size = strlen(buffer) + 1; time_t my_unix_ts = time(NULL); struct tm* my_tm_local = localtime(&my_unix_ts); - strftime(buffer, buffer_size, "%Y%m%dT%H%M%S", my_tm_local); + if (strlen(buffer) == 15) { + strftime(buffer, buffer_size, "%Y%m%dT%H%M%S", my_tm_local); + } else if (strlen(buffer) == 16) { + strftime(buffer, buffer_size, "%Y%m%dT%H%M%SZ", my_tm_local); + } } // 20230823T194138 -> 2023-08-23 19:41:38 @@ -31,6 +39,26 @@ void pretty_print_date_time(char date_time[]) { } } +void marshall_date_time(char date_time[]) { + char transformed_string[strlen(date_time)]; + int j = 0; + remove_nl_and_cr(date_time); + for (int i = 0; i<=strlen(date_time); i++) { + if (date_time[i] != ':' && date_time[i] != '-') { + if (date_time[i] == ' ') { + transformed_string[j] = 'T'; + } else { + transformed_string[j] = date_time[i]; + } + + j++; + } + + } + memset(date_time, '\0', strlen(date_time)); + strcpy(date_time, transformed_string); +} + void print_end_date(char end_date[]) { // end_date is all day event if (strlen(end_date) == 8) @@ -43,4 +71,18 @@ void print_end_date(char end_date[]) { printf ("%c%c:", time[2], time[3]); printf ("%c%c", time[4], time[5]); } - + +char *get_tz() { + char *timezone_path = malloc(256); + ssize_t bytes_read = readlink("/etc/localtime", timezone_path, 255); + + if (bytes_read != -1) { + // Null-terminate the string + timezone_path[bytes_read] = '\0'; + } else { + perror("readlink"); + exit(1); + } + + return timezone_path; +} diff --git a/src/date_time_handling.h b/src/date_time_handling.h index 9a9415b..d76cda1 100644 --- a/src/date_time_handling.h +++ b/src/date_time_handling.h @@ -2,5 +2,7 @@ #include void get_date(char buffer[]); +char *get_tz(); void pretty_print_date_time(char date_time[]); +void marshall_date_time(char date_time[]); void print_end_date(char end_date[]); diff --git a/src/insert_event.c b/src/insert_event.c new file mode 100644 index 0000000..5c00896 --- /dev/null +++ b/src/insert_event.c @@ -0,0 +1,187 @@ +#include "insert_event.h" +#include "string_handling.h" +#include "date_time_handling.h" +#include +#include +#include +#include +#include +#include + +void insert_event(char *file_name) { + int myfd = open(file_name, O_RDWR); + int all_day_event = 0; + char summary_buf[256] = "SUMMARY:"; + char *input_buffer = &summary_buf[8]; + uuid_t uuid; + char uuid_str[37] = ""; + char dtstamp[] = "YYYYmmddTHHMMSSZ"; + char *time_zone = get_tz(); + char *dtstart_buffer = malloc(128); + char *dtend_buffer = malloc(128); + + remove_until_delim(time_zone, '/', 4); + + printf("Insert a new event\n"); + + printf("Is this an all day event? [y/n] "); + all_day_event = binary_user_choice(); + + printf("SUMMARY: "); + fgets (input_buffer, (sizeof(summary_buf)-strlen(summary_buf)), stdin); + + if (strchr(input_buffer, '\n') == NULL) + printf ("The input has been truncated to:\n%s\n", input_buffer); + + // modify the string to have \r\n line ending + summary_buf[strlen(summary_buf)+1] = '\0'; + summary_buf[strlen(summary_buf)-1] = '\r'; + summary_buf[strlen(summary_buf)] = '\n'; + + uuid_generate(uuid); + // parse uuid to a string + uuid_unparse(uuid, uuid_str); + + get_date(dtstamp); + + get_dtstart_dtend(dtstart_buffer, all_day_event, "start"); + marshall_date_time(dtstart_buffer); + form_dtstart_string(dtstart_buffer, time_zone); + + get_dtstart_dtend(dtend_buffer, all_day_event, "end"); + marshall_date_time(dtend_buffer); + form_dtend_string(dtend_buffer, time_zone); + + seek_cal_end(myfd); + write(myfd, "BEGIN:VEVENT\r\n", strlen("BEGIN:VEVENT\r\n")); + write(myfd, "UID:", strlen("UID:")); + write(myfd, uuid_str, strlen(uuid_str)); + write(myfd, "\r\n", strlen("\r\n")); + write(myfd, "DTSTAMP:", strlen("DTSTAMP:")); + write(myfd, dtstamp, strlen(dtstamp)); + write(myfd, "\r\n", strlen("\r\n")); + write(myfd, dtstart_buffer, strlen(dtstart_buffer)); + write(myfd, dtend_buffer, strlen(dtend_buffer)); + write(myfd, "SEQUENCE:2\r\n", strlen("SEQUENCE:2\r\n")); + write(myfd, summary_buf, strlen(summary_buf)); + write(myfd, "TRANSP:OPAQUE\r\n", strlen("TRANSP:OPAQUE\r\n")); + write(myfd, "CLASS:PUBLIC\r\n", strlen("CLASS:PUBLIC\r\n")); + write(myfd, "CREATED:", strlen("CREATED:")); + write(myfd, dtstamp, strlen(dtstamp)); + write(myfd, "\r\n", strlen("\r\n")); + write(myfd, "LAST-MODIFIED:", strlen("LAST-MODIFIED:")); + write(myfd, dtstamp, strlen(dtstamp)); + write(myfd, "\r\n", strlen("\r\n")); + write(myfd, "END:VEVENT\r\n", strlen("END:VEVENT\r\n")); + + write(myfd, "END:VCALENDAR\r\n", strlen("END:VCALENDAR\r\n")); + + close(myfd); + free(time_zone); + free(dtstart_buffer); + free(dtend_buffer); + + exit(0); +} + +void seek_cal_end(int fd) { + char search_string[] = "END:VCALENDAR"; + int j = 0; + char char_reader = '\0'; + + lseek(fd, -1, SEEK_END); + + while(read(fd, &char_reader, 1)) { + // no need to compare to the null terminator of the search_string + if (char_reader == search_string[strlen(search_string)-j-1]) { + j++; + } else { + j = 0; + } + if (j == (strlen(search_string))) { + lseek(fd, -1, SEEK_CUR); + break; + } + lseek(fd, -2, SEEK_CUR); + } +} + +int binary_user_choice() { + char input_buffer[64] = ""; + if (fgets (input_buffer, sizeof(input_buffer), stdin) == NULL) { + printf ("an fgets error occured\n"); + } + if (! strchr(input_buffer, '\n')) { + printf ("Input buffer overflow!\n"); + exit(1); + } + if (input_buffer[0] == 'n' || input_buffer[0] == 'N') { + return 0; + } else if (input_buffer[0] == 'y' || input_buffer[0] == 'Y') { + return 1; + } else { + printf ("Please enter a N/n or Y/y character!\n"); + exit(1); + } +} + +// char *start_or_end should contain "start" or "end" +void get_dtstart_dtend(char input_buffer[], int all_day_event, char *start_or_end) { + if (all_day_event) { + printf("Enter the %s date in YYYY-mm-dd format!\n", start_or_end); + if (fgets(input_buffer, 128, stdin) == NULL) { + perror ("fgets"); + exit(1); + } + if (strlen(input_buffer) != 11) { + printf ("Wrong format!\n"); + exit(1); + } + } else { + printf("Enter the %s date in YYYY-mm-dd HH:MM:SS format!\n", start_or_end); + if (fgets(input_buffer, 128, stdin) == NULL) { + perror ("fgets"); + exit(1); + } + if (strlen(input_buffer) != 20) { + printf ("Wrong format!\n"); + exit(1); + } + } +} + +void form_dtstart_string(char dtstart_buffer[], char time_zone[]) { + char dtstart_copy[strlen(dtstart_buffer)]; + strcpy(dtstart_copy, dtstart_buffer); + + // not all day event + if (strlen(dtstart_buffer) == 15) { + strcpy(dtstart_buffer, "DTSTART;TZID=/freeassociation.sourceforge.net"); + strcat(dtstart_buffer, time_zone); + strcat(dtstart_buffer, ":\r\n "); + strcat(dtstart_buffer, dtstart_copy); + } else { + // all day event + strcpy(dtstart_buffer, "DTSTART;VALUE=DATE:"); + strcat(dtstart_buffer, dtstart_copy); + } + strcat(dtstart_buffer, "\r\n"); +} + +void form_dtend_string(char dtend_buffer[], char time_zone[]) { + char dtend_copy[strlen(dtend_buffer)]; + strcpy(dtend_copy, dtend_buffer); + + // not all day event + if (strlen(dtend_buffer) == 15) { + strcpy(dtend_buffer, "DTEND;TZID=/freeassociation.sourceforge.net"); + strcat(dtend_buffer, time_zone); + strcat(dtend_buffer, ":\r\n "); + strcat(dtend_buffer, dtend_copy); + } else { + // all day event + strcpy(dtend_buffer, "DTEND;VALUE=DATE:"); + strcat(dtend_buffer, dtend_copy); + } + strcat(dtend_buffer, "\r\n"); +} diff --git a/src/insert_event.h b/src/insert_event.h new file mode 100644 index 0000000..5d012a9 --- /dev/null +++ b/src/insert_event.h @@ -0,0 +1,8 @@ +#pragma once + +void insert_event(char *file_name); +void seek_cal_end(int fd); +int binary_user_choice(); +void get_dtstart_dtend(char input_buffer[], int all_day_event, char *start_or_end); +void form_dtstart_string(char dtstart_buffer[], char time_zone[]); +void form_dtend_string(char dtend_buffer[], char time_zone[]); diff --git a/src/main.c b/src/main.c index 74902f8..f3d5d8f 100644 --- a/src/main.c +++ b/src/main.c @@ -1,12 +1,11 @@ #include "cli_arg_parsing.h" #include "date_time_handling.h" #include "list_handling.h" -#include "cut_string.h" +#include "string_handling.h" #include "move_lines.h" #include "read_until_nl.h" #include "read_until_string.h" #include "seek_string_a.h" -#include "remove_whitespace.h" #include #include #include @@ -14,8 +13,8 @@ #include int main(int argc, char **argv) { - char *ics_path = ""; - get_cli_args(argc, argv, &ics_path); + char *ics_path = ""; + get_cli_args(argc, argv, &ics_path); char my_line[4096] = ""; diff --git a/src/remove_whitespace.c b/src/remove_whitespace.c deleted file mode 100644 index 646e2aa..0000000 --- a/src/remove_whitespace.c +++ /dev/null @@ -1,45 +0,0 @@ -// this function removes all new lines and carriage returns from a string -// you might want to write a new function that replaces '\r' and '\n' -// with a delimiter of user's choice - -#include - -void remove_nl_and_cr(char raw_string[]) { - char processed_string[strlen(raw_string)]; - - // counter for num of elements of processed_string - int j = 0; - for (int i = 0; i +#include + +void cut_string(char my_string[], char delimiter, int side) { + char part1[256] = ""; + char part2[256] = ""; + + int split = 0; + + int j = 0; + for (int i = 0; i < strlen(my_string); i++) { + if (my_string[i] == delimiter) { + if (split == 0) { + split = 1; + continue; + } + } + + if (split == 0) { + part1[i] = my_string[i]; + } else { + part2[j] = my_string[i]; + j++; + } + } + + memset(my_string, '\0', strlen(my_string)); + if (side == 0) { + strcpy(my_string, part1); + } else { + strcpy(my_string, part2); + } +} + +// this function removes all new lines and carriage returns from a string +// you might want to write a new function that replaces '\r' and '\n' +// with a delimiter of user's choice +void remove_nl_and_cr(char raw_string[]) { + char processed_string[strlen(raw_string)]; + + // counter for num of elements of processed_string + int j = 0; + for (int i = 0; i= occurence) { + tmp_string[j] = raw_string[i]; + j++; + } + } + strcpy(raw_string, tmp_string); + free(tmp_string); +} diff --git a/src/string_handling.h b/src/string_handling.h new file mode 100644 index 0000000..114e836 --- /dev/null +++ b/src/string_handling.h @@ -0,0 +1,8 @@ +#pragma once + +// cut string into two parts and choose one part +// side is 0 or 1 +void cut_string(char my_string[], char delimiter, int side); +void remove_nl_and_cr(char raw_string[]); +void remove_whitespace(char raw_string[]); +void remove_until_delim(char raw_string[], char delimiter, int occurence);