mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:54:38 +08:00
Split message log record module
This commit is contained in:
parent
5240756f96
commit
3252e4583c
7 changed files with 190 additions and 133 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
21
include/message_log.h
Normal 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 */
|
||||
163
src/message.c
163
src/message.c
|
|
@ -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
129
src/message_log.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue