fix(review): address PR #94 reviewer comments
Blockers: - SPDX/CProof copyright headers (7 files) - stanza-id non-uniqueness: warn-only, don't skip (matches SQLite) - fopen → open(O_EXCL|O_NOFOLLOW, 0600) + fdopen in export Medium: - log_info → log_warning for SQLite fallback - remove prefs_save() auto-save from backend switch - vtable null → log_warning in 4 dispatch functions - log_debug → log_warning + null error handling in export - rename failure → cons_show_error to user - remove unused total_skipped variable - LMC respects PREF_CORRECTION_ALLOW Low nits: - man page .SS fix + jabber.space URL - profrc.example simplified - show flatfile path on /logging flatfile - EXPORT_PROGRESS_INTERVAL constant (magic 500) - FF_META_PREFIX_ID/AID constants + FF_INDEX_STEP comment - buffer overflow guard on close[1]/close[2] - for-loop → memchr for JID resource scan - BOM check extracted to ff_skip_bom() (DRY) - index building extracted to _ff_maybe_index_line() - O_APPEND+O_EXCL comment rewritten - Makefile.am spaces → tabs
This commit is contained in:
@@ -195,9 +195,9 @@ functionaltest_sources = \
|
||||
tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \
|
||||
tests/functionaltests/test_history.c tests/functionaltests/test_history.h \
|
||||
tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \
|
||||
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
|
||||
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
|
||||
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
|
||||
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
|
||||
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
|
||||
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
|
||||
tests/functionaltests/functionaltests.c
|
||||
|
||||
main_source = src/main.c
|
||||
|
||||
@@ -197,7 +197,8 @@ Configuration for
|
||||
.B Profanity
|
||||
is stored in
|
||||
.I $XDG_CONFIG_HOME/profanity/profrc
|
||||
, details on commands for configuring Profanity can be found at <https://profanity-im.github.io/reference.html> or the respective built\-in help or man pages..SS Message History Storage
|
||||
, details on commands for configuring Profanity can be found at <https://jabber.space/command-reference/> or the respective built\-in help or man pages.
|
||||
.SS Message History Storage
|
||||
By default, message history is stored in an SQLite database. An alternative flat-file backend
|
||||
stores messages as human-readable plain text files that can be edited with any text editor.
|
||||
.PP
|
||||
|
||||
@@ -49,12 +49,7 @@ grlog=true
|
||||
maxsize=1048580
|
||||
rotate=true
|
||||
shared=true
|
||||
# Database backend for message history:
|
||||
# on - SQLite database (default)
|
||||
# off - no message logging
|
||||
# redact - store with redacted message content
|
||||
# flatfile - plain text files, editable with any text editor
|
||||
# stored in ~/.local/share/profanity/flatlog/
|
||||
# Database backend: on (SQLite), off, redact, flatfile
|
||||
dblog=on
|
||||
|
||||
[otr]
|
||||
|
||||
@@ -6687,7 +6687,9 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
cons_show("Messages are going to be redacted.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
} else if (g_strcmp0(arg, "flatfile") == 0) {
|
||||
auto_gchar gchar* ff_path = files_get_data_path(DIR_FLATLOG);
|
||||
cons_show("Using flat-file backend for message logging. Takes effect on next connection.");
|
||||
cons_show("Flatfile directory: %s", ff_path);
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
prefs_set_boolean(PREF_HISTORY, TRUE);
|
||||
|
||||
@@ -75,7 +75,7 @@ log_database_init(ProfAccount* account)
|
||||
active_db_backend = db_backend_sqlite();
|
||||
log_info("Using SQLite database backend");
|
||||
#else
|
||||
log_info("SQLite not compiled in, falling back to flat-file backend");
|
||||
log_warning("SQLite not compiled in, falling back to flat-file backend");
|
||||
active_db_backend = db_backend_flatfile();
|
||||
#endif
|
||||
}
|
||||
@@ -103,9 +103,8 @@ log_database_switch_backend(const char* new_backend)
|
||||
// Close current backend
|
||||
log_database_close();
|
||||
|
||||
// Update preference
|
||||
// Update preference (user must /save to persist across restarts)
|
||||
prefs_set_string(PREF_DBLOG, new_backend);
|
||||
prefs_save();
|
||||
|
||||
// Get current account to reinitialize
|
||||
const char* account_name = session_get_account_name();
|
||||
@@ -130,6 +129,8 @@ log_database_add_incoming(ProfMessage* message)
|
||||
{
|
||||
if (active_db_backend && active_db_backend->add_incoming) {
|
||||
active_db_backend->add_incoming(message);
|
||||
} else {
|
||||
log_warning("log_database_add_incoming: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +139,8 @@ log_database_add_outgoing_chat(const char* const id, const char* const barejid,
|
||||
{
|
||||
if (active_db_backend && active_db_backend->add_outgoing_chat) {
|
||||
active_db_backend->add_outgoing_chat(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_chat: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +149,8 @@ log_database_add_outgoing_muc(const char* const id, const char* const barejid, c
|
||||
{
|
||||
if (active_db_backend && active_db_backend->add_outgoing_muc) {
|
||||
active_db_backend->add_outgoing_muc(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_muc: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +159,8 @@ log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid
|
||||
{
|
||||
if (active_db_backend && active_db_backend->add_outgoing_muc_pm) {
|
||||
active_db_backend->add_outgoing_muc_pm(id, barejid, message, replace_id, enc);
|
||||
} else {
|
||||
log_warning("log_database_add_outgoing_muc_pm: no backend or method available");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_export.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Cross-backend export/import for message history.
|
||||
* Reads from one backend, writes to the other, with merge + dedup.
|
||||
*/
|
||||
@@ -30,6 +18,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
@@ -54,6 +43,9 @@
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
|
||||
// Print progress to console every N messages during export/import
|
||||
#define EXPORT_PROGRESS_INTERVAL 500
|
||||
|
||||
// =========================================================================
|
||||
// Dedup key: stanza_id if present, else hash of timestamp+from+body
|
||||
// =========================================================================
|
||||
@@ -106,8 +98,10 @@ _ff_list_contacts(void)
|
||||
GDir* dir = g_dir_open(base_dir, 0, &err);
|
||||
if (!dir) {
|
||||
if (err) {
|
||||
log_debug("export: cannot open flatlog dir %s: %s", base_dir, err->message);
|
||||
log_warning("export: cannot open flatlog dir %s: %s", base_dir, err->message);
|
||||
g_error_free(err);
|
||||
} else {
|
||||
log_warning("export: cannot open flatlog dir %s (no error details)", base_dir);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -237,7 +231,6 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
|
||||
int total_exported = 0;
|
||||
int total_skipped = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
@@ -277,14 +270,34 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
ff_ensure_dir(dir);
|
||||
|
||||
auto_gchar gchar* tmp_path = g_strdup_printf("%s.export.tmp", log_path);
|
||||
FILE* fp = fopen(tmp_path, "w");
|
||||
if (!fp) {
|
||||
|
||||
// Use open() with O_NOFOLLOW|O_EXCL to prevent symlink attacks,
|
||||
// and explicit 0600 permissions (chat history is private).
|
||||
int tmp_fd = open(tmp_path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, S_IRUSR | S_IWUSR);
|
||||
if (tmp_fd < 0) {
|
||||
if (errno == EEXIST) {
|
||||
// Stale temp file from previous failed export — remove and retry
|
||||
unlink(tmp_path);
|
||||
tmp_fd = open(tmp_path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, S_IRUSR | S_IWUSR);
|
||||
}
|
||||
}
|
||||
if (tmp_fd < 0) {
|
||||
log_error("export: cannot create %s: %s", tmp_path, strerror(errno));
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
continue;
|
||||
}
|
||||
FILE* fp = fdopen(tmp_fd, "w");
|
||||
if (!fp) {
|
||||
log_error("export: fdopen failed for %s: %s", tmp_path, strerror(errno));
|
||||
close(tmp_fd);
|
||||
unlink(tmp_path);
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lock the temp file
|
||||
int fd = fileno(fp);
|
||||
@@ -372,7 +385,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
pl->to_jid, pl->to_resource, pl->marked_read,
|
||||
pl->message);
|
||||
written++;
|
||||
if (written % 500 == 0) {
|
||||
if (written % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d written so far", contact, written, (int)g_slist_length(merged));
|
||||
}
|
||||
}
|
||||
@@ -386,14 +399,14 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
// Atomic rename
|
||||
if (rename(tmp_path, log_path) != 0) {
|
||||
log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno));
|
||||
cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno));
|
||||
unlink(tmp_path);
|
||||
} else {
|
||||
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
|
||||
contact, contact_exported, contact_skipped, existing_count);
|
||||
total_exported += contact_exported;
|
||||
}
|
||||
|
||||
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
|
||||
contact, contact_exported, contact_skipped, existing_count);
|
||||
total_exported += contact_exported;
|
||||
total_skipped += contact_skipped;
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
@@ -439,7 +452,6 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
|
||||
int total_imported = 0;
|
||||
int total_skipped = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
@@ -531,7 +543,7 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
contact_imported++;
|
||||
|
||||
if (contact_imported % 500 == 0) {
|
||||
if (contact_imported % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d imported so far", contact, contact_imported, total_lines);
|
||||
}
|
||||
}
|
||||
@@ -546,7 +558,6 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
cons_show("Imported %s: %d new, %d skipped (already in SQLite)",
|
||||
contact, contact_imported, contact_skipped);
|
||||
total_imported += contact_imported;
|
||||
total_skipped += contact_skipped;
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(ff_lines, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Flat-file database backend: init/close, message write, message read,
|
||||
* LMC correction logic, and the backend vtable.
|
||||
*
|
||||
@@ -110,10 +98,10 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
|
||||
if (parts) {
|
||||
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] + 3);
|
||||
} else if (g_str_has_prefix(parts[i], "aid:") && !archive_id) {
|
||||
archive_id = ff_unescape_meta_value(parts[i] + 4);
|
||||
if (g_str_has_prefix(parts[i], FF_META_PREFIX_ID) && !stanza_id) {
|
||||
stanza_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_ID));
|
||||
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_AID) && !archive_id) {
|
||||
archive_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_AID));
|
||||
}
|
||||
}
|
||||
g_strfreev(parts);
|
||||
@@ -121,17 +109,12 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
|
||||
// Extract from_jid (needed for stanza_senders map): after "] " before "/" or ": "
|
||||
char* from_jid = NULL;
|
||||
if (stanza_id && *(close + 1) == ' ') {
|
||||
if (stanza_id && close[1] != '\0' && close[1] == ' ' && close[2] != '\0') {
|
||||
const char* sender_start = close + 2;
|
||||
const char* colonspace = ff_find_unescaped_colonspace(sender_start);
|
||||
if (colonspace) {
|
||||
const char* jid_end = colonspace;
|
||||
for (const char* p = sender_start; p < colonspace; p++) {
|
||||
if (*p == '/') {
|
||||
jid_end = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const char* slash = memchr(sender_start, '/', colonspace - sender_start);
|
||||
const char* jid_end = slash ? slash : colonspace;
|
||||
from_jid = g_strndup(sender_start, jid_end - sender_start);
|
||||
}
|
||||
}
|
||||
@@ -150,6 +133,36 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to add an index entry for the current line.
|
||||
// Called every FF_INDEX_STEP messages during build/extend.
|
||||
static void
|
||||
_ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
|
||||
{
|
||||
char* space = strchr(buf, ' ');
|
||||
if (!space)
|
||||
return;
|
||||
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||
if (!dt) {
|
||||
log_warning("flatfile: unparseable timestamp in %s at offset %ld: %s",
|
||||
state->filepath, (long)pos, ts_str);
|
||||
g_free(ts_str);
|
||||
return;
|
||||
}
|
||||
g_free(ts_str);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_ff_state_build(ff_contact_state_t* state)
|
||||
{
|
||||
@@ -157,15 +170,7 @@ _ff_state_build(ff_contact_state_t* state)
|
||||
if (!fp)
|
||||
return FALSE;
|
||||
|
||||
int c1 = fgetc(fp);
|
||||
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->bom_len = ff_skip_bom(fp);
|
||||
|
||||
state->n_entries = 0;
|
||||
state->total_lines = 0;
|
||||
@@ -197,23 +202,7 @@ _ff_state_build(ff_contact_state_t* state)
|
||||
_ff_cache_line_ids(state, buf);
|
||||
|
||||
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);
|
||||
}
|
||||
_ff_maybe_index_line(state, buf, pos);
|
||||
}
|
||||
msg_count++;
|
||||
free(buf);
|
||||
@@ -284,23 +273,7 @@ _ff_state_extend(ff_contact_state_t* state)
|
||||
_ff_cache_line_ids(state, buf);
|
||||
|
||||
if (new_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);
|
||||
}
|
||||
_ff_maybe_index_line(state, buf, pos);
|
||||
}
|
||||
new_count++;
|
||||
free(buf);
|
||||
@@ -445,9 +418,10 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file with O_NOFOLLOW to prevent symlink attacks,
|
||||
// and O_APPEND for safe concurrent appends.
|
||||
// Open the file with O_NOFOLLOW to prevent symlink attacks.
|
||||
// Try O_CREAT|O_EXCL first to detect new files atomically (no TOCTOU).
|
||||
// O_APPEND is included for consistency — it has no effect on a new file
|
||||
// but ensures append semantics if the fd is inherited after the fallback.
|
||||
int fd = open(log_path, O_WRONLY | O_APPEND | O_CREAT | O_EXCL | O_NOFOLLOW,
|
||||
S_IRUSR | S_IWUSR);
|
||||
gboolean is_new = (fd >= 0);
|
||||
@@ -601,14 +575,15 @@ _flatfile_add_incoming(ProfMessage* message)
|
||||
} else {
|
||||
contact = message->from_jid->barejid;
|
||||
}
|
||||
// MAM dedup: skip if this stanza-id already exists in the log (XEP-0313 replay)
|
||||
// Stanza-id duplicate warning (non-blocking).
|
||||
// XEP-0359 stanza-ids SHOULD be unique per server, but older clients
|
||||
// (Pidgin, Adium, old Profanity) may use incremental IDs that servers
|
||||
// echo back, causing false positives. Log but do NOT skip — matches
|
||||
// SQLite backend behavior. See https://github.com/profanity-im/profanity/issues/1899
|
||||
if (message->stanzaid && !message->is_mam) {
|
||||
if (_ff_has_archive_id(contact, message->stanzaid)) {
|
||||
log_error("flatfile: duplicate stanza-id '%s' from %s, skipping",
|
||||
message->stanzaid, message->from_jid->barejid);
|
||||
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.",
|
||||
message->from_jid->fulljid);
|
||||
return;
|
||||
log_warning("flatfile: duplicate stanza-id '%s' from %s (may be non-unique client ID)",
|
||||
message->stanzaid, message->from_jid->barejid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,9 +878,22 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
ff_parsed_line_t* oldest = all_parsed->data;
|
||||
state->cursor_offset = oldest->file_offset;
|
||||
|
||||
// Apply LMC corrections and build ProfMessage list
|
||||
// Apply LMC corrections (if enabled) and build ProfMessage list
|
||||
GSList* pre_result = NULL;
|
||||
_ff_apply_lmc(all_parsed, &pre_result);
|
||||
if (prefs_get_boolean(PREF_CORRECTION_ALLOW)) {
|
||||
_ff_apply_lmc(all_parsed, &pre_result);
|
||||
} else {
|
||||
// LMC disabled — convert all lines to ProfMessage without correction
|
||||
for (GSList* cur = all_parsed; cur; cur = cur->next) {
|
||||
ff_parsed_line_t* pl = cur->data;
|
||||
if (pl) {
|
||||
ProfMessage* msg = ff_parsed_to_profmessage(pl);
|
||||
if (msg)
|
||||
pre_result = g_slist_prepend(pre_result, msg);
|
||||
}
|
||||
}
|
||||
pre_result = g_slist_reverse(pre_result);
|
||||
}
|
||||
g_slist_free_full(all_parsed, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
if (!pre_result)
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile.h
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Internal header shared between database_flatfile*.c modules.
|
||||
* Not part of the public API — do not include from outside the flatfile backend.
|
||||
*/
|
||||
@@ -69,8 +57,16 @@ typedef struct
|
||||
|
||||
// --- Sparse index for single-file lookup ---
|
||||
|
||||
// Sample every N-th log line for the sparse index.
|
||||
// 500 lines ≈ one index entry per ~50 KB of log data, so a 100K-message
|
||||
// contact requires only ~200 entries (~3 KB) for O(log n) time-range lookup.
|
||||
#define FF_INDEX_STEP 500
|
||||
|
||||
// --- Metadata field prefixes (used in _ff_cache_line_ids and ff_parse_line) ---
|
||||
|
||||
#define FF_META_PREFIX_ID "id:"
|
||||
#define FF_META_PREFIX_AID "aid:"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
off_t byte_offset;
|
||||
@@ -127,6 +123,9 @@ char* ff_unescape_meta_value(const char* val);
|
||||
|
||||
// --- I/O ---
|
||||
|
||||
// Skip UTF-8 BOM if present; returns 3 if skipped, 0 otherwise.
|
||||
int ff_skip_bom(FILE* fp);
|
||||
|
||||
char* ff_readline(FILE* fp, gboolean* truncated);
|
||||
void ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc,
|
||||
const char* stanza_id, const char* archive_id, const char* replace_id,
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile_parser.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Flat-file backend: type helpers, path helpers, escape/unescape,
|
||||
* line I/O, and the tolerant log-line parser.
|
||||
*/
|
||||
@@ -339,6 +327,24 @@ ff_unescape_meta_value(const char* val)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BOM helper
|
||||
// =========================================================================
|
||||
//
|
||||
// Skip UTF-8 BOM (EF BB BF) at the beginning of a file.
|
||||
// Returns 3 if BOM was present, 0 otherwise. File position is set past
|
||||
// the BOM (or rewound to the start).
|
||||
int
|
||||
ff_skip_bom(FILE* fp)
|
||||
{
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)
|
||||
return 3;
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Readline helper
|
||||
// =========================================================================
|
||||
//
|
||||
@@ -658,18 +664,18 @@ ff_parse_line(const char* line)
|
||||
result->type = g_strdup(parts[i]);
|
||||
} else if (i == 1) {
|
||||
result->enc = g_strdup(parts[i]);
|
||||
} else if (g_str_has_prefix(parts[i], "id:")) {
|
||||
result->stanza_id = ff_unescape_meta_value(parts[i] + 3);
|
||||
} else if (g_str_has_prefix(parts[i], "aid:")) {
|
||||
result->archive_id = ff_unescape_meta_value(parts[i] + 4);
|
||||
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_ID)) {
|
||||
result->stanza_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_ID));
|
||||
} else if (g_str_has_prefix(parts[i], FF_META_PREFIX_AID)) {
|
||||
result->archive_id = ff_unescape_meta_value(parts[i] + strlen(FF_META_PREFIX_AID));
|
||||
} else if (g_str_has_prefix(parts[i], "corrects:")) {
|
||||
result->replace_id = ff_unescape_meta_value(parts[i] + 9);
|
||||
result->replace_id = ff_unescape_meta_value(parts[i] + strlen("corrects:"));
|
||||
} else if (g_str_has_prefix(parts[i], "to:")) {
|
||||
result->to_jid = ff_unescape_meta_value(parts[i] + 3);
|
||||
result->to_jid = ff_unescape_meta_value(parts[i] + strlen("to:"));
|
||||
} else if (g_str_has_prefix(parts[i], "to_res:")) {
|
||||
result->to_resource = ff_unescape_meta_value(parts[i] + 7);
|
||||
result->to_resource = ff_unescape_meta_value(parts[i] + strlen("to_res:"));
|
||||
} else if (g_str_has_prefix(parts[i], "read:")) {
|
||||
result->marked_read = atoi(parts[i] + 5) ? 1 : 0;
|
||||
result->marked_read = atoi(parts[i] + strlen("read:")) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
g_strfreev(parts);
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_flatfile_verify.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Flat-file backend: integrity verification (/history verify).
|
||||
* Checks: parsability, timestamp ordering, duplicate IDs, broken LMC
|
||||
* references, file permissions, BOM, CRLF, UTF-8, control chars.
|
||||
@@ -117,18 +105,13 @@ ff_verify_integrity(const gchar* const contact_barejid)
|
||||
continue;
|
||||
|
||||
// BOM check
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
|
||||
if (ff_skip_bom(fp)) {
|
||||
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_prepend(issues, issue);
|
||||
} else {
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char* buf = NULL;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* test_database_export.c
|
||||
*
|
||||
* Unit tests for the flatfile write→parse round-trip, escape/unescape helpers,
|
||||
* jid_to_dir path construction, and parser edge cases — these are the core
|
||||
* functions exercised by database_export.c during export and import.
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
* License: GPL-3.0-or-later (same as Profanity)
|
||||
*/
|
||||
|
||||
#include "prof_cmocka.h"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* test_database_stress.c
|
||||
*
|
||||
* Stress tests for flat-file database I/O: rapid-fire writes, MUC messages
|
||||
* with many participants, large message bodies, index/cache correctness,
|
||||
* export/import merge under load, and deep LMC correction chains.
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
* License: GPL-3.0-or-later (same as Profanity)
|
||||
*/
|
||||
|
||||
#include "prof_cmocka.h"
|
||||
|
||||
Reference in New Issue
Block a user