#include "mdiary.h" /** * @brief The dateFormats struct contains regex/date-info pairs to parse different date formats. */ struct dateFormats { gchar *regex; guint index_count; guint index_year; guint index_month; guint index_day; guint index_hour; guint index_minute; } dateFormats_list[] = { { .regex = "(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s*(\\d{1,2}):(\\d{1,2}).*", .index_count = 5, .index_year = 1, .index_month = 2, .index_day = 3, .index_hour = 4, .index_minute = 5 }, { .regex = "(\\d{1,2})\\.(\\d{1,2})\\.(\\d{1,4})\\s*(\\d{1,2}):(\\d{1,2}).*", .index_count = 5, .index_year = 3, .index_month = 2, .index_day = 1, .index_hour = 4, .index_minute = 5 }, { .regex = "(\\d{1,4})-(\\d{1,2})-(\\d{1,2}).*", .index_count = 3, .index_year = 1, .index_month = 2, .index_day = 3, }, { .regex = "(\\d{1,2})\\.(\\d{1,2})\\.(\\d{1,4}).*", .index_count = 3, .index_year = 3, .index_month = 2, .index_day = 1, }, { .regex = NULL } }; /** * @brief mdiary_get_date_from_string tries to guess the date format used and converts it to GDateTime. * @param Input string. * @return A GDateTime object or NULL on error. You need to free it after use. */ GDateTime *mdiary_get_date_from_string(gchar *string) { return mdiary_get_date_from_string_ext(string, "", ""); } /** * @brief mdiary_get_date_from_string_ext tries to guess the date format used and converts it to GDateTime. * @param Input string. * @return A GDateTime object or NULL on error. You need to free it after use. */ GDateTime *mdiary_get_date_from_string_ext(gchar *string, gchar *prefix, gchar *suffix) { GRegex *regex; GMatchInfo *match_info; struct dateFormats *dateFormats; GDateTime *datetime = NULL; guint year = 0; guint month = 0; guint day = 0; guint hour = 0; guint minute = 0; gchar *regex_string; dateFormats = dateFormats_list; do { regex_string = g_strdup_printf("^\\s*%s%s%s", prefix, dateFormats->regex, suffix); regex = g_regex_new(regex_string, G_REGEX_RAW, 0, NULL); if (g_regex_match(regex, string, 0, &match_info) && g_regex_get_capture_count(regex) >= dateFormats->index_count && g_match_info_matches(match_info)) { if (dateFormats->index_year) year = atoi(g_match_info_fetch(match_info, dateFormats->index_year)); if (dateFormats->index_month) month = atoi(g_match_info_fetch(match_info, dateFormats->index_month)); if (dateFormats->index_day) day = atoi(g_match_info_fetch(match_info, dateFormats->index_day)); if (dateFormats->index_hour) hour = atoi(g_match_info_fetch(match_info, dateFormats->index_hour)); if (dateFormats->index_minute) minute = atoi(g_match_info_fetch(match_info, dateFormats->index_minute)); if (year >= 0 && month >= 1 && month <= 12 && day >= 1 && day <= 31 && hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) { datetime = g_date_time_new_local(year, month, day, hour, minute, 0); g_regex_unref(regex); g_free(regex_string); break; } } g_free(regex_string); g_regex_unref(regex); } while ((++dateFormats)->regex); return datetime; } static gchar *mdiary_get_title_from_string(gchar *string) { GRegex *regex; GMatchInfo *match_info; gchar *ret = NULL; regex = g_regex_new("\\# (.*)", G_REGEX_RAW, 0, NULL); if (g_regex_match(regex, string, 0, &match_info) && g_regex_get_capture_count(regex) > 0 && g_match_info_matches(match_info)) { ret = g_strdup(g_match_info_fetch(match_info, 1)); } g_regex_unref(regex); return ret; } static gchar *mdiary_get_summary_from_string(gchar *string) { GRegex *regex; GMatchInfo *match_info; gchar *ret = NULL; regex = g_regex_new("^\\s*Summary: (.*)", G_REGEX_RAW, 0, NULL); if (g_regex_match(regex, string, 0, &match_info) && g_regex_get_capture_count(regex) > 0 && g_match_info_matches(match_info)) { ret = g_strdup(g_match_info_fetch(match_info, 1)); } g_regex_unref(regex); return ret; } static gboolean mdiary_is_empty_line(gchar *string) { GRegex *regex; GMatchInfo *match_info; gboolean ret = 0; regex = g_regex_new("^\\s*$", G_REGEX_RAW, 0, NULL); if (g_regex_match(regex, string, 0, &match_info) && g_match_info_matches(match_info)) { ret = 1; } g_regex_unref(regex); return ret; } static GList *mdiary_add_tags_from_string(gchar *string) { GRegex *regex; GMatchInfo *match_info; GList *ret = NULL; gchar *ptr; gchar *beg_ptr; gchar *orig_ptr; gboolean collected = 0; gchar bak; regex = g_regex_new("^\\s*Tags: (.*)", G_REGEX_RAW, 0, NULL); if (g_regex_match(regex, string, 0, &match_info) && g_regex_get_capture_count(regex) > 0 && g_match_info_matches(match_info)) { /** * TODO: This function should be rewritten to fully use GRegex... */ ptr = beg_ptr = orig_ptr = g_strdup(g_match_info_fetch(match_info, 1)); do { if (*ptr == ',' || *ptr == '\0') { bak = *ptr; if (collected) { collected = 0; *ptr = '\0'; ret = g_list_append(ret, g_strdup(beg_ptr)); beg_ptr = ptr + 1; *ptr = bak; } else { beg_ptr = ptr + 1; } } else if (*ptr == ' ') { if (!collected) beg_ptr = ptr + 1; } else { collected = 1; } } while (*(ptr++)); g_free(orig_ptr); } g_regex_unref(regex); return ret; } /** * @brief mdiary_get_line_from_string WARNING: This does modify str and the content of str! * @param str The pointer to the string where to search the linebreak * @return The current line (null terminated). Do not free, this is a direct pointer into the original string. */ static gchar *mdiary_get_line_from_string(gchar **str_ptr) { gchar *start; gchar *str; if (!str_ptr || !*str_ptr || !**str_ptr) return NULL; str = *str_ptr; start = str; while (*str != '\n' && *str != '\0') str++; *str = '\0'; *str_ptr = ++str; return start; } /** * @brief mdiary_add_file_to_store * @param filename * @param content NULL when the file shall be read by the function. Otherwise the file content. * @param entryListStore * @param autoCompletion */ static void mdiary_add_file_to_store(struct mdiary_scanner *mdiary_scanner, gchar *filename, gchar *content, GList *autotags, GtkListStore *entryListStore, GtkListStore *autoCompletion) { GFile *file = NULL; GError *err = NULL; GFileInputStream *stream = NULL; GDataInputStream *dstream = NULL; gchar *line = NULL; gchar *content_ptr = NULL; GDateTime *datetime = NULL; gchar *title = NULL; GList *tagList = NULL; gchar *summary = NULL; GString *text = NULL; guint header_state = 0; GList *l; g_message("Add file: %s\n", filename); mdiary_scanner->entries_failed++; /* Will be decremented again when entry was successfully added. */ if (!content) { file = g_file_new_for_path(filename); stream = g_file_read(file, NULL, &err); if (err != NULL) { g_print("PARSER: %s: %s\n", filename, err->message); g_error_free(err); return; } dstream = g_data_input_stream_new(G_INPUT_STREAM(stream)); } content_ptr = content; while (line = (content ? mdiary_get_line_from_string(&content_ptr) : g_data_input_stream_read_line(G_DATA_INPUT_STREAM(dstream), NULL, NULL, NULL))) { if (header_state == 0) { if (!datetime) { datetime = mdiary_get_date_from_string_ext(line, "Date: ", ""); if (datetime) continue; } if (!summary) { summary = mdiary_get_summary_from_string(line); if (summary) continue; } if (!title) { title = mdiary_get_title_from_string(line); if (title) continue; } if (!tagList) { tagList = mdiary_add_tags_from_string(line); if (tagList) continue; } header_state = mdiary_is_empty_line(line); } else if (header_state == 1) { header_state += !mdiary_is_empty_line(line); } if (header_state == 2) { if (text) { text = g_string_append(text, "\n"); text = g_string_append(text, line); } else { text = g_string_new(line); } } } if (!datetime) { g_warning("PARSER: Could not detect date in file!\n"); datetime = g_date_time_new_from_unix_local(0); } if (!tagList) { g_warning("PARSER: Could not detect tags in file!\n"); tagList = g_list_append(tagList, g_strdup("untagged")); } if (!text) { g_warning("PARSER: Could not find any text in file!\n"); text = g_string_new("No content found."); } if (!summary) { g_warning("PARSER: Could not detect summary in file!\n"); summary = g_strdup("No summary found."); } if (!title) { g_warning("PARSER: Could not detect title in file!\n"); title = g_strdup("Untitled"); } for (l = autotags; l != NULL; l = l->next) { tagList = g_list_append(tagList, g_strdup(l->data)); } mdiary_scanner->entries_failed--; mdiary_scanner->entries_added++; mdiary_add_entry_to_store(entryListStore, autoCompletion, title, datetime, tagList, summary, text->str, filename); if (g_date_time_to_unix(datetime) > mdiary_scanner->time_latest) mdiary_scanner->time_latest = g_date_time_to_unix(datetime); if (g_date_time_to_unix(datetime) < mdiary_scanner->time_earliest) mdiary_scanner->time_earliest = g_date_time_to_unix(datetime); if (!content) { g_input_stream_close(G_INPUT_STREAM(dstream), NULL, NULL); g_input_stream_close(G_INPUT_STREAM(stream), NULL, NULL); g_object_unref(file); } g_list_free_full(tagList, *g_free); g_string_free(text, 0); g_free(summary); g_free(title); g_date_time_unref(datetime); } static void mdiary_add_gpg_to_store(struct mdiary_scanner *mdiary_scanner, gchar *filename, GList **autotags, GtkListStore *entryListStore, GtkListStore *autoCompletion) { gchar *cmd = NULL; gchar *cmd_stdout = NULL; gchar *cmd_stderr = NULL; gint exit_status = 0; g_message("Decrypt file: %s", filename); if (mdiary_scanner->gpg_enabled == 2) { /* GPG operation was manually cancelled */ g_warning("GPG: Skipping file."); mdiary_scanner->entries_failed++; /* Simply count files that won't be decoded. */ return; } cmd = g_strdup_printf("gpg --decrypt \"%s\"", filename); if (g_spawn_command_line_sync(cmd, &cmd_stdout, &cmd_stderr, &exit_status, NULL) && exit_status == 0) { g_message("GPG: Decryption OK!\n"); *autotags = g_list_append(*autotags, "encrypted"); mdiary_add_file_to_store(mdiary_scanner, filename, cmd_stdout, *autotags, entryListStore, autoCompletion); *autotags = g_list_remove(*autotags, g_list_last(*autotags)->data); } else { mdiary_scanner->entries_failed++; mdiary_scanner->error_code |= MDS_ERROR_GPG; g_warning("GPG: Decryption of message failed with error code %d!\n" "---------- SNIP ----------\n" "%s" "---------- SNIP ----------\n", exit_status, cmd_stderr ? cmd_stderr : "Unknown error.\n"); if (cmd_stderr && strstr(cmd_stderr, "Operation cancelled")) { g_warning("GPG: Aborting import of further GPG messages."); mdiary_scanner->gpg_enabled = 2; } } g_free(cmd); g_free(cmd_stdout); g_free(cmd_stderr); } static void mdiary_recurse_and_collect(struct mdiary_scanner *mdiary_scanner, gchar *base_dir, GList **autotags, GtkListStore *entryListStore, GtkListStore *autoCompletion, guint max_level) { GDir *dir = g_dir_open(base_dir, 0, NULL); gchar *dirname; gchar *fullPath; GRegex *regex; GMatchInfo *match_info; if (!dir) { g_print("Could not open base directory.\n"); mdiary_scanner->error_code |= MDS_ERROR_BASE_DIR; } else { while (dirname = (gchar *)g_dir_read_name(dir)) { fullPath = g_strdup_printf("%s/%s", base_dir, dirname); if (g_file_test(fullPath, G_FILE_TEST_IS_REGULAR)) { regex = g_regex_new("\\.md$", G_REGEX_CASELESS, 0, NULL); if (g_regex_match(regex, fullPath, 0, &match_info) && g_match_info_matches(match_info)) mdiary_add_file_to_store(mdiary_scanner, fullPath, NULL, *autotags, entryListStore, autoCompletion); g_regex_unref(regex); regex = g_regex_new("\\.md.gpg$", G_REGEX_CASELESS, 0, NULL); if (g_regex_match(regex, fullPath, 0, &match_info) && g_match_info_matches(match_info)) { mdiary_scanner->entries_encrypted++; if (mdiary_scanner->gpg_enabled) mdiary_add_gpg_to_store(mdiary_scanner, fullPath, autotags, entryListStore, autoCompletion); } g_regex_unref(regex); } else if (g_file_test(fullPath, G_FILE_TEST_IS_DIR) && max_level) { *autotags = g_list_append(*autotags, dirname); mdiary_recurse_and_collect(mdiary_scanner, fullPath, autotags, entryListStore, autoCompletion, max_level - 1); *autotags = g_list_remove(*autotags, g_list_last(*autotags)->data); } g_free(fullPath); } g_dir_close(dir); } } struct mdiary_scanner *mdiary_scanner_new(gboolean gpg_enabled) { struct mdiary_scanner *mdiary_scanner; mdiary_scanner = malloc(sizeof(struct mdiary_scanner)); if (mdiary_scanner) { mdiary_scanner->time_earliest = G_MAXINT64; mdiary_scanner->time_latest = 0; mdiary_scanner->entries_added = 0; mdiary_scanner->gpg_enabled = gpg_enabled; mdiary_scanner->entries_failed = 0; mdiary_scanner->entries_encrypted = 0; mdiary_scanner->error_code = 0; } return mdiary_scanner; } void mdiary_scanner_free(struct mdiary_scanner *mdiary_scanner) { free(mdiary_scanner); } /** * @brief mdiary_scan_to_store recursively (max. 5 levels) scans the base_dir into the entryListStore. * @param base_dir The base directory to start scanning in * @param entryListStore Target GtkListStore * @return The amount of entries added in total across all scans */ gint mdiary_scan_to_store(struct mdiary_scanner *mdiary_scanner, gchar *base_dir, GtkListStore *entryListStore, GtkListStore *autoCompletion) { GList *autotags = NULL; mdiary_recurse_and_collect(mdiary_scanner, base_dir, &autotags, entryListStore, autoCompletion, 5); g_list_free(autotags); return mdiary_scanner->entries_added; } /** * @brief mainWindow_taglist_to_string concatenates a list with commas * @param list is a GList of strings to concatenate * @return The string, it needs to be freed using g_free(). */ static gchar *mdiary_taglist_to_string(GList *list) { GString *str = NULL; gchar *ret = NULL; GList *l; for (l = list; l != NULL; l = l->next) { if (str == NULL) str = g_string_new(""); else str = g_string_append(str, ", "); str = g_string_append(str, l->data); } ret = str->str; g_string_free(str, 0); return ret; } struct tag_compare_struct { gchar *text; gboolean result; }; gboolean mdiary_test_duplicate(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { gchar *temp = NULL; gboolean ret = FALSE; struct tag_compare_struct *tag_compare_struct = (struct tag_compare_struct *)data; gtk_tree_model_get(model, iter, 0, &temp, -1); if (!g_strcmp0(temp, tag_compare_struct->text)) { ret = TRUE; tag_compare_struct->result = TRUE; } g_free(temp); return ret; } /** * @brief mdiary_add_entry_to_store adds the specified entry to the store. All params are copied. * @param entryListStore target store * @param title Entry title * @param datetime Entry GDateTime * @param tags Entry tags * @param text Entry MD text (with header) */ void mdiary_add_entry_to_store(GtkListStore *entryListStore, GtkListStore *autoCompletion, gchar *title, GDateTime *datetime, GList *tags, gchar *summary, gchar *text, gchar *file_url) { GtkTreeIter iter; GDateTime *datetime_copy; GList *taglist_copy; gchar *date_text; gchar *taglistString; GList *l; struct tag_compare_struct tag_compare_struct = { .text = "", .result = FALSE }; datetime_copy = g_date_time_add(datetime, 0); date_text = g_date_time_format(datetime_copy, "%A, %e %B %Y %R"); taglist_copy = g_list_copy_deep(tags, (GCopyFunc) g_strdup, NULL); taglistString = mdiary_taglist_to_string(taglist_copy); gtk_list_store_append(entryListStore, &iter); gtk_list_store_set(entryListStore, &iter, COL_TITLE, title, COL_DATE_TEXT, date_text, COL_TAGS_TEXT, taglistString, COL_TIMESTAMP, datetime_copy, COL_TAGLIST, taglist_copy, /* TODO: Verify that the duplication worked! */ COL_SUMMARY, summary, /* Automatically strdupd */ COL_TEXT, text, /* Automatically strdupd */ COL_URL, file_url, /* Automatically strdupd */ -1); g_free(date_text); for (l = tags; l != NULL; l = l->next) { tag_compare_struct.text = l->data; gtk_tree_model_foreach(GTK_TREE_MODEL(autoCompletion), mdiary_test_duplicate, &tag_compare_struct); if (!tag_compare_struct.result) { gtk_list_store_append(autoCompletion, &iter); gtk_list_store_set(autoCompletion, &iter, 0, l->data, -1); } } tag_compare_struct.result = FALSE; tag_compare_struct.text = title; gtk_tree_model_foreach(GTK_TREE_MODEL(autoCompletion), mdiary_test_duplicate, &tag_compare_struct); if (!tag_compare_struct.result) { gtk_list_store_append(autoCompletion, &iter); gtk_list_store_set(autoCompletion, &iter, 0, title, -1); } } /** * @brief mdiary_free_entry_elements frees all non-GtkListView-managed parts of the mdiary entry * @param model * @param path * @param iter * @param data * @return FALSE */ gboolean mdiary_free_entry_elements(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { gpointer ptr; gtk_tree_model_get(model, iter, COL_TAGLIST, &ptr, -1); g_list_free_full((GList *)ptr, g_free); gtk_tree_model_get(model, iter, COL_TIMESTAMP, &ptr, -1); g_date_time_unref((GDateTime *)ptr); return FALSE; } /** * @brief mdiary_reset_store clears the two GtkListStores and frees all of their respective elements. (Make sure that * no other part of the program tries to access the elements of the store while executing this function.) * @param entryListStore * @param autoCompletion */ void mdiary_reset_store(GtkListStore *entryListStore, GtkListStore *autoCompletion) { gtk_tree_model_foreach(GTK_TREE_MODEL(entryListStore), mdiary_free_entry_elements, NULL); gtk_list_store_clear(entryListStore); gtk_list_store_clear(autoCompletion); }