diff --git a/lib/include/edapi/journal/file.h b/lib/include/edapi/journal/file.h index 6ece6cb..4534f32 100644 --- a/lib/include/edapi/journal/file.h +++ b/lib/include/edapi/journal/file.h @@ -14,12 +14,20 @@ G_DECLARE_FINAL_TYPE(EDJournalFile, ed_journal_file, ED, JOURNALFILE, GObject); EDJournalFile *ed_journal_file_new(void); +EDErrorCode ed_journal_file_parse_filename( + char const *basename, + gchar **date, + gint *part + ); + EDErrorCode ed_journal_file_parse(EDJournalFile *file, char const *filename); EDErrorCode ed_journal_file_open(EDJournalFile *file, char const *filename, GError **error); +GDateTime *ed_journal_file_get_datetime(EDJournalFile *self); + G_END_DECLS #endif diff --git a/lib/src/journal/file.c b/lib/src/journal/file.c index 6a98a7c..8219205 100644 --- a/lib/src/journal/file.c +++ b/lib/src/journal/file.c @@ -3,11 +3,13 @@ #include #include +#include typedef struct { gchar *filename; - gchar *timestamp; - int index; + gchar *datetime; + GDateTime *timestamp; + gint part; GList *entries; } EDJournalFilePrivate; @@ -35,8 +37,16 @@ static void ed_journal_file_finalize(GObject *obj) free(p->filename); p->filename = NULL; - free(p->timestamp); - p->timestamp = NULL; + free(p->datetime); + p->datetime = NULL; + + if (p->timestamp != NULL) { + /* my only pet peeve of glib: you never know if their functions + * fail on a NULL pointer or not + */ + g_date_time_unref(p->timestamp); + p->timestamp = NULL; + } g_list_free_full(p->entries, g_object_unref); p->entries = NULL; @@ -58,20 +68,15 @@ EDJournalFile *ed_journal_file_new(void) return g_object_new(ED_TYPE_JOURNALFILE, NULL); } -EDErrorCode ed_journal_file_parse(EDJournalFile *file, char const *filename) +EDErrorCode ed_journal_file_parse_filename(char const *basename, + gchar **date, + gint *part) { - EDJournalFilePrivate *p = ed_journal_file_get_instance_private(file); - gchar *basename = NULL; - EDErrorCode ret = ed_error_internal; - GRegex *new_style = NULL; GMatchInfo *matches = NULL; - gchar *timestamp = NULL; gchar *index = NULL; - - return_if_true(file == NULL, ed_error_args); - return_if_true(S_EMPTY(filename), ed_error_args); + EDErrorCode ret = ed_error_invalid; new_style = g_regex_new( "Journal\\.([\\dT\\-]+)\\.(\\d+)\\.log", @@ -79,24 +84,111 @@ EDErrorCode ed_journal_file_parse(EDJournalFile *file, char const *filename) ); goto_if_true(new_style == NULL, done); - basename = g_path_get_basename(filename); - goto_if_true(S_EMPTY(basename), done); - if (g_regex_match(new_style, basename, 0, &matches)) { timestamp = g_match_info_fetch(matches, 1); index = g_match_info_fetch(matches, 2); + + ret = ed_error_success; + } else { + ret = ed_error_invalid; } g_match_info_unref(matches); matches = NULL; - goto_if_true(S_EMPTY(timestamp), done); - goto_if_true(S_EMPTY(index), done); + if (ED_SUCCESS(ret)) { + if (date != NULL) { + *date = timestamp; + /* we gave timestamp to the caller */ + timestamp = NULL; + } + if (part != NULL) { + *part = strtol(index, NULL, 10); + } + } - g_free(p->timestamp); - p->timestamp = g_strdup(timestamp); +done: - p->index = strtol(index, NULL, 0); + g_free(timestamp); + g_free(index); + g_regex_unref(new_style); + + return ret; +} + +static EDErrorCode ed_journal_file_parse_timestamp(EDJournalFile *file) +{ + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(file); + + return_if_true(p->datetime == NULL, ed_error_invalid); + + if (strchr(p->datetime, 'T') != NULL) { + /** + * new style ISO timestamps + */ + GTimeZone *utc = g_time_zone_new_utc(); + GDateTime *dt = g_date_time_new_from_iso8601(p->datetime, utc); + g_time_zone_unref(utc); + return_if_true(dt == NULL, ed_error_invalid); + p->timestamp = dt; + + return ed_error_success; + } else { + /** + * old school non-ISO timestamps, used around 2021 + */ + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + int ret = 0; + + ret = sscanf(p->datetime, + "%2d%2d%2d%2d%2d%2d", + &year, &month, &day, + &hour, &minute, &second + ); + + if (ret != 6) { + return ed_error_invalid; + } + + /* add base year to years + */ + year += 2000; + + GDateTime *dt = g_date_time_new_utc( + year, month, day, hour, minute, second); + return_if_true(dt == NULL, ed_error_invalid); + p->timestamp = dt; + + return ed_error_success; + } + + return ed_error_invalid; +} + +EDErrorCode ed_journal_file_parse(EDJournalFile *self, char const *filename) +{ + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + gchar *basename = NULL; + EDErrorCode ret = ed_error_internal; + + return_if_true(self == NULL, ed_error_args); + return_if_true(S_EMPTY(filename), ed_error_args); + + basename = g_path_get_basename(filename); + goto_if_true(S_EMPTY(basename), done); + + ret = ed_journal_file_parse_filename(basename, &p->datetime, &p->part); + if (ED_ERROR(ret)) { + goto done; + } + + ret = ed_journal_file_parse_timestamp(self); + if (ED_ERROR(ret)) { + g_free(p->datetime); + p->datetime = NULL; + p->part = 0; + goto done; + } g_free(p->filename); p->filename = g_strdup(filename); @@ -105,10 +197,7 @@ EDErrorCode ed_journal_file_parse(EDJournalFile *file, char const *filename) done: - g_free(timestamp); - g_free(index); g_free(basename); - g_regex_unref(new_style); return ret; } @@ -156,6 +245,15 @@ static EDErrorCode ed_journal_file_load(EDJournalFile *self, } } + /* ignore empty lines + */ + g_strchomp(line); + if (strlen(line) <= 0) { + g_free(line); + line = NULL; + linelen = 0; + } + entry = ed_journal_entry_new(); goto_if_true(entry == NULL, done); @@ -200,3 +298,10 @@ EDErrorCode ed_journal_file_open(EDJournalFile *file, return r; } + +GDateTime *ed_journal_file_get_datetime(EDJournalFile *self) +{ + return_if_true(self == NULL, NULL); + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + return p->timestamp; +} diff --git a/lib/src/journal/journal.c b/lib/src/journal/journal.c index 74994af..fddb0ce 100644 --- a/lib/src/journal/journal.c +++ b/lib/src/journal/journal.c @@ -1,106 +1,170 @@ -#include -#include - -typedef struct { - gchar *location; -} EDJournalPrivate; - -struct _EDJournal { - GObject parent; -}; - -struct _EDJournalClass { - GObjectClass parent_class; -}; - -G_DEFINE_TYPE_EXTENDED( - EDJournal, - ed_journal, - G_TYPE_OBJECT, - 0, - G_ADD_PRIVATE(EDJournal) - ); - -static void ed_journal_finalize(GObject *obj) -{ - EDJournal *self = ED_JOURNAL(obj); - EDJournalPrivate *p = ed_journal_get_instance_private(self); - - free(p->location); - p->location = NULL; - - G_OBJECT_CLASS(ed_journal_parent_class)->finalize(obj); -} - -static void ed_journal_class_init(EDJournalClass *klass) -{ - G_OBJECT_CLASS(klass)->finalize = ed_journal_finalize; -} - -static EDErrorCode ed_journal_determine_location(EDJournal *self) -{ - EDJournalPrivate *p = ed_journal_get_instance_private(self); - gchar *location = NULL; - -#ifdef G_OS_WIN32 - char const *env = getenv("%USERPROFILE%"); - - if (!S_EMPTY(env)) { - location = g_build_path( - G_DIR_SEPARATOR_S, - env, - "Saved Games", - "Frontier Developments", - "Elite Dangerous", - NULL - ); - } -#endif - - if (S_EMPTY(location)) { - return ed_error_invalid; - } - - if (!g_file_test(location, G_FILE_TEST_IS_DIR)) { - g_free(location); - return ed_error_invalid; - } - - g_free(p->location); - p->location = location; - - return ed_error_success; -} - -static void ed_journal_init(EDJournal *self) -{ - ed_journal_determine_location(self); -} - -EDJournal *ed_journal_new(void) -{ - return g_object_new(ED_TYPE_JOURNAL, NULL); -} - -gchar const *ed_journal_get_location(EDJournal *self) -{ - return_if_true(self == NULL, NULL); - EDJournalPrivate *p = ed_journal_get_instance_private(self); - return p->location; -} - -EDErrorCode ed_journal_set_location(EDJournal *self, gchar const *dir) -{ - return_if_true(self == NULL || dir == NULL, ed_error_args); - - EDJournalPrivate *p = ed_journal_get_instance_private(self); - - if (!g_file_test(dir, G_FILE_TEST_IS_DIR)) { - return ed_error_invalid; - } - - g_free(p->location); - p->location = g_strdup(dir); - - return ed_error_success; -} +#include +#include +#include + +typedef struct { + gchar *location; + GList *files; +} EDJournalPrivate; + +struct _EDJournal { + GObject parent; +}; + +struct _EDJournalClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE_EXTENDED( + EDJournal, + ed_journal, + G_TYPE_OBJECT, + 0, + G_ADD_PRIVATE(EDJournal) + ); + +static void ed_journal_finalize(GObject *obj) +{ + EDJournal *self = ED_JOURNAL(obj); + EDJournalPrivate *p = ed_journal_get_instance_private(self); + + free(p->location); + p->location = NULL; + + G_OBJECT_CLASS(ed_journal_parent_class)->finalize(obj); +} + +static void ed_journal_class_init(EDJournalClass *klass) +{ + G_OBJECT_CLASS(klass)->finalize = ed_journal_finalize; +} + +static EDErrorCode ed_journal_determine_location(EDJournal *self) +{ + gchar *location = NULL; + gchar *tmp = NULL; + + /** + * on Windows we have %USERPROFILE% which points to the current + * users home directory. See if they have a Saved Games folder. + */ + char const *env1 = getenv("%USERPROFILE%"); + char const *env2 = getenv("USERPROFILE"); + if (!S_EMPTY(env1) || !S_EMPTY(env2)) { + tmp = g_build_path( + G_DIR_SEPARATOR_S, + S_EMPTY(env1) ? env2 : env1, + "Saved Games", + "Frontier Developments", + "Elite Dangerous", + NULL + ); + + if (g_file_test(tmp, G_FILE_TEST_IS_DIR)) { + location = tmp; + tmp = NULL; + } + } + + if (S_EMPTY(location)) { + return ed_error_invalid; + } + + if (!g_file_test(location, G_FILE_TEST_IS_DIR)) { + g_free(location); + return ed_error_invalid; + } + + ed_journal_set_location(self, location); + + return ed_error_success; +} + +static void ed_journal_init(EDJournal *self) +{ + ed_journal_determine_location(self); +} + +EDJournal *ed_journal_new(void) +{ + return g_object_new(ED_TYPE_JOURNAL, NULL); +} + +static void ed_journal_load_files(EDJournal *self) +{ + EDJournalPrivate *p = ed_journal_get_instance_private(self); + GDir *loc = NULL; + gchar const *name = NULL; + EDErrorCode ret = ed_error_success; + EDJournalFile *journalfile = NULL; + + g_list_free_full(p->files, g_object_unref); + p->files = NULL; + + loc = g_dir_open(p->location, 0, NULL); + goto_if_true(loc == NULL, done); + + while ((name = g_dir_read_name(loc)) != NULL) { + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + continue; + } + + gchar *full = g_build_path( + G_DIR_SEPARATOR_S, + p->location, + name, + NULL + ); + + if (g_file_test(full, G_FILE_TEST_IS_DIR)) { + goto next; + } + + ret = ed_journal_file_parse_filename(name, NULL, NULL); + goto_if_error(ret, next); + + journalfile = ed_journal_file_new(); + goto_if_true(journalfile == NULL, next); + + ret = ed_journal_file_open(journalfile, full, NULL); + if (ED_SUCCESS(ret)) { + p->files = g_list_append(p->files, journalfile); + } + + next: + + g_free(full); + } + +done: + + if (loc != NULL) { + g_dir_close(loc); + loc = NULL; + } +} + +gchar const *ed_journal_get_location(EDJournal *self) +{ + return_if_true(self == NULL, NULL); + EDJournalPrivate *p = ed_journal_get_instance_private(self); + return p->location; +} + +EDErrorCode ed_journal_set_location(EDJournal *self, gchar const *dir) +{ + return_if_true(self == NULL || dir == NULL, ed_error_args); + + EDJournalPrivate *p = ed_journal_get_instance_private(self); + + if (!g_file_test(dir, G_FILE_TEST_IS_DIR)) { + return ed_error_invalid; + } + + g_free(p->location); + p->location = g_strdup(dir); + + ed_journal_load_files(self); + + return ed_error_success; +} diff --git a/lib/tests/CMakeLists.txt b/lib/tests/CMakeLists.txt index f26a701..74ce44b 100644 --- a/lib/tests/CMakeLists.txt +++ b/lib/tests/CMakeLists.txt @@ -9,7 +9,7 @@ SET(TESTS INCLUDE_DIRECTORIES( "${CMAKE_CURRENT_SOURCE_DIR}/../include" - "${CMOCKA_INCLUDE_DIRS}" + #"${CMOCKA_INCLUDE_DIRS}" ) FOREACH(TEST ${TESTS}) @@ -22,6 +22,6 @@ FOREACH(TEST ${TESTS}) ADD_TEST( NAME ${TEST} COMMAND ${TEST} - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.." + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) ENDFOREACH() diff --git a/lib/tests/Journal.2023-04-18T061507.01.log b/lib/tests/Journal.2023-04-18T061507.01.log new file mode 100644 index 0000000..05a44ab --- /dev/null +++ b/lib/tests/Journal.2023-04-18T061507.01.log @@ -0,0 +1 @@ +{ "timestamp":"2023-04-18T04:14:56Z", "event":"Fileheader", "part":1, "language":"English/UK", "Odyssey":true, "gameversion":"4.0.0.1477", "build":"r291050/r0 " } diff --git a/lib/tests/Saved Games/Frontier Developments/Elite Dangerous/Journal.2023-04-18T061507.01.log b/lib/tests/Saved Games/Frontier Developments/Elite Dangerous/Journal.2023-04-18T061507.01.log new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/test-journal-file.c b/lib/tests/test-journal-file.c index 3ec1b28..5cbf215 100644 --- a/lib/tests/test-journal-file.c +++ b/lib/tests/test-journal-file.c @@ -12,11 +12,13 @@ static void test_new_filename(void **state) EDJournalFile *file = ed_journal_file_new(); EDErrorCode ret = 0; + GError *error = NULL; assert_non_null(file); - ret = ed_journal_file_parse(file, filename); + ret = ed_journal_file_open(file, filename, &error); assert_int_equal(ret, ed_error_success); + assert_null(error); g_clear_object(&file); } diff --git a/lib/tests/test-journal.c b/lib/tests/test-journal.c index 80789d0..a31eb3a 100644 --- a/lib/tests/test-journal.c +++ b/lib/tests/test-journal.c @@ -1,27 +1,32 @@ -#include -#include -#include -#include - -#include -#include - -static void test_new_location(void **state) -{ - EDJournal *journal = ed_journal_new(); - assert_non_null(journal); - - gchar const *location = ed_journal_get_location(journal); - assert_non_null(location); - - g_clear_object(&journal); -} - -int main(int ac, char **av) -{ - static const struct CMUnitTest tests[] = { - cmocka_unit_test(test_new_location), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} +#define _DEFAULT_SOURCE +#include +#include +#include +#include + +#include +#include + +#include + +static void test_userprofile_location(void **state) +{ + setenv("USERPROFILE", "./", 1); + + EDJournal *journal = ed_journal_new(); + assert_non_null(journal); + + gchar const *location = ed_journal_get_location(journal); + assert_non_null(location); + + g_clear_object(&journal); +} + +int main(int ac, char **av) +{ + static const struct CMUnitTest tests[] = { + cmocka_unit_test(test_userprofile_location), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}