diff --git a/lib/include/edapi/journal/file.h b/lib/include/edapi/journal/file.h index 040849b..cf0fb86 100644 --- a/lib/include/edapi/journal/file.h +++ b/lib/include/edapi/journal/file.h @@ -1,6 +1,7 @@ #ifndef EDAPI_JOURNAL_FILE_H #define EDAPI_JOURNAL_FILE_H +#include #include #include @@ -25,7 +26,11 @@ EDErrorCode ed_journal_file_parse_filename( * function fails if the given file cannot be opened. Loading * entries is a time consuming task (especially with multiple * files in a journal), so this function only peeks the first - * few entries to figure out game version and CMDR name. + * few entries to figure out game version and CMDR name, and + * it peeks the last entry to determine date range of the + * journal file. + * + * To fully load all entries call ed_journal_file_load(). */ EDErrorCode ed_journal_file_open(EDJournalFile *file, char const *filename, @@ -42,6 +47,9 @@ gchar const *ed_journal_file_get_commander(EDJournalFile *self); gchar const *ed_journal_file_get_gameversion(EDJournalFile *self); +EDJournalEntry *ed_journal_file_get_first(EDJournalFile *self); +EDJournalEntry *ed_journal_file_get_last(EDJournalFile *self); + gint ed_journal_file_compare(EDJournalFile *lhs, EDJournalFile *rhs); G_END_DECLS diff --git a/lib/src/journal/file.c b/lib/src/journal/file.c index b9b0685..7049422 100644 --- a/lib/src/journal/file.c +++ b/lib/src/journal/file.c @@ -13,6 +13,9 @@ typedef struct { GList *entries; gchar *commander; gchar *gameversion; + + EDJournalEntry *first; + EDJournalEntry *last; } EDJournalFilePrivate; struct _EDJournalFile { @@ -31,6 +34,17 @@ G_DEFINE_TYPE_EXTENDED( G_ADD_PRIVATE(EDJournalFile) ); +static void ed_journal_file_dispose(GObject *obj) +{ + EDJournalFile *self = ED_JOURNALFILE(obj); + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + + g_clear_object(&p->first); + g_clear_object(&p->last); + + G_OBJECT_CLASS(ed_journal_file_parent_class)->dispose(obj); +} + static void ed_journal_file_finalize(GObject *obj) { EDJournalFile *self = ED_JOURNALFILE(obj); @@ -64,6 +78,7 @@ static void ed_journal_file_finalize(GObject *obj) static void ed_journal_file_class_init(EDJournalFileClass *klass) { + G_OBJECT_CLASS(klass)->dispose = ed_journal_file_dispose; G_OBJECT_CLASS(klass)->finalize = ed_journal_file_finalize; } @@ -244,6 +259,183 @@ ed_journal_file_parse_commander(EDJournalFile *self, return ed_error_success; } +static EDErrorCode ed_journal_file_read_first_(EDJournalFile *self) +{ + EDErrorCode ret = ed_error_internal; + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + + GFile *file = NULL; + GFileInputStream *stream = NULL; + GDataInputStream *reader = NULL; + EDJournalEntry *entry = NULL; + + char *line = NULL; + gsize linelen = 0; + + file = g_file_new_for_path(p->filename); + goto_if_true(file == NULL, done); + + stream = g_file_read(file, NULL, NULL); + if (stream == NULL) { + goto done; + } + + reader = g_data_input_stream_new(G_INPUT_STREAM(stream)); + if (reader == NULL) { + goto done; + } + + while (TRUE) { + line = g_data_input_stream_read_line_utf8( + reader, &linelen, NULL, NULL + ); + goto_if_true(line == NULL, done); + + /* ignore empty lines + */ + g_strchomp(line); + if (strlen(line) <= 0) { + g_free(line); + line = NULL; + linelen = 0; + continue; + } + + entry = ed_journal_entry_new_parse(line, NULL); + + g_free(line); + line = NULL; + linelen = 0; + + if (entry == NULL) { + continue; + } + + g_clear_object(&p->first); + p->first = g_object_ref(entry); + + g_clear_object(&entry); + + break; + } + + + ret = ed_error_success; + +done: + + g_clear_object(&reader); + g_clear_object(&stream); + g_clear_object(&file); + + return ret; +} + +static EDErrorCode ed_journal_file_read_last_(EDJournalFile *self) +{ + EDErrorCode ret = ed_error_internal; + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + + GFile *file = NULL; + GFileInputStream *stream = NULL; + GDataInputStream *reader = NULL; + EDJournalEntry *entry = NULL; + + goffset end = 0, pos = 0; + + char *line = NULL; + char *last = NULL; + gsize linelen = 0; + + file = g_file_new_for_path(p->filename); + goto_if_true(file == NULL, done); + + stream = g_file_read(file, NULL, NULL); + if (stream == NULL) { + goto done; + } + + reader = g_data_input_stream_new(G_INPUT_STREAM(stream)); + if (reader == NULL) { + goto done; + } + + if (!g_seekable_seek(G_SEEKABLE(stream), 0, G_SEEK_END, NULL, NULL)) { + goto done; + } + + end = g_seekable_tell(G_SEEKABLE(stream)); + pos = end; + + while (TRUE) { + /* arbitrary */ + pos = end - 100; + if (pos <= 0) { + break; + } + + if (!g_seekable_seek(G_SEEKABLE(stream), pos, G_SEEK_SET, + NULL, NULL)) { + goto done; + } + + /* try to read last line if we accidentally seeked over two + */ + do { + line = g_data_input_stream_read_line_utf8( + reader, &linelen, NULL, NULL + ); + + if (line == NULL) { + break; + } + + /* ignore empty lines + */ + g_strchomp(line); + if (strlen(line) <= 0) { + g_free(line); + line = NULL; + linelen = 0; + continue; + } + + g_free(last); + last = line; + } while (TRUE); + + if (last == NULL) { + goto done; + } + + entry = ed_journal_entry_new_parse(last, NULL); + + g_free(last); + last = NULL; + + if (entry == NULL) { + continue; + } + + g_clear_object(&p->last); + p->last = g_object_ref(entry); + + g_clear_object(&entry); + + break; + } + + ret = ed_error_success; + +done: + + g_clear_object(&reader); + g_clear_object(&stream); + g_clear_object(&file); + + return ret; +} + static EDErrorCode ed_journal_file_load_(EDJournalFile *self, GError **error, @@ -348,6 +540,10 @@ EDErrorCode ed_journal_file_open(EDJournalFile *file, return r; } + /* files may be empty */ + ed_journal_file_read_first_(file); + ed_journal_file_read_last_(file); + return r; } @@ -378,6 +574,20 @@ gchar const *ed_journal_file_get_gameversion(EDJournalFile *self) return p->gameversion; } +EDJournalEntry *ed_journal_file_get_first(EDJournalFile *self) +{ + return_if_true(self == NULL, NULL); + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + return p->first; +} + +EDJournalEntry *ed_journal_file_get_last(EDJournalFile *self) +{ + return_if_true(self == NULL, NULL); + EDJournalFilePrivate *p = ed_journal_file_get_instance_private(self); + return p->last; +} + gint ed_journal_file_compare(EDJournalFile *lhs, EDJournalFile *rhs) { return_if_true(lhs == NULL || rhs == NULL, 0); diff --git a/lib/tests/Journal.2024-04-18T061507.01.log b/lib/tests/Journal.2024-04-18T061507.01.log new file mode 100644 index 0000000..b6592d6 --- /dev/null +++ b/lib/tests/Journal.2024-04-18T061507.01.log @@ -0,0 +1,4 @@ +{ "timestamp":"2023-04-18T04:14:56Z", "event":"Fileheader", "part":1, "language":"English/UK", "Odyssey":true, "gameversion":"4.0.0.1477", "build":"r291050/r0 " } +{ "timestamp":"2023-04-18T04:15:39Z", "event":"Commander", "FID":"F123456", "Name":"DeiMuata" } +{ "timestamp":"2023-04-18T05:15:39Z", "event":"Something" } +{ "timestamp":"2023-04-18T06:15:39Z", "event":"Shutdown" } diff --git a/lib/tests/test-journal-file.c b/lib/tests/test-journal-file.c index 0503a7a..a77963d 100644 --- a/lib/tests/test-journal-file.c +++ b/lib/tests/test-journal-file.c @@ -146,6 +146,34 @@ static void test_valid_peek(void **state) g_clear_object(&file); } +static void test_first_last(void **state) +{ + char const *filename = "Journal.2024-04-18T061507.01.log"; + + EDJournalFile *file = NULL; + EDErrorCode ret = 0; + EDJournalEntry *e = NULL; + GError *error = NULL; + + file = ed_journal_file_new(); + assert_non_null(file); + + ret = ed_journal_file_open(file, filename, &error); + + assert_null(error); + assert_int_equal(ret, ed_error_success); + + e = ed_journal_file_get_first(file); + assert_non_null(e); + assert_true(ed_journal_entry_is(e, "Fileheader")); + + e = ed_journal_file_get_last(file); + assert_non_null(e); + assert_true(ed_journal_entry_is(e, "Shutdown")); + + g_clear_object(&file); +} + int main(int ac, char **av) { static const struct CMUnitTest tests[] = { @@ -155,6 +183,7 @@ int main(int ac, char **av) cmocka_unit_test(test_new_datetime), cmocka_unit_test(test_old_datetime), cmocka_unit_test(test_valid_peek), + cmocka_unit_test(test_first_last), }; return cmocka_run_group_tests(tests, NULL, NULL);