Split message log record module

This commit is contained in:
m1ngsama 2026-05-27 10:08:32 +08:00
parent 5240756f96
commit 3252e4583c
7 changed files with 190 additions and 133 deletions

View file

@ -63,6 +63,8 @@
- `scripts/logrotate.sh` now has validated arguments, stable exit statuses,
dry-run support, archive retention, gzip-aware archives, and a regression
test in the normal test suite.
- `messages.log` v1 record parsing and formatting now live in a dedicated
`message_log` module instead of being embedded in `message.c`.
- The two-user lifecycle test now covers opening `:inbox` before a private
message arrives, matching the way users often leave an inbox page open.
- Private-message inbox access now uses its own mutex instead of sharing the

View file

@ -81,6 +81,7 @@ src/
├── exec.c - SSH exec command dispatch
├── chat_room.c - Chat room state, message ring, and update sequence
├── message.c - Message persistence (RFC3339 format)
├── message_log.c - messages.log v1 parsing and formatting
├── history_view.c - NORMAL-mode scroll window rules
├── tui.c - Terminal UI rendering (ANSI escape codes)
├── tui_status.c - Mode/status/input-line rendering
@ -103,6 +104,7 @@ include/
├── bootstrap.h - SSH session bootstrap interface
├── chat_room.h - Chat room interface
├── message.h - Message structure and persistence
├── message_log.h - messages.log v1 parser/formatter interface
├── command_catalog.h - COMMAND-mode command metadata interface
├── history_view.h - Scroll-state helpers
├── tui.h - TUI rendering functions

View file

@ -71,6 +71,7 @@ STRUCTURE
src/exec_catalog.c SSH exec command matching, usage, argument shape
src/exec.c SSH exec command dispatch
src/message.c persistence, search
src/message_log.c messages.log v1 parsing and formatting
src/history_view.c message viewport / scroll state
src/help_text.c full-screen key reference text
src/manual.c concise manual panel rendering

21
include/message_log.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef MESSAGE_LOG_H
#define MESSAGE_LOG_H
#include "message.h"
#define MESSAGE_LOG_MAX_LINE 2048
void message_log_format_timestamp_utc(time_t ts, char *buffer,
size_t buf_size);
/* Parse one complete messages.log v1 record. `now` is used to reject records
* outside TNT's accepted replay window. */
bool message_log_parse_record(const char *line, message_t *out, time_t now);
/* Format one messages.log v1 record. record_len receives the number of bytes
* that would be written, excluding the trailing NUL. Passing NULL/0 for the
* output buffer is allowed when only the length is needed. */
int message_log_format_record(const message_t *msg, char *buffer,
size_t buf_size, size_t *record_len);
#endif /* MESSAGE_LOG_H */

View file

@ -1,10 +1,12 @@
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE /* for timegm() on glibc */
#define _DEFAULT_SOURCE /* for strcasestr() on glibc */
#endif
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
#define _DARWIN_C_SOURCE /* for strcasestr() on macOS */
#endif
#include "message.h"
#include "message_log.h"
#include "utf8.h"
#include <errno.h>
#include <unistd.h>
@ -12,32 +14,6 @@
static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
static time_t parse_rfc3339_utc(const char *timestamp_str) {
struct tm tm = {0};
if (!timestamp_str) {
return (time_t)-1;
}
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
if (!result || *result != '\0') {
return (time_t)-1;
}
return timegm(&tm);
}
static void format_rfc3339_utc(time_t ts, char *buffer, size_t buf_size) {
struct tm tm_info;
if (!buffer || buf_size == 0) {
return;
}
gmtime_r(&ts, &tm_info);
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
}
static void discard_line_remainder(FILE *fp) {
int c;
@ -45,96 +21,23 @@ static void discard_line_remainder(FILE *fp) {
}
}
static bool parse_log_record(const char *line, message_t *out,
time_t now) {
char line_copy[2048];
char *first_sep;
char *second_sep;
char *timestamp_str;
char *username;
char *content;
time_t msg_time;
size_t line_len;
if (!line || !out) {
return false;
}
line_len = strlen(line);
if (line_len == 0 || line[line_len - 1] != '\n') {
return false;
}
if (line_len >= sizeof(line_copy)) {
return false;
}
memcpy(line_copy, line, line_len + 1);
line_copy[line_len - 1] = '\0';
first_sep = strchr(line_copy, '|');
if (!first_sep) {
return false;
}
second_sep = strchr(first_sep + 1, '|');
if (!second_sep || strchr(second_sep + 1, '|')) {
return false;
}
*first_sep = '\0';
*second_sep = '\0';
timestamp_str = line_copy;
username = first_sep + 1;
content = second_sep + 1;
if (timestamp_str[0] == '\0' || username[0] == '\0' ||
content[0] == '\0') {
return false;
}
if (strlen(username) >= MAX_USERNAME_LEN ||
strlen(content) >= MAX_MESSAGE_LEN) {
return false;
}
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) {
return false;
}
msg_time = parse_rfc3339_utc(timestamp_str);
if (msg_time == (time_t)-1) {
return false;
}
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
return false;
}
out->timestamp = msg_time;
strncpy(out->username, username, MAX_USERNAME_LEN - 1);
out->username[MAX_USERNAME_LEN - 1] = '\0';
strncpy(out->content, content, MAX_MESSAGE_LEN - 1);
out->content[MAX_MESSAGE_LEN - 1] = '\0';
return true;
}
static int append_dump_record(char **output, size_t *capacity,
size_t *len, const message_t *msg) {
char timestamp[64];
int needed;
size_t needed;
size_t available;
if (!output || !capacity || !len || !msg) {
return -1;
}
format_rfc3339_utc(msg->timestamp, timestamp, sizeof(timestamp));
needed = snprintf(NULL, 0, "%s|%s|%s\n", timestamp, msg->username,
msg->content);
if (needed < 0) {
if (message_log_format_record(msg, NULL, 0, &needed) < 0) {
return -1;
}
available = *capacity > *len ? *capacity - *len : 0;
if ((size_t)needed + 1 > available) {
if (needed + 1 > available) {
size_t new_capacity = *capacity ? *capacity : 1024;
while ((size_t)needed + 1 > new_capacity - *len) {
while (needed + 1 > new_capacity - *len) {
if (new_capacity > SIZE_MAX / 2) {
return -1;
}
@ -149,9 +52,11 @@ static int append_dump_record(char **output, size_t *capacity,
*capacity = new_capacity;
}
snprintf(*output + *len, *capacity - *len, "%s|%s|%s\n", timestamp,
msg->username, msg->content);
*len += (size_t)needed;
if (message_log_format_record(msg, *output + *len, *capacity - *len,
NULL) < 0) {
return -1;
}
*len += needed;
return 0;
}
@ -247,7 +152,7 @@ int message_load(message_t **messages, int max_messages) {
fseek(fp, 0, SEEK_SET);
read_messages:;
char line[2048];
char line[MESSAGE_LOG_MAX_LINE];
int count = 0;
time_t now = time(NULL);
@ -261,7 +166,7 @@ read_messages:;
}
message_t parsed;
if (!parse_log_record(line, &parsed, now)) {
if (!message_log_parse_record(line, &parsed, now)) {
continue;
}
@ -277,6 +182,9 @@ read_messages:;
/* Save a message to log file */
int message_save(const message_t *msg) {
char log_path[PATH_MAX];
message_t safe_msg;
char record[MAX_USERNAME_LEN + MAX_MESSAGE_LEN + 48];
size_t record_len = 0;
int rc = 0;
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
@ -291,36 +199,29 @@ int message_save(const message_t *msg) {
return -1;
}
/* Format timestamp as RFC3339 */
char timestamp[64];
struct tm tm_info;
gmtime_r(&msg->timestamp, &tm_info);
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm_info);
/* Sanitize username and content to prevent log injection */
char safe_username[MAX_USERNAME_LEN];
char safe_content[MAX_MESSAGE_LEN];
safe_msg.timestamp = msg->timestamp;
strncpy(safe_msg.username, msg->username, sizeof(safe_msg.username) - 1);
safe_msg.username[sizeof(safe_msg.username) - 1] = '\0';
strncpy(safe_username, msg->username, sizeof(safe_username) - 1);
safe_username[sizeof(safe_username) - 1] = '\0';
strncpy(safe_content, msg->content, sizeof(safe_content) - 1);
safe_content[sizeof(safe_content) - 1] = '\0';
strncpy(safe_msg.content, msg->content, sizeof(safe_msg.content) - 1);
safe_msg.content[sizeof(safe_msg.content) - 1] = '\0';
/* Replace pipe characters and newlines to prevent log format corruption */
for (char *p = safe_username; *p; p++) {
for (char *p = safe_msg.username; *p; p++) {
if (*p == '|' || *p == '\n' || *p == '\r') {
*p = '_';
}
}
for (char *p = safe_content; *p; p++) {
for (char *p = safe_msg.content; *p; p++) {
if (*p == '|' || *p == '\n' || *p == '\r') {
*p = ' ';
}
}
/* Write to file: timestamp|username|content */
if (fprintf(fp, "%s|%s|%s\n", timestamp, safe_username, safe_content) < 0 ||
if (message_log_format_record(&safe_msg, record, sizeof(record),
&record_len) < 0 ||
fwrite(record, 1, record_len, fp) != record_len ||
fflush(fp) != 0) {
rc = -1;
}
@ -361,7 +262,7 @@ int message_search(const char *query, message_t **results, int max_results) {
return 0;
}
char line[2048];
char line[MESSAGE_LOG_MAX_LINE];
int count = 0;
time_t now = time(NULL);
@ -373,7 +274,7 @@ int message_search(const char *query, message_t **results, int max_results) {
}
message_t m;
if (!parse_log_record(line, &m, now)) continue;
if (!message_log_parse_record(line, &m, now)) continue;
if (strcasestr(m.username, query) == NULL &&
strcasestr(m.content, query) == NULL) continue;
@ -440,7 +341,7 @@ int message_dump_text(char **output, size_t *output_len, int max_records) {
return 0;
}
char line[2048];
char line[MESSAGE_LOG_MAX_LINE];
time_t now = time(NULL);
while (fgets(line, sizeof(line), fp)) {
size_t line_len = strlen(line);
@ -450,7 +351,7 @@ int message_dump_text(char **output, size_t *output_len, int max_records) {
}
message_t parsed;
if (!parse_log_record(line, &parsed, now)) {
if (!message_log_parse_record(line, &parsed, now)) {
continue;
}

129
src/message_log.c Normal file
View file

@ -0,0 +1,129 @@
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE /* for timegm() on glibc */
#endif
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
#endif
#include "message_log.h"
#include "utf8.h"
static time_t parse_rfc3339_utc(const char *timestamp_str) {
struct tm tm = {0};
if (!timestamp_str) {
return (time_t)-1;
}
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
if (!result || *result != '\0') {
return (time_t)-1;
}
return timegm(&tm);
}
void message_log_format_timestamp_utc(time_t ts, char *buffer,
size_t buf_size) {
struct tm tm_info;
if (!buffer || buf_size == 0) {
return;
}
gmtime_r(&ts, &tm_info);
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
}
bool message_log_parse_record(const char *line, message_t *out, time_t now) {
char line_copy[MESSAGE_LOG_MAX_LINE];
char *first_sep;
char *second_sep;
char *timestamp_str;
char *username;
char *content;
time_t msg_time;
size_t line_len;
if (!line || !out) {
return false;
}
line_len = strlen(line);
if (line_len == 0 || line[line_len - 1] != '\n') {
return false;
}
if (line_len >= sizeof(line_copy)) {
return false;
}
memcpy(line_copy, line, line_len + 1);
line_copy[line_len - 1] = '\0';
first_sep = strchr(line_copy, '|');
if (!first_sep) {
return false;
}
second_sep = strchr(first_sep + 1, '|');
if (!second_sep || strchr(second_sep + 1, '|')) {
return false;
}
*first_sep = '\0';
*second_sep = '\0';
timestamp_str = line_copy;
username = first_sep + 1;
content = second_sep + 1;
if (timestamp_str[0] == '\0' || username[0] == '\0' ||
content[0] == '\0') {
return false;
}
if (strlen(username) >= MAX_USERNAME_LEN ||
strlen(content) >= MAX_MESSAGE_LEN) {
return false;
}
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) {
return false;
}
msg_time = parse_rfc3339_utc(timestamp_str);
if (msg_time == (time_t)-1) {
return false;
}
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
return false;
}
out->timestamp = msg_time;
strncpy(out->username, username, MAX_USERNAME_LEN - 1);
out->username[MAX_USERNAME_LEN - 1] = '\0';
strncpy(out->content, content, MAX_MESSAGE_LEN - 1);
out->content[MAX_MESSAGE_LEN - 1] = '\0';
return true;
}
int message_log_format_record(const message_t *msg, char *buffer,
size_t buf_size, size_t *record_len) {
char timestamp[64];
int needed;
if (!msg) {
return -1;
}
message_log_format_timestamp_utc(msg->timestamp, timestamp,
sizeof(timestamp));
needed = snprintf(buffer, buf_size, "%s|%s|%s\n", timestamp,
msg->username, msg->content);
if (needed < 0) {
return -1;
}
if (record_len) {
*record_len = (size_t)needed;
}
if (!buffer || buf_size == 0) {
return 0;
}
return (size_t)needed < buf_size ? 0 : -1;
}

View file

@ -12,6 +12,7 @@ endif
# Source files
UTF8_SRC = ../../src/utf8.c
MESSAGE_SRC = ../../src/message.c
MESSAGE_LOG_SRC = ../../src/message_log.c
COMMON_SRC = ../../src/common.c
COMMAND_CATALOG_SRC = ../../src/command_catalog.c
CLI_TEXT_SRC = ../../src/cli_text.c
@ -34,10 +35,10 @@ all: $(TESTS)
test_utf8: test_utf8.c $(UTF8_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_message: test_message.c $(MESSAGE_SRC) $(UTF8_SRC) $(COMMON_SRC)
test_message: test_message.c $(MESSAGE_SRC) $(MESSAGE_LOG_SRC) $(UTF8_SRC) $(COMMON_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_chat_room: test_chat_room.c $(CHAT_ROOM_SRC) $(MESSAGE_SRC) $(UTF8_SRC) $(COMMON_SRC)
test_chat_room: test_chat_room.c $(CHAT_ROOM_SRC) $(MESSAGE_SRC) $(MESSAGE_LOG_SRC) $(UTF8_SRC) $(COMMON_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)