no-DB mode implementation #94
@@ -56,7 +56,8 @@ typedef enum {
|
||||
} integrity_level_t;
|
||||
|
||||
// A single integrity issue found during verification
|
||||
typedef struct {
|
||||
typedef struct
|
||||
{
|
||||
integrity_level_t level;
|
||||
char* file;
|
||||
int line;
|
||||
@@ -66,7 +67,8 @@ typedef struct {
|
||||
void integrity_issue_free(integrity_issue_t* issue);
|
||||
|
||||
// Backend vtable: pluggable storage backends implement this interface
|
||||
typedef struct db_backend_t {
|
||||
typedef struct db_backend_t
|
||||
{
|
||||
const char* name;
|
||||
gboolean (*init)(ProfAccount* account);
|
||||
void (*close)(void);
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <stdio.h>
|
||||
@@ -40,8 +42,10 @@
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/message.h"
|
||||
|
||||
#define DIR_FLATLOG "flatlog"
|
||||
#define FLATFILE_HEADER "# profanity chat log — UTF-8, LF line endings\n# vim: set fileencoding=utf-8 fileformat=unix :\n"
|
||||
#define DIR_FLATLOG "flatlog"
|
||||
#define FLATFILE_HEADER "# profanity chat log — UTF-8, LF line endings\n# vim: set fileencoding=utf-8 fileformat=unix :\n"
|
||||
#define FF_MAX_LINE_LEN (10 * 1024 * 1024) /* 10 MB — reject lines longer than this */
|
||||
#define FF_MAX_LMC_DEPTH 100 /* max correction chain depth */
|
||||
|
||||
// Account JID stored during init for path construction
|
||||
static char* g_flatfile_account_jid = NULL;
|
||||
@@ -112,12 +116,48 @@ _ff_get_message_enc_type(const char* const encstr)
|
||||
|
||||
// --- Path helpers ---
|
||||
|
||||
|
jabber.developer marked this conversation as resolved
|
||||
// Replace '@' with '_at_' in JID for filesystem-safe directory names
|
||||
// Sanitise a JID for use as a directory name.
|
||||
// 1. Replace '@' with '_at_'
|
||||
// 2. Reject / strip path-separator and traversal characters: '/', '\0', '..'
|
||||
// This prevents a malicious federated JID like "../../../tmp/pwned" from
|
||||
// escaping the log directory tree.
|
||||
static char*
|
||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
what if what if `*(close + 1)` overflows?
jabber.developer2
commented
Corrected Corrected
|
||||
_ff_jid_to_dir(const char* jid)
|
||||
|
jabber.developer marked this conversation as resolved
jabber.developer
commented
again no overflow check again no overflow check
jabber.developer2
commented
Corrected Corrected
|
||||
{
|
||||
if (!jid) return NULL;
|
||||
return str_replace(jid, "@", "_at_");
|
||||
if (!jid || jid[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
|
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, '/')`
|
||||
// Reject embedded null bytes (strlen already stops, but be explicit)
|
||||
// Replace '@' first
|
||||
char* step1 = str_replace(jid, "@", "_at_");
|
||||
if (!step1)
|
||||
return NULL;
|
||||
|
||||
// Replace '/' and '\\' with '_' to prevent path traversal
|
||||
GString* out = g_string_sized_new(strlen(step1));
|
||||
for (const char* p = step1; *p; p++) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
g_string_append_c(out, '_');
|
||||
} else {
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
free(step1);
|
||||
|
||||
// Collapse any remaining ".." sequences to "__" (belt-and-suspenders)
|
||||
char* result = g_string_free(out, FALSE);
|
||||
char* dotdot;
|
||||
while ((dotdot = strstr(result, "..")) != NULL) {
|
||||
dotdot[0] = '_';
|
||||
dotdot[1] = '_';
|
||||
}
|
||||
|
||||
// Reject empty result
|
||||
if (result[0] == '\0') {
|
||||
g_free(result);
|
||||
return NULL;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the base directory for a contact's logs:
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
Please, extract bom-check to a different method. it is violates DRY and SRP. Please, extract bom-check to a different method. it is violates DRY and SRP. `database_flatfile_validate.c:123`
jabber.developer2
commented
Corrected Corrected
|
||||
@@ -125,7 +165,8 @@ _ff_jid_to_dir(const char* jid)
|
||||
static char*
|
||||
_ff_get_contact_dir(const char* contact_barejid)
|
||||
{
|
||||
if (!g_flatfile_account_jid || !contact_barejid) return NULL;
|
||||
if (!g_flatfile_account_jid || !contact_barejid)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* data_path = files_get_data_path(DIR_FLATLOG);
|
||||
auto_gchar gchar* my_dir = _ff_jid_to_dir(g_flatfile_account_jid);
|
||||
@@ -141,18 +182,26 @@ static char*
|
||||
_ff_get_log_path(const char* contact_barejid, GDateTime* dt)
|
||||
{
|
||||
auto_gchar gchar* contact_dir = _ff_get_contact_dir(contact_barejid);
|
||||
if (!contact_dir) return NULL;
|
||||
if (!contact_dir)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* date_str = g_date_time_format(dt, "%Y_%m_%d");
|
||||
char* result = g_strdup_printf("%s/%s.log", contact_dir, date_str);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ensure the directory exists, create if needed
|
||||
// Ensure the directory exists, create if needed.
|
||||
// Refuses to follow symlinks at the final component.
|
||||
static gboolean
|
||||
_ff_ensure_dir(const char* path)
|
||||
{
|
||||
if (g_file_test(path, G_FILE_TEST_IS_DIR)) {
|
||||
// Verify it's not a symlink
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
please add comment explaining purpose please add comment explaining purpose
jabber.developer2
commented
Corrected Corrected
|
||||
struct stat st;
|
||||
if (g_lstat(path, &st) == 0 && S_ISLNK(st.st_mode)) {
|
||||
log_error("flatfile: directory path is a symlink, refusing: %s", path);
|
||||
return FALSE;
|
||||
}
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
Are Are `!dt` and `!space` cases ok to be ignored?
jabber.developer2
commented
Postponed — discussion. Current behaviour is "skip silently" (lines outside index step are not indexed); this is by design. No specific change requested. Postponed — discussion. Current behaviour is "skip silently" (lines outside index step are not indexed); this is by design. No specific change requested.
|
||||
return TRUE;
|
||||
}
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
please, consider reduction of nesting please, consider reduction of nesting
jabber.developer2
commented
Corrected Corrected
|
||||
if (g_mkdir_with_parents(path, S_IRWXU) != 0) {
|
||||
@@ -162,11 +211,192 @@ _ff_ensure_dir(const char* path)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// --- Escape / unescape helpers ---
|
||||
//
|
||||
// Message text and metadata values from remote peers can contain arbitrary
|
||||
// characters including newlines, pipes and brackets. Without escaping, a
|
||||
// crafted message could inject fake log lines (log injection / format
|
||||
// injection). We escape on write and unescape on read.
|
||||
|
||||
// Escape message body: \ -> \\, \n -> \n literal, \r -> \r literal
|
||||
static char*
|
||||
_ff_escape_message(const char* text)
|
||||
{
|
||||
if (!text)
|
||||
return g_strdup("");
|
||||
GString* out = g_string_sized_new(strlen(text));
|
||||
for (const char* p = text; *p; p++) {
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
g_string_append(out, "\\\\");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append(out, "\\n");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append(out, "\\r");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Unescape message body: \\ -> \, \n -> newline, \r -> CR
|
||||
static char*
|
||||
_ff_unescape_message(const char* text)
|
||||
{
|
||||
if (!text)
|
||||
return g_strdup("");
|
||||
GString* out = g_string_sized_new(strlen(text));
|
||||
for (const char* p = text; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
g_string_append_c(out, '\\');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c(out, '\n');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c(out, '\r');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Escape metadata value (stanza_id, archive_id, replace_id):
|
||||
// these come from remote servers and may contain |, ], \, newlines.
|
||||
static char*
|
||||
_ff_escape_meta_value(const char* val)
|
||||
{
|
||||
if (!val || strlen(val) == 0)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(val));
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
the logic seemingly mirrors the one from the logic seemingly mirrors the one from `_ff_state_build`, creating repetition. I suggest extracting it to a different method to keep things DRYer and more maintainable
jabber.developer2
commented
Corrected Corrected
|
||||
for (const char* p = val; *p; p++) {
|
||||
switch (*p) {
|
||||
case '|':
|
||||
g_string_append(out, "\\|");
|
||||
break;
|
||||
case ']':
|
||||
g_string_append(out, "\\]");
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append(out, "\\\\");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append(out, "\\n");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append(out, "\\r");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// Unescape metadata value
|
||||
static char*
|
||||
_ff_unescape_meta_value(const char* val)
|
||||
{
|
||||
if (!val)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(val));
|
||||
for (const char* p = val; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '|':
|
||||
g_string_append_c(out, '|');
|
||||
break;
|
||||
case ']':
|
||||
g_string_append_c(out, ']');
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append_c(out, '\\');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c(out, '\n');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c(out, '\r');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// --- Readline helper ---
|
||||
//
|
||||
// POSIX getline() for dynamic-length lines. Returns line without trailing
|
||||
// newline, or NULL on EOF. Sets *truncated=TRUE if the line had no trailing
|
||||
// newline at EOF (partial write detection).
|
||||
static char*
|
||||
_ff_readline(FILE* fp, gboolean* truncated)
|
||||
{
|
||||
char* line = NULL;
|
||||
size_t cap = 0;
|
||||
ssize_t nread = getline(&line, &cap, fp);
|
||||
if (nread == -1) {
|
||||
free(line);
|
||||
return NULL;
|
||||
}
|
||||
// Guard against pathological lines that could exhaust memory.
|
||||
// getline() already allocated, so we free and skip if too long.
|
||||
if (nread > FF_MAX_LINE_LEN) {
|
||||
log_error("flatfile: line too long (%zd bytes), skipping", nread);
|
||||
// Check newline termination before freeing
|
||||
gboolean had_newline = (nread > 0 && line[nread - 1] == '\n');
|
||||
free(line);
|
||||
// Skip to next newline if the overlength line wasn't newline-terminated
|
||||
if (!had_newline) {
|
||||
int ch;
|
||||
while ((ch = fgetc(fp)) != EOF && ch != '\n') {}
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
// Return empty string so caller's loop continues (parse will reject it)
|
||||
return g_strdup("");
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
if (nread > 0 && line[nread - 1] == '\n') {
|
||||
line[--nread] = '\0';
|
||||
} else if (feof(fp)) {
|
||||
// Line without trailing newline at EOF — likely a partial write
|
||||
if (truncated)
|
||||
*truncated = TRUE;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
// --- Line format ---
|
||||
//
|
||||
// Format: {ISO8601} [{type}|{enc}|id:{stanza_id}|aid:{archive_id}|corrects:{replace_id}] {from_jid}/{resource}: {message}
|
||||
//
|
||||
// Metadata fields after type|enc are optional.
|
||||
// Message text is escaped: \\ -> \\\\, newline -> \\n, CR -> \\r
|
||||
// Metadata values are escaped: |, ], \\, newline, CR
|
||||
// Lines starting with '#' are comments.
|
||||
// Empty lines are skipped.
|
||||
|
||||
@@ -175,41 +405,162 @@ _ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* en
|
||||
const char* stanza_id, const char* archive_id, const char* replace_id,
|
||||
const char* from_jid, const char* from_resource, const char* message_text)
|
||||
{
|
||||
// Escape metadata values from remote peers
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
note to check: is it case sensitive/get forced lowercase when set by user? note to check: is it case sensitive/get forced lowercase when set by user?
jabber.developer
commented
Checked cmd_funcs.c:6676: resolved Checked cmd_funcs.c:6676:
```c
if (g_strcmp0(arg, "on") == 0) {
cons_show("Logging enabled.");
prefs_set_string(PREF_DBLOG, arg);
prefs_set_boolean(PREF_CHLOG, TRUE);
prefs_set_boolean(PREF_HISTORY, TRUE);
} else if (g_strcmp0(arg, "off") == 0) {
cons_show("Logging disabled.");
prefs_set_string(PREF_DBLOG, arg);
prefs_set_boolean(PREF_CHLOG, FALSE);
prefs_set_boolean(PREF_HISTORY, FALSE);
}
```
resolved
|
||||
auto_gchar gchar* safe_sid = _ff_escape_meta_value(stanza_id);
|
||||
auto_gchar gchar* safe_aid = _ff_escape_meta_value(archive_id);
|
||||
auto_gchar gchar* safe_rid = _ff_escape_meta_value(replace_id);
|
||||
|
||||
// Build metadata section: [type|enc|id:...|aid:...|corrects:...]
|
||||
GString* meta = g_string_new("[");
|
||||
g_string_append(meta, type ? type : "chat");
|
||||
g_string_append_c(meta, '|');
|
||||
g_string_append(meta, enc ? enc : "none");
|
||||
if (stanza_id && strlen(stanza_id) > 0) {
|
||||
g_string_append_printf(meta, "|id:%s", stanza_id);
|
||||
if (safe_sid) {
|
||||
g_string_append_printf(meta, "|id:%s", safe_sid);
|
||||
}
|
||||
if (archive_id && strlen(archive_id) > 0) {
|
||||
g_string_append_printf(meta, "|aid:%s", archive_id);
|
||||
if (safe_aid) {
|
||||
g_string_append_printf(meta, "|aid:%s", safe_aid);
|
||||
}
|
||||
if (replace_id && strlen(replace_id) > 0) {
|
||||
g_string_append_printf(meta, "|corrects:%s", replace_id);
|
||||
if (safe_rid) {
|
||||
g_string_append_printf(meta, "|corrects:%s", safe_rid);
|
||||
}
|
||||
g_string_append_c(meta, ']');
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
nit: for readability, maybe we can do something like that? nit: for readability, maybe we can do something like that?
```c
gboolean is_incoming = myjid && myjid->barejid && g_strcmp0(from_barejid, myjid->barejid) == 0;
contact = is_incoming ? to_barejid : from_barejid;
```
jabber.developer2
commented
Corrected Corrected
|
||||
|
||||
// Build sender
|
||||
// Build sender — escape ": " in the resource part to prevent
|
||||
// the parser from splitting at the wrong point.
|
||||
// JID bare parts (user@domain) never contain ": " but XMPP resources
|
||||
// can contain arbitrary UTF-8 including ": ".
|
||||
GString* sender = g_string_new(from_jid ? from_jid : "unknown");
|
||||
if (from_resource && strlen(from_resource) > 0) {
|
||||
g_string_append_printf(sender, "/%s", from_resource);
|
||||
// Escape backslash and colon-space in resource
|
||||
GString* safe_res = g_string_sized_new(strlen(from_resource));
|
||||
for (const char* p = from_resource; *p; p++) {
|
||||
if (*p == '\\') {
|
||||
g_string_append(safe_res, "\\\\");
|
||||
} else if (*p == ':' && *(p + 1) == ' ') {
|
||||
g_string_append(safe_res, "\\: ");
|
||||
p++; // skip the space too
|
||||
} else {
|
||||
g_string_append_c(safe_res, *p);
|
||||
}
|
||||
}
|
||||
g_string_append_printf(sender, "/%s", safe_res->str);
|
||||
g_string_free(safe_res, TRUE);
|
||||
}
|
||||
|
||||
fprintf(fp, "%s %s %s: %s\n",
|
||||
timestamp,
|
||||
meta->str,
|
||||
sender->str,
|
||||
message_text ? message_text : "");
|
||||
// Escape message body to prevent log injection
|
||||
|
jabber.developer marked this conversation as resolved
Outdated
jabber.developer
commented
`O_APPEND` won't execute since `O_EXCL` would throw an error if file already exists.
jabber.developer2
commented
Corrected Corrected
|
||||
char* safe_msg = _ff_escape_message(message_text);
|
||||
|
||||
// Build complete line and write atomically with a single write()
|
||||
GString* full_line = g_string_new(NULL);
|
||||
g_string_printf(full_line, "%s %s %s: %s\n",
|
||||
timestamp, meta->str, sender->str, safe_msg);
|
||||
|
||||
size_t to_write = full_line->len;
|
||||
ssize_t written = fwrite(full_line->str, 1, to_write, fp);
|
||||
if (written != (ssize_t)to_write) {
|
||||
log_error("flatfile: partial write (%zd/%zu)", written, to_write);
|
||||
}
|
||||
|
||||
g_string_free(full_line, TRUE);
|
||||
g_free(safe_msg);
|
||||
g_string_free(meta, TRUE);
|
||||
g_string_free(sender, TRUE);
|
||||
}
|
||||
|
||||
// --- Line parser helpers ---
|
||||
|
||||
// Find the first occurrence of 'ch' that is not preceded by an unescaped backslash.
|
||||
// Returns pointer to 'ch' in 'str', or NULL if not found.
|
||||
static const char*
|
||||
_ff_find_unescaped_char(const char* str, char ch)
|
||||
{
|
||||
if (!str)
|
||||
return NULL;
|
||||
for (const char* p = str; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped character
|
||||
continue;
|
||||
}
|
||||
if (*p == ch)
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Split metadata content on unescaped '|'. Returns a NULL-terminated array.
|
||||
// Caller must g_strfreev() the result.
|
||||
static char**
|
||||
_ff_split_meta(const char* meta)
|
||||
{
|
||||
GPtrArray* arr = g_ptr_array_new();
|
||||
const char* start = meta;
|
||||
for (const char* p = meta;; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped char
|
||||
continue;
|
||||
}
|
||||
if (*p == '|' || *p == '\0') {
|
||||
g_ptr_array_add(arr, g_strndup(start, p - start));
|
||||
if (*p == '\0')
|
||||
break;
|
||||
start = p + 1;
|
||||
}
|
||||
}
|
||||
g_ptr_array_add(arr, NULL);
|
||||
return (char**)g_ptr_array_free(arr, FALSE);
|
||||
}
|
||||
|
||||
// Find first unescaped ": " (colon-space) in a string.
|
||||
// Escaped form is "\: " — a backslash before the colon.
|
||||
static const char*
|
||||
_ff_find_unescaped_colonspace(const char* str)
|
||||
{
|
||||
if (!str)
|
||||
return NULL;
|
||||
for (const char* p = str; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++; // skip escaped character
|
||||
continue;
|
||||
}
|
||||
if (*p == ':' && *(p + 1) == ' ') {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Unescape a sender resource: "\\" -> '\', "\: " -> ": "
|
||||
static char*
|
||||
_ff_unescape_sender_resource(const char* res)
|
||||
{
|
||||
if (!res)
|
||||
return NULL;
|
||||
GString* out = g_string_sized_new(strlen(res));
|
||||
for (const char* p = res; *p; p++) {
|
||||
if (*p == '\\' && *(p + 1)) {
|
||||
p++;
|
||||
if (*p == '\\') {
|
||||
g_string_append_c(out, '\\');
|
||||
} else if (*p == ':' && *(p + 1) == ' ') {
|
||||
g_string_append(out, ": ");
|
||||
p++; // skip the space
|
||||
} else {
|
||||
// Unknown escape — preserve literally
|
||||
g_string_append_c(out, '\\');
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
} else {
|
||||
g_string_append_c(out, *p);
|
||||
}
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
// --- Line parser ---
|
||||
|
||||
typedef struct {
|
||||
typedef struct
|
||||
{
|
||||
char* timestamp_str;
|
||||
GDateTime* timestamp;
|
||||
char* type;
|
||||
@@ -225,9 +576,11 @@ typedef struct {
|
||||
static void
|
||||
_ff_parsed_line_free(flatfile_parsed_line_t* pl)
|
||||
{
|
||||
if (!pl) return;
|
||||
if (!pl)
|
||||
return;
|
||||
g_free(pl->timestamp_str);
|
||||
if (pl->timestamp) g_date_time_unref(pl->timestamp);
|
||||
if (pl->timestamp)
|
||||
g_date_time_unref(pl->timestamp);
|
||||
g_free(pl->type);
|
||||
g_free(pl->enc);
|
||||
g_free(pl->stanza_id);
|
||||
@@ -272,7 +625,8 @@ _ff_parse_line(const char* line)
|
||||
g_free(work);
|
||||
work = converted;
|
||||
} else {
|
||||
if (err) g_error_free(err);
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
@@ -290,12 +644,13 @@ _ff_parse_line(const char* line)
|
||||
result->timestamp_str = g_strndup(work, first_space - work);
|
||||
|
||||
// Parse metadata section [...]
|
||||
char* bracket_end = strchr(bracket_start, ']');
|
||||
// Use escape-aware bracket finder to handle escaped ']' in values
|
||||
const char* bracket_end = _ff_find_unescaped_char(bracket_start + 1, ']');
|
||||
if (bracket_end) {
|
||||
char* meta_content = g_strndup(bracket_start + 1, bracket_end - bracket_start - 1);
|
||||
|
||||
// Split by '|'
|
||||
char** parts = g_strsplit(meta_content, "|", -1);
|
||||
// Split by unescaped '|'
|
||||
char** parts = _ff_split_meta(meta_content);
|
||||
if (parts) {
|
||||
int i = 0;
|
||||
for (; parts[i]; i++) {
|
||||
@@ -304,11 +659,11 @@ _ff_parse_line(const char* line)
|
||||
} else if (i == 1) {
|
||||
result->enc = g_strdup(parts[i]);
|
||||
} else if (g_str_has_prefix(parts[i], "id:")) {
|
||||
result->stanza_id = g_strdup(parts[i] + 3);
|
||||
result->stanza_id = _ff_unescape_meta_value(parts[i] + 3);
|
||||
} else if (g_str_has_prefix(parts[i], "aid:")) {
|
||||
result->archive_id = g_strdup(parts[i] + 4);
|
||||
result->archive_id = _ff_unescape_meta_value(parts[i] + 4);
|
||||
} else if (g_str_has_prefix(parts[i], "corrects:")) {
|
||||
result->replace_id = g_strdup(parts[i] + 9);
|
||||
result->replace_id = _ff_unescape_meta_value(parts[i] + 9);
|
||||
}
|
||||
}
|
||||
g_strfreev(parts);
|
||||
|
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
|
||||
@@ -316,29 +671,31 @@ _ff_parse_line(const char* line)
|
||||
g_free(meta_content);
|
||||
|
||||
// Parse sender: message after '] '
|
||||
char* after_meta = bracket_end + 1;
|
||||
if (*after_meta == ' ') after_meta++;
|
||||
const char* after_meta = bracket_end + 1;
|
||||
if (*after_meta == ' ')
|
||||
after_meta++;
|
||||
|
||||
// Find first ': ' which separates sender from message
|
||||
char* colon = strstr(after_meta, ": ");
|
||||
// Find first *unescaped* ': ' which separates sender from message.
|
||||
// Resources may contain escaped \: sequences.
|
||||
const char* colon = _ff_find_unescaped_colonspace(after_meta);
|
||||
if (colon) {
|
||||
char* sender = g_strndup(after_meta, colon - after_meta);
|
||||
char* raw_sender = g_strndup(after_meta, colon - after_meta);
|
||||
|
||||
// Split sender into jid/resource
|
||||
char* slash = strchr(sender, '/');
|
||||
// Split sender into jid/resource, then unescape resource
|
||||
char* slash = strchr(raw_sender, '/');
|
||||
if (slash) {
|
||||
result->from_jid = g_strndup(sender, slash - sender);
|
||||
result->from_resource = g_strdup(slash + 1);
|
||||
result->from_jid = g_strndup(raw_sender, slash - raw_sender);
|
||||
result->from_resource = _ff_unescape_sender_resource(slash + 1);
|
||||
} else {
|
||||
result->from_jid = g_strdup(sender);
|
||||
result->from_jid = g_strdup(raw_sender);
|
||||
}
|
||||
g_free(sender);
|
||||
g_free(raw_sender);
|
||||
|
||||
result->message = g_strdup(colon + 2);
|
||||
result->message = _ff_unescape_message(colon + 2);
|
||||
} else {
|
||||
// No ': ' found, treat entire rest as message with unknown sender
|
||||
result->from_jid = g_strdup("unknown");
|
||||
result->message = g_strdup(after_meta);
|
||||
result->message = _ff_unescape_message(after_meta);
|
||||
}
|
||||
} else {
|
||||
// No closing bracket — malformed metadata
|
||||
@@ -370,10 +727,10 @@ _ff_parse_line(const char* line)
|
||||
result->from_jid = g_strdup(sender);
|
||||
}
|
||||
g_free(sender);
|
||||
result->message = g_strdup(colon + 2);
|
||||
result->message = _ff_unescape_message(colon + 2);
|
||||
} else {
|
||||
result->from_jid = g_strdup("unknown");
|
||||
result->message = g_strdup(rest);
|
||||
result->message = _ff_unescape_message(rest);
|
||||
}
|
||||
} else {
|
||||
// No space at all — can't parse
|
||||
@@ -385,15 +742,17 @@ _ff_parse_line(const char* line)
|
||||
// Parse timestamp
|
||||
result->timestamp = g_date_time_new_from_iso8601(result->timestamp_str, NULL);
|
||||
if (!result->timestamp) {
|
||||
log_warning("flatfile: unparseable timestamp: %s", result->timestamp_str);
|
||||
log_warning("flatfile: unparsable timestamp: %s", result->timestamp_str);
|
||||
_ff_parsed_line_free(result);
|
||||
g_free(work);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Default type/enc if missing
|
||||
if (!result->type) result->type = g_strdup("chat");
|
||||
if (!result->enc) result->enc = g_strdup("none");
|
||||
if (!result->type)
|
||||
result->type = g_strdup("chat");
|
||||
if (!result->enc)
|
||||
result->enc = g_strdup("none");
|
||||
|
||||
g_free(work);
|
||||
return result;
|
||||
@@ -463,19 +822,36 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file is new (needs header)
|
||||
gboolean is_new = !g_file_test(log_path, G_FILE_TEST_EXISTS);
|
||||
// Open the file with O_NOFOLLOW to prevent symlink attacks,
|
||||
// and O_APPEND for safe concurrent appends.
|
||||
// Try O_CREAT|O_EXCL first to detect new files atomically (no TOCTOU).
|
||||
int fd = open(log_path, O_WRONLY | O_APPEND | O_CREAT | O_EXCL | O_NOFOLLOW,
|
||||
S_IRUSR | S_IWUSR);
|
||||
gboolean is_new = (fd >= 0);
|
||||
if (fd < 0) {
|
||||
if (errno == EEXIST) {
|
||||
// File already exists — open for append
|
||||
fd = open(log_path, O_WRONLY | O_APPEND | O_NOFOLLOW);
|
||||
}
|
||||
if (fd < 0) {
|
||||
// ELOOP (symlink) or other error
|
||||
log_error("flatfile: could not open %s for writing (errno=%d: %s)",
|
||||
log_path, errno, strerror(errno));
|
||||
g_free(effective_msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FILE* fp = fopen(log_path, "a");
|
||||
FILE* fp = fdopen(fd, "a");
|
||||
if (!fp) {
|
||||
log_error("flatfile: could not open %s for writing (errno=%d)", log_path, errno);
|
||||
log_error("flatfile: fdopen failed for %s (errno=%d)", log_path, errno);
|
||||
close(fd);
|
||||
g_free(effective_msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_new) {
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
g_chmod(log_path, S_IRUSR | S_IWUSR);
|
||||
}
|
||||
|
||||
_ff_write_line(fp, date_fmt, type, _ff_get_message_enc_str(enc),
|
||||
@@ -524,7 +900,8 @@ _flatfile_close(void)
|
||||
static void
|
||||
_flatfile_add_incoming(ProfMessage* message)
|
||||
{
|
||||
if (!message || !message->from_jid) return;
|
||||
if (!message || !message->from_jid)
|
||||
return;
|
||||
|
||||
const Jid* to_jid = message->to_jid ? message->to_jid : connection_get_jid();
|
||||
const char* type = _ff_get_message_type_str(message->type);
|
||||
@@ -577,7 +954,8 @@ _ff_list_log_files(const char* contact_barejid)
|
||||
}
|
||||
|
||||
GDir* dir = g_dir_open(contact_dir, 0, NULL);
|
||||
if (!dir) return NULL;
|
||||
if (!dir)
|
||||
return NULL;
|
||||
|
||||
GSList* files = NULL;
|
||||
const gchar* fname;
|
||||
@@ -601,7 +979,8 @@ _ff_read_file_messages(const char* filepath, const char* contact_barejid,
|
||||
GSList* messages = NULL;
|
||||
|
||||
FILE* fp = fopen(filepath, "r");
|
||||
if (!fp) return NULL;
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
// BOM detection
|
||||
int c1 = fgetc(fp);
|
||||
@@ -612,14 +991,19 @@ _ff_read_file_messages(const char* filepath, const char* contact_barejid,
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
while (fgets(buf, sizeof(buf), fp)) {
|
||||
// Strip trailing newline
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0';
|
||||
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;
|
||||
}
|
||||
|
||||
flatfile_parsed_line_t* pl = _ff_parse_line(buf);
|
||||
if (!pl) continue;
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
|
||||
// Check timestamp bounds
|
||||
if (start_time) {
|
||||
@@ -648,8 +1032,7 @@ _ff_read_file_messages(const char* filepath, const char* contact_barejid,
|
||||
// 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)) {
|
||||
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0) || (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
|
||||
matches = TRUE;
|
||||
}
|
||||
} else {
|
||||
@@ -691,16 +1074,25 @@ _ff_apply_lmc(GSList* parsed_lines, GSList** result)
|
||||
if (pl->replace_id && strlen(pl->replace_id) > 0) {
|
||||
flatfile_parsed_line_t* original = g_hash_table_lookup(id_map, pl->replace_id);
|
||||
if (original) {
|
||||
// Follow chain to the root original
|
||||
// Follow chain to the root original, with cycle/depth guard
|
||||
flatfile_parsed_line_t* root = original;
|
||||
while (root->replace_id && strlen(root->replace_id) > 0) {
|
||||
GHashTable* visited = g_hash_table_new(g_direct_hash, g_direct_equal);
|
||||
g_hash_table_insert(visited, root, root);
|
||||
int depth = 0;
|
||||
while (root->replace_id && strlen(root->replace_id) > 0 && depth < FF_MAX_LMC_DEPTH) {
|
||||
flatfile_parsed_line_t* parent = g_hash_table_lookup(id_map, root->replace_id);
|
||||
if (parent && parent != root) {
|
||||
if (parent && !g_hash_table_contains(visited, parent)) {
|
||||
g_hash_table_insert(visited, parent, parent);
|
||||
root = parent;
|
||||
depth++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_hash_table_destroy(visited);
|
||||
if (depth >= FF_MAX_LMC_DEPTH) {
|
||||
log_warning("flatfile: LMC correction chain too deep (>%d), ignoring", FF_MAX_LMC_DEPTH);
|
||||
}
|
||||
g_hash_table_insert(correction_map, root, pl);
|
||||
g_hash_table_insert(corrections, pl, pl);
|
||||
}
|
||||
@@ -768,7 +1160,7 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
GSList* all_parsed = NULL;
|
||||
for (GSList* f = files; f; f = f->next) {
|
||||
GSList* file_msgs = _ff_read_file_messages(f->data, contact_barejid,
|
||||
start_time, effective_end, myjid);
|
||||
start_time, effective_end, myjid);
|
||||
all_parsed = g_slist_concat(all_parsed, file_msgs);
|
||||
}
|
||||
|
||||
@@ -797,7 +1189,8 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
GSList* nth = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE);
|
||||
if (nth) {
|
||||
GSList* prev = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE - 1);
|
||||
if (prev) prev->next = NULL;
|
||||
if (prev)
|
||||
prev->next = NULL;
|
||||
g_slist_free_full(nth, (GDestroyNotify)message_free);
|
||||
}
|
||||
} else {
|
||||
@@ -805,7 +1198,8 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
guint skip = total - MESSAGES_TO_RETRIEVE;
|
||||
GSList* keep_start = g_slist_nth(pre_result, skip);
|
||||
GSList* prev = g_slist_nth(pre_result, skip - 1);
|
||||
if (prev) prev->next = NULL;
|
||||
if (prev)
|
||||
prev->next = NULL;
|
||||
g_slist_free_full(pre_result, (GDestroyNotify)message_free);
|
||||
pre_result = keep_start;
|
||||
}
|
||||
@@ -826,13 +1220,15 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
ProfMessage* msg = message_init();
|
||||
|
||||
if (!g_flatfile_account_jid || !contact_barejid) {
|
||||
if (is_last) msg->timestamp = g_date_time_new_now_utc();
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
GSList* files = _ff_list_log_files(contact_barejid);
|
||||
if (!files) {
|
||||
if (is_last) msg->timestamp = g_date_time_new_now_utc();
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -844,7 +1240,8 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
FILE* fp = fopen(target_file, "r");
|
||||
if (!fp) {
|
||||
g_slist_free_full(files, g_free);
|
||||
if (is_last) msg->timestamp = g_date_time_new_now_utc();
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -856,25 +1253,25 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
char* buf = NULL;
|
||||
flatfile_parsed_line_t* found = NULL;
|
||||
|
||||
if (!is_last) {
|
||||
// Find first valid line
|
||||
while (fgets(buf, sizeof(buf), fp)) {
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0';
|
||||
while ((buf = _ff_readline(fp, NULL)) != NULL) {
|
||||
found = _ff_parse_line(buf);
|
||||
if (found) break;
|
||||
free(buf);
|
||||
if (found)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Find last valid line — read all and keep last
|
||||
while (fgets(buf, sizeof(buf), fp)) {
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0';
|
||||
while ((buf = _ff_readline(fp, NULL)) != NULL) {
|
||||
flatfile_parsed_line_t* pl = _ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (pl) {
|
||||
if (found) _ff_parsed_line_free(found);
|
||||
if (found)
|
||||
_ff_parsed_line_free(found);
|
||||
found = pl;
|
||||
}
|
||||
}
|
||||
@@ -952,7 +1349,8 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
const char* cdir_path = cd->data;
|
||||
|
||||
GDir* dir = g_dir_open(cdir_path, 0, NULL);
|
||||
if (!dir) continue;
|
||||
if (!dir)
|
||||
continue;
|
||||
|
||||
GSList* log_files = NULL;
|
||||
const gchar* fname;
|
||||
@@ -988,7 +1386,8 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
}
|
||||
|
||||
FILE* fp = fopen(filepath, "r");
|
||||
if (!fp) continue;
|
||||
if (!fp)
|
||||
continue;
|
||||
|
||||
// BOM check (4l)
|
||||
int c1 = fgetc(fp);
|
||||
@@ -1005,7 +1404,7 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
char* buf = NULL;
|
||||
int lineno = 0;
|
||||
GDateTime* prev_ts = NULL;
|
||||
GDateTime* first_ts = NULL;
|
||||
@@ -1013,10 +1412,9 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
gboolean has_crlf = FALSE;
|
||||
gboolean is_empty = TRUE;
|
||||
|
||||
while (fgets(buf, sizeof(buf), fp)) {
|
||||
while ((buf = _ff_readline(fp, NULL)) != NULL) {
|
||||
lineno++;
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0';
|
||||
|
||||
// CRLF check (4m)
|
||||
if (len > 0 && buf[len - 1] == '\r') {
|
||||
@@ -1025,7 +1423,10 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
}
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (len == 0 || buf[0] == '#') continue;
|
||||
if (len == 0 || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
is_empty = FALSE;
|
||||
|
||||
// UTF-8 validation (4k/4n)
|
||||
@@ -1037,6 +1438,7 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1061,13 +1463,18 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
issue->level = INTEGRITY_ERROR;
|
||||
issue->file = g_strdup(basename);
|
||||
issue->line = lineno;
|
||||
issue->message = g_strdup("Unparseable line");
|
||||
issue->message = g_strdup("Unparsable line");
|
||||
issues = g_slist_append(issues, issue);
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first_ts) first_ts = g_date_time_ref(pl->timestamp);
|
||||
if (last_ts) g_date_time_unref(last_ts);
|
||||
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 (4c)
|
||||
@@ -1081,7 +1488,8 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
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);
|
||||
if (prev_ts)
|
||||
g_date_time_unref(prev_ts);
|
||||
prev_ts = g_date_time_ref(pl->timestamp);
|
||||
|
||||
// Duplicate stanza-id / archive-id (4f)
|
||||
@@ -1155,12 +1563,16 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
}
|
||||
}
|
||||
|
||||
if (prev_file_last_ts) g_date_time_unref(prev_file_last_ts);
|
||||
if (prev_file_last_ts)
|
||||
g_date_time_unref(prev_file_last_ts);
|
||||
prev_file_last_ts = last_ts ? g_date_time_ref(last_ts) : NULL;
|
||||
|
||||
if (prev_ts) g_date_time_unref(prev_ts);
|
||||
if (first_ts) g_date_time_unref(first_ts);
|
||||
if (last_ts) g_date_time_unref(last_ts);
|
||||
if (prev_ts)
|
||||
g_date_time_unref(prev_ts);
|
||||
if (first_ts)
|
||||
g_date_time_unref(first_ts);
|
||||
if (last_ts)
|
||||
g_date_time_unref(last_ts);
|
||||
}
|
||||
|
||||
// Second pass: check LMC references across all files (4g)
|
||||
@@ -1170,7 +1582,8 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
basename_lmc = basename_lmc ? basename_lmc + 1 : filepath;
|
||||
|
||||
FILE* fp = fopen(filepath, "r");
|
||||
if (!fp) continue;
|
||||
if (!fp)
|
||||
continue;
|
||||
|
||||
// Skip BOM
|
||||
int b1 = fgetc(fp);
|
||||
@@ -1180,17 +1593,22 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
char* buf = NULL;
|
||||
int lineno = 0;
|
||||
while (fgets(buf, sizeof(buf), fp)) {
|
||||
while ((buf = _ff_readline(fp, NULL)) != NULL) {
|
||||
lineno++;
|
||||
gsize len = strlen(buf);
|
||||
if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0';
|
||||
if (len > 0 && buf[len - 1] == '\r') buf[--len] = '\0';
|
||||
if (len == 0 || buf[0] == '#') continue;
|
||||
if (len > 0 && buf[len - 1] == '\r')
|
||||
buf[--len] = '\0';
|
||||
if (len == 0 || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
flatfile_parsed_line_t* pl = _ff_parse_line(buf);
|
||||
if (!pl) continue;
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
|
||||
if (pl->replace_id && strlen(pl->replace_id) > 0) {
|
||||
if (!g_hash_table_contains(all_stanza_ids, pl->replace_id)) {
|
||||
@@ -1207,7 +1625,8 @@ _flatfile_verify_integrity(const gchar* const contact_barejid)
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
if (prev_file_last_ts) g_date_time_unref(prev_file_last_ts);
|
||||
if (prev_file_last_ts)
|
||||
g_date_time_unref(prev_file_last_ts);
|
||||
g_hash_table_destroy(seen_ids);
|
||||
g_hash_table_destroy(all_stanza_ids);
|
||||
g_slist_free_full(log_files, g_free);
|
||||
|
||||
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.
Corrected