no-DB mode implementation #94
@@ -31,6 +31,8 @@
|
|||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -52,6 +54,261 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
char* g_flatfile_account_jid = NULL;
|
char* g_flatfile_account_jid = NULL;
|
||||||
|
static GHashTable* g_contact_states = NULL;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Per-contact state / sparse index
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
ff_contact_state_t*
|
||||||
|
ff_state_new(const char* filepath)
|
||||||
|
{
|
||||||
|
ff_contact_state_t* state = g_malloc0(sizeof(ff_contact_state_t));
|
||||||
|
state->filepath = g_strdup(filepath);
|
||||||
|
state->cursor_offset = -1;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ff_state_free(ff_contact_state_t* state)
|
||||||
|
{
|
||||||
|
if (!state)
|
||||||
|
return;
|
||||||
|
g_free(state->filepath);
|
||||||
|
g_free(state->entries);
|
||||||
|
g_free(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
_ff_state_build(ff_contact_state_t* state)
|
||||||
|
{
|
||||||
|
FILE* fp = fopen(state->filepath, "r");
|
||||||
|
if (!fp)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
int c1 = fgetc(fp);
|
||||||
|
jabber.developer marked this conversation as resolved
|
|||||||
|
int c2 = fgetc(fp);
|
||||||
|
int c3 = fgetc(fp);
|
||||||
|
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
|
||||||
|
state->bom_len = 3;
|
||||||
|
} else {
|
||||||
|
state->bom_len = 0;
|
||||||
|
fseek(fp, 0, SEEK_SET);
|
||||||
|
}
|
||||||
|
|
||||||
|
state->n_entries = 0;
|
||||||
|
state->total_lines = 0;
|
||||||
|
size_t msg_count = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
off_t pos = ftell(fp);
|
||||||
|
gboolean truncated = FALSE;
|
||||||
|
char* buf = ff_readline(fp, &truncated);
|
||||||
|
if (!buf)
|
||||||
|
break;
|
||||||
|
if (truncated) {
|
||||||
|
free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf[0] == '\0' || buf[0] == '#') {
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
nit: I suggest this version for readability (avoid magic number of 3 and 4). Ideally, we can also move "id:" and "aid:" into separate constants. It would also increase robustness in case of prefix changes. nit: I suggest this version for readability (avoid magic number of 3 and 4).
```c
for (int i = 0; parts[i]; i++) {
if (g_str_has_prefix(parts[i], "id:") && !stanza_id) {
stanza_id = ff_unescape_meta_value(parts[i] + strlen("id:"));
} else if (g_str_has_prefix(parts[i], "aid:") && !archive_id) {
archive_id = ff_unescape_meta_value(parts[i] + strlen("aid:"));
}
}
```
Ideally, we can also move "id:" and "aid:" into separate constants. It would also increase robustness in case of prefix changes.
jabber.developer2
commented
Corrected Corrected
|
|||||||
|
state->total_lines++;
|
||||||
|
|
||||||
|
if (msg_count % FF_INDEX_STEP == 0) {
|
||||||
|
char* space = strchr(buf, ' ');
|
||||||
|
if (space) {
|
||||||
|
char* ts_str = g_strndup(buf, space - buf);
|
||||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
what if what if `*(close + 1)` overflows?
jabber.developer2
commented
Corrected Corrected
|
|||||||
|
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
again no overflow check again no overflow check
jabber.developer2
commented
Corrected Corrected
|
|||||||
|
if (dt) {
|
||||||
|
if (state->n_entries >= state->cap_entries) {
|
||||||
|
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||||
|
state->entries = g_realloc(state->entries,
|
||||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
Instead of loop, we can do something like that: Instead of loop, we can do something like that:
`g_utf8_strchr(sender_start, colonspace, '/')`
|
|||||||
|
state->cap_entries * sizeof(ff_index_entry_t));
|
||||||
|
}
|
||||||
|
state->entries[state->n_entries].byte_offset = pos;
|
||||||
|
state->entries[state->n_entries].timestamp_epoch = g_date_time_to_unix(dt);
|
||||||
|
state->n_entries++;
|
||||||
|
g_date_time_unref(dt);
|
||||||
|
}
|
||||||
|
g_free(ts_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg_count++;
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(state->filepath, &st) == 0) {
|
||||||
|
state->stamp.mtime = st.st_mtime;
|
||||||
|
state->stamp.size = st.st_size;
|
||||||
|
state->stamp.inode = st.st_ino;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_debug("flatfile: index built for %s (%zu entries, %zu lines)",
|
||||||
|
state->filepath, state->n_entries, state->total_lines);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
_ff_state_extend(ff_contact_state_t* state)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
if (stat(state->filepath, &st) != 0)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (st.st_size <= state->stamp.size)
|
||||||
|
return _ff_state_build(state);
|
||||||
|
|
||||||
|
FILE* fp = fopen(state->filepath, "r");
|
||||||
|
if (!fp)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
fseek(fp, state->stamp.size, SEEK_SET);
|
||||||
|
if (state->stamp.size > 0) {
|
||||||
|
fseek(fp, state->stamp.size - 1, SEEK_SET);
|
||||||
|
int prev = fgetc(fp);
|
||||||
|
if (prev != '\n' && prev != EOF) {
|
||||||
|
int ch;
|
||||||
|
while ((ch = fgetc(fp)) != EOF && ch != '\n') {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t msg_count = state->total_lines;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
off_t pos = ftell(fp);
|
||||||
|
gboolean truncated = FALSE;
|
||||||
|
char* buf = ff_readline(fp, &truncated);
|
||||||
|
if (!buf)
|
||||||
|
break;
|
||||||
|
if (truncated) {
|
||||||
|
free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf[0] == '\0' || buf[0] == '#') {
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->total_lines++;
|
||||||
|
|
||||||
|
if (msg_count % FF_INDEX_STEP == 0) {
|
||||||
|
char* space = strchr(buf, ' ');
|
||||||
|
if (space) {
|
||||||
|
char* ts_str = g_strndup(buf, space - buf);
|
||||||
|
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||||
|
if (dt) {
|
||||||
|
if (state->n_entries >= state->cap_entries) {
|
||||||
|
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||||
|
state->entries = g_realloc(state->entries,
|
||||||
|
state->cap_entries * sizeof(ff_index_entry_t));
|
||||||
|
}
|
||||||
|
state->entries[state->n_entries].byte_offset = pos;
|
||||||
|
state->entries[state->n_entries].timestamp_epoch = g_date_time_to_unix(dt);
|
||||||
|
state->n_entries++;
|
||||||
|
g_date_time_unref(dt);
|
||||||
|
}
|
||||||
|
g_free(ts_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg_count++;
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
state->stamp.mtime = st.st_mtime;
|
||||||
|
state->stamp.size = st.st_size;
|
||||||
|
state->stamp.inode = st.st_ino;
|
||||||
|
|
||||||
|
log_debug("flatfile: index extended for %s (%zu entries, %zu lines)",
|
||||||
|
state->filepath, state->n_entries, state->total_lines);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
ff_state_ensure_fresh(ff_contact_state_t* state)
|
||||||
|
{
|
||||||
|
if (!state || !state->filepath)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(state->filepath, &st) != 0) {
|
||||||
|
state->total_lines = 0;
|
||||||
|
state->n_entries = 0;
|
||||||
|
state->stamp.size = 0;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->stamp.size == 0 && state->n_entries == 0)
|
||||||
|
return _ff_state_build(state);
|
||||||
|
|
||||||
|
if (st.st_mtime == state->stamp.mtime
|
||||||
|
&& st.st_size == state->stamp.size
|
||||||
|
&& st.st_ino == state->stamp.inode) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.st_ino == state->stamp.inode && st.st_size > state->stamp.size)
|
||||||
|
return _ff_state_extend(state);
|
||||||
|
|
||||||
|
state->cursor_offset = -1;
|
||||||
|
return _ff_state_build(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
off_t
|
||||||
|
ff_state_offset_for_time(ff_contact_state_t* state, const char* iso_time)
|
||||||
|
{
|
||||||
|
if (!state || !iso_time || state->n_entries == 0)
|
||||||
|
return state ? (off_t)state->bom_len : 0;
|
||||||
|
|
||||||
|
GDateTime* dt = g_date_time_new_from_iso8601(iso_time, NULL);
|
||||||
|
if (!dt)
|
||||||
|
return state->bom_len;
|
||||||
|
|
||||||
|
gint64 target = g_date_time_to_unix(dt);
|
||||||
|
g_date_time_unref(dt);
|
||||||
|
|
||||||
|
size_t lo = 0, hi = state->n_entries;
|
||||||
|
while (lo < hi) {
|
||||||
|
size_t mid = lo + (hi - lo) / 2;
|
||||||
|
if (state->entries[mid].timestamp_epoch <= target)
|
||||||
|
lo = mid + 1;
|
||||||
|
else
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lo == 0)
|
||||||
|
return state->bom_len;
|
||||||
|
return state->entries[lo - 1].byte_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ff_contact_state_t*
|
||||||
|
_ff_get_state(const char* contact_barejid)
|
||||||
|
{
|
||||||
|
if (!g_contact_states || !contact_barejid)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
ff_contact_state_t* state = g_hash_table_lookup(g_contact_states, contact_barejid);
|
||||||
|
if (state)
|
||||||
|
return state;
|
||||||
|
|
||||||
|
auto_gchar gchar* filepath = ff_get_log_path(contact_barejid);
|
||||||
|
if (!filepath)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
state = ff_state_new(filepath);
|
||||||
|
g_hash_table_insert(g_contact_states, g_strdup(contact_barejid), state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Core write logic
|
// Core write logic
|
||||||
@@ -79,6 +336,7 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
|||||||
|
|
||||||
GDateTime* dt = timestamp ? g_date_time_ref(timestamp) : g_date_time_new_now_local();
|
GDateTime* dt = timestamp ? g_date_time_ref(timestamp) : g_date_time_new_now_local();
|
||||||
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
|
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
|
||||||
|
g_date_time_unref(dt);
|
||||||
|
|
||||||
// Determine which JID to use for the log file path (the "other" party)
|
// Determine which JID to use for the log file path (the "other" party)
|
||||||
const char* contact = NULL;
|
const char* contact = NULL;
|
||||||
@@ -89,8 +347,7 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
|||||||
contact = from_barejid;
|
contact = from_barejid;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_gchar gchar* log_path = ff_get_log_path(contact, dt);
|
auto_gchar gchar* log_path = ff_get_log_path(contact);
|
||||||
g_date_time_unref(dt);
|
|
||||||
|
|
||||||
if (!log_path) {
|
if (!log_path) {
|
||||||
log_error("flatfile: could not determine log path for %s", contact);
|
log_error("flatfile: could not determine log path for %s", contact);
|
||||||
@@ -170,6 +427,12 @@ _flatfile_init(ProfAccount* account)
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (g_contact_states) {
|
||||||
|
g_hash_table_destroy(g_contact_states);
|
||||||
|
}
|
||||||
|
g_contact_states = g_hash_table_new_full(g_str_hash, g_str_equal,
|
||||||
|
g_free, (GDestroyNotify)ff_state_free);
|
||||||
|
|
||||||
log_info("Initialized flat-file database backend: %s", base_dir);
|
log_info("Initialized flat-file database backend: %s", base_dir);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
@@ -177,6 +440,10 @@ _flatfile_init(ProfAccount* account)
|
|||||||
static void
|
static void
|
||||||
_flatfile_close(void)
|
_flatfile_close(void)
|
||||||
{
|
{
|
||||||
|
if (g_contact_states) {
|
||||||
|
g_hash_table_destroy(g_contact_states);
|
||||||
|
g_contact_states = NULL;
|
||||||
|
}
|
||||||
g_free(g_flatfile_account_jid);
|
g_free(g_flatfile_account_jid);
|
||||||
g_flatfile_account_jid = NULL;
|
g_flatfile_account_jid = NULL;
|
||||||
log_debug("flatfile: closed");
|
log_debug("flatfile: closed");
|
||||||
@@ -235,113 +502,6 @@ _flatfile_add_outgoing_muc_pm(const char* const id, const char* const barejid,
|
|||||||
// Read logic
|
// Read logic
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
// List all log files for a contact, sorted by filename (date order)
|
|
||||||
static GSList*
|
|
||||||
_ff_list_log_files(const char* contact_barejid)
|
|
||||||
{
|
|
||||||
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
|
|
||||||
if (!contact_dir || !g_file_test(contact_dir, G_FILE_TEST_IS_DIR)) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
GDir* dir = g_dir_open(contact_dir, 0, NULL);
|
|
||||||
if (!dir)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
GSList* files = NULL;
|
|
||||||
const gchar* fname;
|
|
||||||
while ((fname = g_dir_read_name(dir)) != NULL) {
|
|
||||||
if (g_str_has_suffix(fname, ".log")) {
|
|
||||||
files = g_slist_insert_sorted(files,
|
|
||||||
g_strdup_printf("%s/%s", contact_dir, fname),
|
|
||||||
(GCompareFunc)g_strcmp0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_dir_close(dir);
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all parsed lines from a file, applying filters
|
|
||||||
static GSList*
|
|
||||||
_ff_read_file_messages(const char* filepath, const char* contact_barejid,
|
|
||||||
const char* start_time, const char* end_time,
|
|
||||||
const Jid* myjid)
|
|
||||||
{
|
|
||||||
GSList* messages = NULL;
|
|
||||||
|
|
||||||
FILE* fp = fopen(filepath, "r");
|
|
||||||
if (!fp)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
// BOM detection
|
|
||||||
int c1 = fgetc(fp);
|
|
||||||
int c2 = fgetc(fp);
|
|
||||||
int c3 = fgetc(fp);
|
|
||||||
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
|
|
||||||
// Not a BOM, rewind
|
|
||||||
fseek(fp, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* buf = NULL;
|
|
||||||
gboolean truncated = FALSE;
|
|
||||||
while ((buf = ff_readline(fp, &truncated)) != NULL) {
|
|
||||||
if (truncated) {
|
|
||||||
log_warning("flatfile: truncated line at EOF in %s, skipping", filepath);
|
|
||||||
free(buf);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
|
||||||
free(buf);
|
|
||||||
if (!pl)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check timestamp bounds
|
|
||||||
if (start_time) {
|
|
||||||
GDateTime* start_dt = g_date_time_new_from_iso8601(start_time, NULL);
|
|
||||||
if (start_dt) {
|
|
||||||
if (g_date_time_compare(pl->timestamp, start_dt) <= 0) {
|
|
||||||
g_date_time_unref(start_dt);
|
|
||||||
ff_parsed_line_free(pl);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
g_date_time_unref(start_dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (end_time) {
|
|
||||||
GDateTime* end_dt = g_date_time_new_from_iso8601(end_time, NULL);
|
|
||||||
if (end_dt) {
|
|
||||||
if (g_date_time_compare(pl->timestamp, end_dt) >= 0) {
|
|
||||||
g_date_time_unref(end_dt);
|
|
||||||
ff_parsed_line_free(pl);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
g_date_time_unref(end_dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check JID filter: message must involve contact and our JID
|
|
||||||
gboolean matches = FALSE;
|
|
||||||
if (myjid && myjid->barejid && contact_barejid) {
|
|
||||||
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0) || (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
|
|
||||||
matches = TRUE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matches = TRUE; // no filter
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
ff_parsed_line_free(pl);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages = g_slist_append(messages, pl);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply LMC: for messages with corrects:X, find original and replace its text
|
// Apply LMC: for messages with corrects:X, find original and replace its text
|
||||||
static void
|
static void
|
||||||
_ff_apply_lmc(GSList* parsed_lines, GSList** result)
|
_ff_apply_lmc(GSList* parsed_lines, GSList** result)
|
||||||
@@ -435,52 +595,160 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
|||||||
return DB_RESPONSE_ERROR;
|
return DB_RESPONSE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no end_time, use now
|
// Get or create per-contact state with sparse index
|
||||||
auto_gchar gchar* effective_end = NULL;
|
ff_contact_state_t* state = _ff_get_state(contact_barejid);
|
||||||
if (end_time) {
|
if (!state)
|
||||||
effective_end = g_strdup(end_time);
|
return DB_RESPONSE_ERROR;
|
||||||
} else {
|
|
||||||
GDateTime* now = g_date_time_new_now_local();
|
|
||||||
effective_end = g_date_time_format_iso8601(now);
|
|
||||||
g_date_time_unref(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all log files for this contact
|
// Ensure index is up-to-date (stat check -> build/extend if file changed)
|
||||||
GSList* files = _ff_list_log_files(contact_barejid);
|
if (!ff_state_ensure_fresh(state))
|
||||||
if (!files) {
|
|
||||||
return DB_RESPONSE_EMPTY;
|
return DB_RESPONSE_EMPTY;
|
||||||
|
if (state->total_lines == 0)
|
||||||
|
return DB_RESPONSE_EMPTY;
|
||||||
|
|
||||||
|
// Reset cursor for filtered (non-Page-Up) queries
|
||||||
|
if (start_time)
|
||||||
|
state->cursor_offset = -1;
|
||||||
|
|
||||||
|
// Determine read byte-range using the sparse index
|
||||||
|
off_t read_from = state->bom_len;
|
||||||
|
off_t read_to = state->stamp.size;
|
||||||
|
|
||||||
|
// Use stored cursor for sequential Page Up (skip bisect)
|
||||||
|
if (!start_time && end_time && state->cursor_offset >= 0) {
|
||||||
|
read_to = state->cursor_offset;
|
||||||
|
} else if (end_time) {
|
||||||
|
// Upper-bound: find first index entry AFTER end_time
|
||||||
|
GDateTime* edt = g_date_time_new_from_iso8601(end_time, NULL);
|
||||||
|
if (edt) {
|
||||||
|
gint64 end_epoch = g_date_time_to_unix(edt);
|
||||||
|
g_date_time_unref(edt);
|
||||||
|
size_t lo = 0, hi = state->n_entries;
|
||||||
|
while (lo < hi) {
|
||||||
|
size_t mid = lo + (hi - lo) / 2;
|
||||||
|
if (state->entries[mid].timestamp_epoch <= end_epoch)
|
||||||
|
lo = mid + 1;
|
||||||
|
else
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
// lo = first entry with timestamp > end_epoch
|
||||||
|
// Use next entry's offset as read_to (+ 1 entry margin)
|
||||||
|
if (lo + 1 < state->n_entries)
|
||||||
|
read_to = state->entries[lo + 1].byte_offset;
|
||||||
|
// else read_to stays at EOF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all matching parsed lines across files
|
if (start_time) {
|
||||||
|
read_from = ff_state_offset_for_time(state, start_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For "last N messages": back up from read_to using index
|
||||||
|
if (!from_start) {
|
||||||
|
int margin_entries = (MESSAGES_TO_RETRIEVE * 3) / FF_INDEX_STEP + 2;
|
||||||
|
|
||||||
|
// Find index entry closest to (but before) read_to
|
||||||
|
size_t entry_idx = 0;
|
||||||
|
for (size_t i = 0; i < state->n_entries; i++) {
|
||||||
|
if (state->entries[i].byte_offset >= read_to)
|
||||||
|
break;
|
||||||
|
entry_idx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t start_entry = entry_idx > (size_t)margin_entries
|
||||||
|
? entry_idx - margin_entries
|
||||||
|
: 0;
|
||||||
|
off_t backed = state->entries[start_entry].byte_offset;
|
||||||
|
if (backed > read_from)
|
||||||
|
read_from = backed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file and read lines in [read_from, read_to)
|
||||||
|
FILE* fp = fopen(state->filepath, "r");
|
||||||
|
if (!fp)
|
||||||
|
return DB_RESPONSE_EMPTY;
|
||||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
we should be careful with LMC, since it can be disabled by user, so this part needs further review and discussion. we should be careful with LMC, since it can be disabled by user, so this part needs further review and discussion.
jabber.developer2
commented
Corrected Corrected
|
|||||||
|
|
||||||
|
fseek(fp, read_from, SEEK_SET);
|
||||||
|
|
||||||
|
// Pre-parse time filter boundaries once (avoid per-line allocation)
|
||||||
|
GDateTime* start_dt = start_time ? g_date_time_new_from_iso8601(start_time, NULL) : NULL;
|
||||||
|
GDateTime* end_dt = end_time ? g_date_time_new_from_iso8601(end_time, NULL) : NULL;
|
||||||
|
|
||||||
GSList* all_parsed = NULL;
|
GSList* all_parsed = NULL;
|
||||||
for (GSList* f = files; f; f = f->next) {
|
while (1) {
|
||||||
GSList* file_msgs = _ff_read_file_messages(f->data, contact_barejid,
|
off_t pos = ftell(fp);
|
||||||
start_time, effective_end, myjid);
|
if (pos < 0 || pos >= read_to)
|
||||||
all_parsed = g_slist_concat(all_parsed, file_msgs);
|
break;
|
||||||
|
|
||||||
|
gboolean truncated = FALSE;
|
||||||
|
char* buf = ff_readline(fp, &truncated);
|
||||||
|
if (!buf)
|
||||||
|
break;
|
||||||
|
if (truncated) {
|
||||||
|
free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||||
|
free(buf);
|
||||||
|
if (!pl)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
pl->file_offset = pos;
|
||||||
|
|
||||||
|
// Time filters
|
||||||
|
if (start_dt && g_date_time_compare(pl->timestamp, start_dt) <= 0) {
|
||||||
|
ff_parsed_line_free(pl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (end_dt && g_date_time_compare(pl->timestamp, end_dt) >= 0) {
|
||||||
|
ff_parsed_line_free(pl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JID filter
|
||||||
|
gboolean matches = FALSE;
|
||||||
|
if (myjid->barejid && contact_barejid) {
|
||||||
|
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0)
|
||||||
|
|| (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
|
||||||
|
matches = TRUE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches = TRUE;
|
||||||
|
}
|
||||||
|
if (!matches) {
|
||||||
|
ff_parsed_line_free(pl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_parsed = g_slist_append(all_parsed, pl);
|
||||||
}
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
g_slist_free_full(files, g_free);
|
if (start_dt)
|
||||||
|
g_date_time_unref(start_dt);
|
||||||
|
if (end_dt)
|
||||||
|
g_date_time_unref(end_dt);
|
||||||
|
|
||||||
if (!all_parsed) {
|
if (!all_parsed)
|
||||||
return DB_RESPONSE_EMPTY;
|
return DB_RESPONSE_EMPTY;
|
||||||
}
|
|
||||||
|
// Update cursor: byte offset of oldest parsed message in this batch
|
||||||
|
ff_parsed_line_t* oldest = all_parsed->data;
|
||||||
|
state->cursor_offset = oldest->file_offset;
|
||||||
|
|
||||||
// Apply LMC corrections and build ProfMessage list
|
// Apply LMC corrections and build ProfMessage list
|
||||||
GSList* pre_result = NULL;
|
GSList* pre_result = NULL;
|
||||||
_ff_apply_lmc(all_parsed, &pre_result);
|
_ff_apply_lmc(all_parsed, &pre_result);
|
||||||
|
|
||||||
// Free parsed lines
|
|
||||||
g_slist_free_full(all_parsed, (GDestroyNotify)ff_parsed_line_free);
|
g_slist_free_full(all_parsed, (GDestroyNotify)ff_parsed_line_free);
|
||||||
|
|
||||||
if (!pre_result) {
|
if (!pre_result)
|
||||||
return DB_RESPONSE_EMPTY;
|
return DB_RESPONSE_EMPTY;
|
||||||
}
|
|
||||||
|
|
||||||
// Apply pagination: take first or last MESSAGES_TO_RETRIEVE
|
// Paginate: keep first or last MESSAGES_TO_RETRIEVE
|
||||||
guint total = g_slist_length(pre_result);
|
guint total = g_slist_length(pre_result);
|
||||||
if (total > MESSAGES_TO_RETRIEVE) {
|
if (total > MESSAGES_TO_RETRIEVE) {
|
||||||
if (from_start) {
|
if (from_start) {
|
||||||
// Keep first N, free rest
|
|
||||||
GSList* nth = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE);
|
GSList* nth = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE);
|
||||||
if (nth) {
|
if (nth) {
|
||||||
GSList* prev = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE - 1);
|
GSList* prev = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE - 1);
|
||||||
@@ -489,7 +757,6 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
|||||||
g_slist_free_full(nth, (GDestroyNotify)message_free);
|
g_slist_free_full(nth, (GDestroyNotify)message_free);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Keep last N, free first (total - N)
|
|
||||||
guint skip = total - MESSAGES_TO_RETRIEVE;
|
guint skip = total - MESSAGES_TO_RETRIEVE;
|
||||||
GSList* keep_start = g_slist_nth(pre_result, skip);
|
GSList* keep_start = g_slist_nth(pre_result, skip);
|
||||||
GSList* prev = g_slist_nth(pre_result, skip - 1);
|
GSList* prev = g_slist_nth(pre_result, skip - 1);
|
||||||
@@ -500,7 +767,6 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply flip (reverse order)
|
|
||||||
if (flip) {
|
if (flip) {
|
||||||
pre_result = g_slist_reverse(pre_result);
|
pre_result = g_slist_reverse(pre_result);
|
||||||
}
|
}
|
||||||
@@ -520,39 +786,32 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
GSList* files = _ff_list_log_files(contact_barejid);
|
ff_contact_state_t* state = _ff_get_state(contact_barejid);
|
||||||
if (!files) {
|
if (!state) {
|
||||||
if (is_last)
|
if (is_last)
|
||||||
msg->timestamp = g_date_time_new_now_utc();
|
msg->timestamp = g_date_time_new_now_utc();
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For first message: read first non-comment line of first file
|
if (!ff_state_ensure_fresh(state) || state->total_lines == 0) {
|
||||||
// For last message: read last non-comment line of last file
|
if (is_last)
|
||||||
const char* target_file = is_last ? (const char*)g_slist_last(files)->data
|
msg->timestamp = g_date_time_new_now_utc();
|
||||||
: (const char*)files->data;
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
FILE* fp = fopen(target_file, "r");
|
FILE* fp = fopen(state->filepath, "r");
|
||||||
if (!fp) {
|
if (!fp) {
|
||||||
g_slist_free_full(files, g_free);
|
|
||||||
if (is_last)
|
if (is_last)
|
||||||
msg->timestamp = g_date_time_new_now_utc();
|
msg->timestamp = g_date_time_new_now_utc();
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOM skip
|
|
||||||
int c1 = fgetc(fp);
|
|
||||||
int c2 = fgetc(fp);
|
|
||||||
int c3 = fgetc(fp);
|
|
||||||
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
|
|
||||||
fseek(fp, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* buf = NULL;
|
|
||||||
ff_parsed_line_t* found = NULL;
|
ff_parsed_line_t* found = NULL;
|
||||||
|
|
||||||
if (!is_last) {
|
if (!is_last) {
|
||||||
// Find first valid line
|
// First message: start from beginning (skip BOM)
|
||||||
|
fseek(fp, state->bom_len, SEEK_SET);
|
||||||
|
char* buf;
|
||||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||||
found = ff_parse_line(buf);
|
found = ff_parse_line(buf);
|
||||||
free(buf);
|
free(buf);
|
||||||
@@ -560,7 +819,13 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Find last valid line — read all and keep last
|
// Last message: seek near end using index for efficiency
|
||||||
|
off_t seek_pos = 0;
|
||||||
|
if (state->n_entries > 0)
|
||||||
|
seek_pos = state->entries[state->n_entries - 1].byte_offset;
|
||||||
|
fseek(fp, seek_pos, SEEK_SET);
|
||||||
|
|
||||||
|
char* buf;
|
||||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||||
free(buf);
|
free(buf);
|
||||||
@@ -581,7 +846,6 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
|||||||
msg->timestamp = g_date_time_new_now_utc();
|
msg->timestamp = g_date_time_new_now_utc();
|
||||||
}
|
}
|
||||||
|
|
||||||
g_slist_free_full(files, g_free);
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
#include "database.h"
|
#include "database.h"
|
||||||
#include "xmpp/xmpp.h"
|
#include "xmpp/xmpp.h"
|
||||||
@@ -60,8 +61,44 @@ typedef struct
|
|||||||
char* from_jid;
|
char* from_jid;
|
||||||
char* from_resource;
|
char* from_resource;
|
||||||
char* message;
|
char* message;
|
||||||
|
off_t file_offset;
|
||||||
} ff_parsed_line_t;
|
} ff_parsed_line_t;
|
||||||
|
|
||||||
|
// --- Sparse index for single-file lookup ---
|
||||||
|
|
||||||
|
#define FF_INDEX_STEP 500
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
off_t byte_offset;
|
||||||
|
gint64 timestamp_epoch;
|
||||||
|
} ff_index_entry_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
time_t mtime;
|
||||||
|
off_t size;
|
||||||
|
ino_t inode;
|
||||||
|
} ff_file_stamp_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char* filepath;
|
||||||
|
ff_index_entry_t* entries;
|
||||||
|
size_t n_entries;
|
||||||
|
size_t cap_entries;
|
||||||
|
size_t total_lines;
|
||||||
|
ff_file_stamp_t stamp;
|
||||||
|
off_t cursor_offset;
|
||||||
|
int bom_len;
|
||||||
|
} ff_contact_state_t;
|
||||||
|
|
||||||
|
// State management
|
||||||
|
ff_contact_state_t* ff_state_new(const char* filepath);
|
||||||
|
void ff_state_free(ff_contact_state_t* state);
|
||||||
|
gboolean ff_state_ensure_fresh(ff_contact_state_t* state);
|
||||||
|
off_t ff_state_offset_for_time(ff_contact_state_t* state, const char* iso_time);
|
||||||
|
|
||||||
// --- Type conversion helpers ---
|
// --- Type conversion helpers ---
|
||||||
|
|
||||||
const char* ff_get_message_type_str(prof_msg_type_t type);
|
const char* ff_get_message_type_str(prof_msg_type_t type);
|
||||||
@@ -73,7 +110,7 @@ prof_enc_t ff_get_message_enc_type(const char* const encstr);
|
|||||||
|
|
||||||
char* ff_jid_to_dir(const char* jid);
|
char* ff_jid_to_dir(const char* jid);
|
||||||
char* ff_get_contact_dir(const char* contact_barejid);
|
char* ff_get_contact_dir(const char* contact_barejid);
|
||||||
char* ff_get_log_path(const char* contact_barejid, GDateTime* dt);
|
char* ff_get_log_path(const char* contact_barejid);
|
||||||
gboolean ff_ensure_dir(const char* path);
|
gboolean ff_ensure_dir(const char* path);
|
||||||
|
|
||||||
// --- Escape / unescape ---
|
// --- Escape / unescape ---
|
||||||
|
|||||||
@@ -167,17 +167,15 @@ ff_get_contact_dir(const char* contact_barejid)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the log file path for a contact on a specific date:
|
// Get the single log file path for a contact: {contact_dir}/history.log
|
||||||
// {contact_dir}/{YYYY_MM_DD}.log
|
|
||||||
char*
|
char*
|
||||||
ff_get_log_path(const char* contact_barejid, GDateTime* dt)
|
ff_get_log_path(const char* contact_barejid)
|
||||||
{
|
{
|
||||||
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
|
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
|
||||||
if (!contact_dir)
|
if (!contact_dir)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
auto_gchar gchar* date_str = g_date_time_format(dt, "%Y_%m_%d");
|
char* result = g_strdup_printf("%s/history.log", contact_dir);
|
||||||
char* result = g_strdup_printf("%s/%s.log", contact_dir, date_str);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +614,7 @@ ff_parse_line(const char* line)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ff_parsed_line_t* result = g_malloc0(sizeof(ff_parsed_line_t));
|
ff_parsed_line_t* result = g_malloc0(sizeof(ff_parsed_line_t));
|
||||||
|
result->file_offset = -1;
|
||||||
|
|
||||||
// Parse timestamp — everything up to first space followed by '['
|
// Parse timestamp — everything up to first space followed by '['
|
||||||
char* bracket_start = strchr(work, '[');
|
char* bracket_start = strchr(work, '[');
|
||||||
|
|||||||
@@ -89,253 +89,185 @@ ff_verify_integrity(const gchar* const contact_barejid)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify each contact directory
|
// Verify each contact directory (single history.log per contact)
|
||||||
for (GSList* cd = contact_dirs; cd; cd = cd->next) {
|
for (GSList* cd = contact_dirs; cd; cd = cd->next) {
|
||||||
const char* cdir_path = cd->data;
|
const char* cdir_path = cd->data;
|
||||||
|
|
||||||
GDir* dir = g_dir_open(cdir_path, 0, NULL);
|
auto_gchar gchar* filepath = g_strdup_printf("%s/history.log", cdir_path);
|
||||||
if (!dir)
|
if (!g_file_test(filepath, G_FILE_TEST_EXISTS))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
GSList* log_files = NULL;
|
const char* basename = "history.log";
|
||||||
const gchar* fname;
|
|
||||||
while ((fname = g_dir_read_name(dir)) != NULL) {
|
|
||||||
if (g_str_has_suffix(fname, ".log")) {
|
|
||||||
log_files = g_slist_insert_sorted(log_files,
|
|
||||||
g_strdup_printf("%s/%s", cdir_path, fname),
|
|
||||||
(GCompareFunc)g_strcmp0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_dir_close(dir);
|
|
||||||
|
|
||||||
GDateTime* prev_file_last_ts = NULL;
|
// Check file permissions
|
||||||
GHashTable* seen_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
struct stat st;
|
||||||
GHashTable* all_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
if (g_stat(filepath, &st) == 0) {
|
||||||
|
if ((st.st_mode & 0777) != (S_IRUSR | S_IWUSR)) {
|
||||||
for (GSList* lf = log_files; lf; lf = lf->next) {
|
|
||||||
const char* filepath = lf->data;
|
|
||||||
const char* basename = strrchr(filepath, '/');
|
|
||||||
basename = basename ? basename + 1 : filepath;
|
|
||||||
|
|
||||||
// Check file permissions
|
|
||||||
struct stat st;
|
|
||||||
if (g_stat(filepath, &st) == 0) {
|
|
||||||
if ((st.st_mode & 0777) != (S_IRUSR | S_IWUSR)) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_WARNING;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = 0;
|
|
||||||
issue->message = g_strdup_printf("File permissions are %o, expected 600 (sensitive data)", st.st_mode & 0777);
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FILE* fp = fopen(filepath, "r");
|
|
||||||
if (!fp)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// BOM check
|
|
||||||
int c1 = fgetc(fp);
|
|
||||||
int c2 = fgetc(fp);
|
|
||||||
int c3 = fgetc(fp);
|
|
||||||
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_INFO;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = 0;
|
|
||||||
issue->message = g_strdup("File has UTF-8 BOM — harmless but unnecessary");
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
} else {
|
|
||||||
fseek(fp, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* buf = NULL;
|
|
||||||
int lineno = 0;
|
|
||||||
GDateTime* prev_ts = NULL;
|
|
||||||
GDateTime* first_ts = NULL;
|
|
||||||
GDateTime* last_ts = NULL;
|
|
||||||
gboolean has_crlf = FALSE;
|
|
||||||
gboolean is_empty = TRUE;
|
|
||||||
|
|
||||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
|
||||||
lineno++;
|
|
||||||
gsize len = strlen(buf);
|
|
||||||
|
|
||||||
// CRLF check
|
|
||||||
if (len > 0 && buf[len - 1] == '\r') {
|
|
||||||
has_crlf = TRUE;
|
|
||||||
buf[--len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if (len == 0 || buf[0] == '#') {
|
|
||||||
free(buf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
is_empty = FALSE;
|
|
||||||
|
|
||||||
// UTF-8 validation
|
|
||||||
const gchar* end;
|
|
||||||
if (!g_utf8_validate(buf, -1, &end)) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_ERROR;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup_printf("Invalid UTF-8 at byte offset %ld", (long)(end - buf));
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
free(buf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control character check
|
|
||||||
for (gsize i = 0; i < len; i++) {
|
|
||||||
unsigned char ch = (unsigned char)buf[i];
|
|
||||||
if (ch < 0x20 && ch != '\t') {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_WARNING;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup_printf("Contains control character 0x%02x", ch);
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse line
|
|
||||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
|
||||||
if (!pl) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_ERROR;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup("Unparsable line");
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
free(buf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(buf); // done with raw line
|
|
||||||
|
|
||||||
if (!first_ts)
|
|
||||||
first_ts = g_date_time_ref(pl->timestamp);
|
|
||||||
if (last_ts)
|
|
||||||
g_date_time_unref(last_ts);
|
|
||||||
last_ts = g_date_time_ref(pl->timestamp);
|
|
||||||
|
|
||||||
// Timestamp order within file
|
|
||||||
if (prev_ts && g_date_time_compare(pl->timestamp, prev_ts) < 0) {
|
|
||||||
auto_gchar gchar* ts_cur = g_date_time_format_iso8601(pl->timestamp);
|
|
||||||
auto_gchar gchar* ts_prev = g_date_time_format_iso8601(prev_ts);
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_WARNING;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup_printf("Timestamp out of order (%s after %s)", ts_cur, ts_prev);
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
}
|
|
||||||
if (prev_ts)
|
|
||||||
g_date_time_unref(prev_ts);
|
|
||||||
prev_ts = g_date_time_ref(pl->timestamp);
|
|
||||||
|
|
||||||
// Duplicate stanza-id / archive-id
|
|
||||||
if (pl->stanza_id && strlen(pl->stanza_id) > 0) {
|
|
||||||
if (g_hash_table_contains(seen_ids, pl->stanza_id)) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_WARNING;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id);
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
} else {
|
|
||||||
g_hash_table_insert(seen_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
|
||||||
}
|
|
||||||
g_hash_table_insert(all_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
|
||||||
}
|
|
||||||
if (pl->archive_id && strlen(pl->archive_id) > 0) {
|
|
||||||
if (g_hash_table_contains(seen_ids, pl->archive_id)) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
|
||||||
issue->level = INTEGRITY_WARNING;
|
|
||||||
issue->file = g_strdup(basename);
|
|
||||||
issue->line = lineno;
|
|
||||||
issue->message = g_strdup_printf("Duplicate archive-id \"%s\"", pl->archive_id);
|
|
||||||
issues = g_slist_append(issues, issue);
|
|
||||||
} else {
|
|
||||||
g_hash_table_insert(seen_ids, g_strdup(pl->archive_id), GINT_TO_POINTER(lineno));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ff_parsed_line_free(pl);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
// CRLF warning for file
|
|
||||||
if (has_crlf) {
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_WARNING;
|
issue->level = INTEGRITY_WARNING;
|
||||||
issue->file = g_strdup(basename);
|
issue->file = g_strdup(basename);
|
||||||
issue->line = 0;
|
issue->line = 0;
|
||||||
issue->message = g_strdup("File uses Windows line endings (CRLF) — consider converting to LF");
|
issue->message = g_strdup_printf("File permissions are %o, expected 600 (sensitive data)", st.st_mode & 0777);
|
||||||
issues = g_slist_append(issues, issue);
|
issues = g_slist_append(issues, issue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Empty file
|
FILE* fp = fopen(filepath, "r");
|
||||||
if (is_empty) {
|
if (!fp)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// BOM check
|
||||||
|
int c1 = fgetc(fp);
|
||||||
|
int c2 = fgetc(fp);
|
||||||
|
int c3 = fgetc(fp);
|
||||||
|
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
|
||||||
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_INFO;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = 0;
|
||||||
|
issue->message = g_strdup("File has UTF-8 BOM — harmless but unnecessary");
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
} else {
|
||||||
|
fseek(fp, 0, SEEK_SET);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* buf = NULL;
|
||||||
|
int lineno = 0;
|
||||||
|
GDateTime* prev_ts = NULL;
|
||||||
|
GHashTable* seen_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||||
|
GHashTable* all_stanza_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||||
|
gboolean has_crlf = FALSE;
|
||||||
|
gboolean is_empty = TRUE;
|
||||||
|
|
||||||
|
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||||
|
lineno++;
|
||||||
|
gsize len = strlen(buf);
|
||||||
|
|
||||||
|
if (len > 0 && buf[len - 1] == '\r') {
|
||||||
|
has_crlf = TRUE;
|
||||||
|
buf[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 0 || buf[0] == '#') {
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
is_empty = FALSE;
|
||||||
|
|
||||||
|
const gchar* end;
|
||||||
|
if (!g_utf8_validate(buf, -1, &end)) {
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_INFO;
|
issue->level = INTEGRITY_ERROR;
|
||||||
issue->file = g_strdup(basename);
|
issue->file = g_strdup(basename);
|
||||||
issue->line = 0;
|
issue->line = lineno;
|
||||||
issue->message = g_strdup("File is empty (no message lines)");
|
issue->message = g_strdup_printf("Invalid UTF-8 at byte offset %ld", (long)(end - buf));
|
||||||
issues = g_slist_append(issues, issue);
|
issues = g_slist_append(issues, issue);
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-file timestamp ordering
|
for (gsize i = 0; i < len; i++) {
|
||||||
if (first_ts && prev_file_last_ts) {
|
unsigned char ch = (unsigned char)buf[i];
|
||||||
if (g_date_time_compare(first_ts, prev_file_last_ts) < 0) {
|
if (ch < 0x20 && ch != '\t') {
|
||||||
auto_gchar gchar* ts_first = g_date_time_format_iso8601(first_ts);
|
|
||||||
auto_gchar gchar* ts_prev_last = g_date_time_format_iso8601(prev_file_last_ts);
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_WARNING;
|
issue->level = INTEGRITY_WARNING;
|
||||||
issue->file = g_strdup(basename);
|
issue->file = g_strdup(basename);
|
||||||
issue->line = 0;
|
issue->line = lineno;
|
||||||
issue->message = g_strdup_printf("First timestamp (%s) is before previous file's last timestamp (%s)",
|
issue->message = g_strdup_printf("Contains control character 0x%02x", ch);
|
||||||
ts_first, ts_prev_last);
|
|
||||||
issues = g_slist_append(issues, issue);
|
issues = g_slist_append(issues, issue);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev_file_last_ts)
|
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||||
g_date_time_unref(prev_file_last_ts);
|
if (!pl) {
|
||||||
prev_file_last_ts = last_ts ? g_date_time_ref(last_ts) : NULL;
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_ERROR;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = lineno;
|
||||||
|
issue->message = g_strdup("Unparsable line");
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
|
||||||
|
// Timestamp ordering
|
||||||
|
if (prev_ts && g_date_time_compare(pl->timestamp, prev_ts) < 0) {
|
||||||
|
auto_gchar gchar* ts_cur = g_date_time_format_iso8601(pl->timestamp);
|
||||||
|
auto_gchar gchar* ts_prev = g_date_time_format_iso8601(prev_ts);
|
||||||
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_WARNING;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = lineno;
|
||||||
|
issue->message = g_strdup_printf("Timestamp out of order (%s after %s)", ts_cur, ts_prev);
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
}
|
||||||
if (prev_ts)
|
if (prev_ts)
|
||||||
g_date_time_unref(prev_ts);
|
g_date_time_unref(prev_ts);
|
||||||
if (first_ts)
|
prev_ts = g_date_time_ref(pl->timestamp);
|
||||||
g_date_time_unref(first_ts);
|
|
||||||
if (last_ts)
|
// Duplicate stanza-id / archive-id
|
||||||
g_date_time_unref(last_ts);
|
if (pl->stanza_id && strlen(pl->stanza_id) > 0) {
|
||||||
|
if (g_hash_table_contains(seen_ids, pl->stanza_id)) {
|
||||||
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_WARNING;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = lineno;
|
||||||
|
issue->message = g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id);
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
} else {
|
||||||
|
g_hash_table_insert(seen_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||||
|
}
|
||||||
|
g_hash_table_insert(all_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||||
|
}
|
||||||
|
if (pl->archive_id && strlen(pl->archive_id) > 0) {
|
||||||
|
if (g_hash_table_contains(seen_ids, pl->archive_id)) {
|
||||||
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_WARNING;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = lineno;
|
||||||
|
issue->message = g_strdup_printf("Duplicate archive-id \"%s\"", pl->archive_id);
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
} else {
|
||||||
|
g_hash_table_insert(seen_ids, g_strdup(pl->archive_id), GINT_TO_POINTER(lineno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ff_parsed_line_free(pl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: check LMC references across all files
|
fclose(fp);
|
||||||
for (GSList* lf = log_files; lf; lf = lf->next) {
|
|
||||||
const char* filepath = lf->data;
|
|
||||||
const char* basename_lmc = strrchr(filepath, '/');
|
|
||||||
basename_lmc = basename_lmc ? basename_lmc + 1 : filepath;
|
|
||||||
|
|
||||||
FILE* fp = fopen(filepath, "r");
|
if (has_crlf) {
|
||||||
if (!fp)
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
continue;
|
issue->level = INTEGRITY_WARNING;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = 0;
|
||||||
|
issue->message = g_strdup("File uses Windows line endings (CRLF) — consider converting to LF");
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip BOM
|
if (is_empty) {
|
||||||
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
|
issue->level = INTEGRITY_INFO;
|
||||||
|
issue->file = g_strdup(basename);
|
||||||
|
issue->line = 0;
|
||||||
|
issue->message = g_strdup("File is empty (no message lines)");
|
||||||
|
issues = g_slist_append(issues, issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: check LMC references
|
||||||
|
fp = fopen(filepath, "r");
|
||||||
|
if (fp) {
|
||||||
int b1 = fgetc(fp);
|
int b1 = fgetc(fp);
|
||||||
int b2 = fgetc(fp);
|
int b2 = fgetc(fp);
|
||||||
int b3 = fgetc(fp);
|
int b3 = fgetc(fp);
|
||||||
if (!(b1 == 0xEF && b2 == 0xBB && b3 == 0xBF)) {
|
if (!(b1 == 0xEF && b2 == 0xBB && b3 == 0xBF))
|
||||||
fseek(fp, 0, SEEK_SET);
|
fseek(fp, 0, SEEK_SET);
|
||||||
}
|
|
||||||
|
|
||||||
char* buf = NULL;
|
lineno = 0;
|
||||||
int lineno = 0;
|
|
||||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||||
lineno++;
|
lineno++;
|
||||||
gsize len = strlen(buf);
|
gsize len = strlen(buf);
|
||||||
@@ -355,7 +287,7 @@ ff_verify_integrity(const gchar* const contact_barejid)
|
|||||||
if (!g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
|
if (!g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_ERROR;
|
issue->level = INTEGRITY_ERROR;
|
||||||
issue->file = g_strdup(basename_lmc);
|
issue->file = g_strdup(basename);
|
||||||
issue->line = lineno;
|
issue->line = lineno;
|
||||||
issue->message = g_strdup_printf("Broken correction reference: corrects:%s not found", pl->replace_id);
|
issue->message = g_strdup_printf("Broken correction reference: corrects:%s not found", pl->replace_id);
|
||||||
issues = g_slist_append(issues, issue);
|
issues = g_slist_append(issues, issue);
|
||||||
@@ -366,11 +298,10 @@ ff_verify_integrity(const gchar* const contact_barejid)
|
|||||||
fclose(fp);
|
fclose(fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev_file_last_ts)
|
if (prev_ts)
|
||||||
g_date_time_unref(prev_file_last_ts);
|
g_date_time_unref(prev_ts);
|
||||||
g_hash_table_destroy(seen_ids);
|
g_hash_table_destroy(seen_ids);
|
||||||
g_hash_table_destroy(all_stanza_ids);
|
g_hash_table_destroy(all_stanza_ids);
|
||||||
g_slist_free_full(log_files, g_free);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g_slist_free_full(contact_dirs, g_free);
|
g_slist_free_full(contact_dirs, g_free);
|
||||||
|
|||||||
Reference in New Issue
Block a user
general note: LMC is an optional feature. If it's disabled and the log is refetched, it should not be applied.
Corrected