Compare commits

..

7 Commits

Author SHA1 Message Date
a546092a5f fix: add build names, use TEST_BUILDS constant, redirect errors to stderr
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 4m45s
CI Code / Linux (debian) (pull_request) Successful in 6m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m15s
CI Code / Linux (arch) (pull_request) Successful in 6m21s
2026-02-02 16:04:08 +03:00
5978c77a45 fix: address code review comments from PR #79
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 37s
CI Code / Code Coverage (pull_request) Successful in 4m47s
CI Code / Linux (debian) (pull_request) Successful in 6m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m12s
CI Code / Linux (arch) (pull_request) Successful in 6m19s
- Add TEST_GROUPS constant to eliminate magic number
- Use stdout for info messages, stderr for errors only
- Remove redundant comments and dead code
- Add CI check before copying coverage.info
- Unify variable declaration order
- Add port allocation explanation comment
- Restore /* 50ms */ inline comment
- Update CONTRIBUTING.md with build 1 description
2026-02-02 14:56:46 +03:00
cf86e60af6 ci: refactor ci-build.sh with constants and helper functions
All checks were successful
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Code Coverage (pull_request) Successful in 4m46s
CI Code / Linux (debian) (pull_request) Successful in 6m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m14s
CI Code / Linux (arch) (pull_request) Successful in 6m19s
- Add CMOCKA_PASSED_PATTERN, CMOCKA_FAILED_PATTERN constants
- Add COVERAGE_PATTERNS constant for lcov extraction
- Add parse_build_stats() function for parsing build logs
- Add extract_test_count() helper function
- Organize code into sections: Constants, Helper Functions, Test Verification
2026-01-29 17:45:11 +03:00
5100efca3b ci: remove coverage from linux builds
All checks were successful
CI Code / Check spelling (pull_request) Successful in 25s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Code Coverage (pull_request) Successful in 4m46s
CI Code / Linux (debian) (pull_request) Successful in 6m7s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m13s
CI Code / Linux (arch) (pull_request) Successful in 6m21s
Coverage is now collected only in the dedicated Code Coverage job.
2026-01-29 17:08:39 +03:00
adc5e26689 ci: integrate coverage into linux arch job, remove duplicate
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 4m47s
CI Code / Linux (debian) (pull_request) Successful in 6m37s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m39s
CI Code / Linux (arch) (pull_request) Successful in 6m49s
- Linux arch job now uploads coverage.info as artifact
- ci-build.sh saves coverage.info before cleanup
- Removed separate coverage job (was duplicating arch build)
- Saves ~25% CI time by eliminating redundant build
2026-01-29 17:01:39 +03:00
6c5c523a1a ci: fix coverage to match Code Coverage job output
All checks were successful
CI Code / Check coding style (pull_request) Successful in 28s
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Linux (arch) (pull_request) Successful in 6m46s
CI Code / Linux (debian) (pull_request) Successful in 6m37s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m37s
CI Code / Code Coverage (pull_request) Successful in 15m23s
Use --extract '*/src/src/*' instead of --remove to properly filter
production code from tests. Coverage now shows 27.5% lines matching
the dedicated Code Coverage job.
2026-01-28 23:10:09 +03:00
9a2a7614a9 ci: improve CI stability with parallel builds, Valgrind, and coverage
All checks were successful
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Linux (debian) (pull_request) Successful in 6m40s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m37s
CI Code / Linux (arch) (pull_request) Successful in 6m50s
CI Code / Code Coverage (pull_request) Successful in 15m25s
2026-01-28 20:42:42 +03:00
40 changed files with 258 additions and 1186 deletions

View File

@@ -50,9 +50,6 @@ jobs:
run: |
grep -P 'auto_(char|gchar|gcharv|guchar|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true
- name: Check CWE-134 format string vulnerabilities
run: ./check-cwe134.sh
- name: Install clang-format
run: |
sudo apt-get update

View File

@@ -144,16 +144,6 @@ scan-build make
scan-view ...
```
### Security checks
We have a static analyzer `check-cwe134.sh` that detects CWE-134 format string vulnerabilities. It runs automatically in CI but you can also run it locally:
```bash
./check-cwe134.sh
```
This checks for unsafe patterns where data could be passed directly as a format string to functions like `printf`, `cons_show`, etc. Never pass a raw string for formatting; use `"%s"` format specifier instead.
### Finding typos
We include a `.codespellrc` configuration file for `codespell` in the root directory.

View File

@@ -1,10 +1,9 @@
FROM archlinux:latest
FROM archlinux
ENV TERM=xterm
ENV CC="ccache gcc"
RUN pacman -Syyu --noconfirm
RUN pacman -Syu --noconfirm
# reflector is optional - if it fails due to network issues, continue with default mirrorlist
RUN pacman -S --needed --noconfirm reflector && \
(reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true)

View File

@@ -185,8 +185,6 @@ functionaltest_sources = \
tests/functionaltests/test_software.c tests/functionaltests/test_software.h \
tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \
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/functionaltests.c
main_source = src/main.c

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# check-cwe134.sh - Static analysis for CWE-134 format string vulnerabilities
#
# This script detects potentially unsafe usage of format string functions
# where user-controlled data may be passed without "%s" wrapper.
#
# Usage: ./check-cwe134.sh [directory]
set -e
DIR="${1:-src}"
echo "=== CWE-134 Format String Vulnerability Check ==="
echo "Scanning: $DIR"
echo ""
# Functions that accept format strings
FORMAT_FUNCS="cons_show|cons_debug|cons_show_error|log_info|log_error|log_warning|log_debug|win_println|win_print"
ERRORS=0
echo "Checking for unsafe format string usage..."
echo ""
# Pattern 1: function call with single variable argument (no format string)
# Example: cons_show(variable); - BAD
# Example: cons_show("%s", variable); - OK
# Matches: func(identifier) or func(identifier->member) or func(identifier[index])
RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\(\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*\w+\s*\])?\s*\)\s*;" "$DIR" 2>/dev/null || true)
# Filter out function definitions, declarations, and safe api_* wrappers
RESULTS=$(echo "$RESULTS" | grep -v "const char\|void \|^[^:]*:[0-9]*:[a-z_]*(\|api_cons_show\|api_log_" || true)
if [ -n "$RESULTS" ]; then
echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND:"
echo ""
echo "$RESULTS"
echo ""
ERRORS=$(echo "$RESULTS" | wc -l)
else
echo "✅ No obvious CWE-134 issues found."
fi
# Additional check: GString->str passed directly (not as %s argument)
echo ""
echo "Checking for GString->str passed to format functions..."
GSTRING_RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\([^)]*->str\s*\)" "$DIR" 2>/dev/null | grep -v '"%s"' || true)
if [ -n "$GSTRING_RESULTS" ]; then
echo "⚠️ GString->str passed without \"%s\" (review manually):"
echo ""
echo "$GSTRING_RESULTS"
echo ""
fi
echo ""
echo "=== Summary ==="
echo "Critical issues: $ERRORS"
if [ "$ERRORS" -gt 0 ]; then
echo ""
echo "Fix by adding \"%s\" format specifier:"
echo " BAD: cons_show(variable);"
echo " GOOD: cons_show(\"%s\", variable);"
exit 1
fi
exit 0

View File

@@ -2825,15 +2825,16 @@ cmd_search_index_any(char* term)
int terms_len = g_strv_length(processed_terms);
for (int i = 0; i < terms_len; i++) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, search_index);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* index_entry = (char*)value;
GList* index_keys = g_hash_table_get_keys(search_index);
GList* curr = index_keys;
while (curr) {
char* index_entry = g_hash_table_lookup(search_index, curr->data);
if (g_str_match_string(processed_terms[i], index_entry, FALSE)) {
results = g_list_append(results, key);
results = g_list_append(results, curr->data);
}
curr = g_list_next(curr);
}
g_list_free(index_keys);
}
return results;
@@ -2847,14 +2848,13 @@ cmd_search_index_all(char* term)
auto_gcharv gchar** terms = g_str_tokenize_and_fold(term, NULL, NULL);
int terms_len = g_strv_length(terms);
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, search_index);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* command = (char*)key;
char* command_index = (char*)value;
GList* commands = g_hash_table_get_keys(search_index);
GList* curr = commands;
while (curr) {
char* command = curr->data;
int matches = 0;
for (int i = 0; i < terms_len; i++) {
char* command_index = g_hash_table_lookup(search_index, command);
if (g_str_match_string(terms[i], command_index, FALSE)) {
matches++;
}
@@ -2862,8 +2862,11 @@ cmd_search_index_all(char* term)
if (matches == terms_len) {
results = g_list_append(results, command);
}
curr = g_list_next(curr);
}
g_list_free(commands);
return results;
}
@@ -2986,18 +2989,7 @@ command_docgen(void)
}
FILE* toc_fragment = fopen("toc_fragment.html", "w");
if (!toc_fragment) {
log_error("command_docgen(): unable to open toc_fragment.html for writing: %s", g_strerror(errno));
g_list_free(cmds);
return;
}
FILE* main_fragment = fopen("main_fragment.html", "w");
if (!main_fragment) {
log_error("command_docgen(): unable to open main_fragment.html for writing: %s", g_strerror(errno));
fclose(toc_fragment);
g_list_free(cmds);
return;
}
fputs("<ul><li><ul><li>\n", toc_fragment);
fputs("<hr>\n", main_fragment);
@@ -3102,11 +3094,6 @@ command_mangen(void)
return;
}
FILE* manpage = fopen(filename, "w");
if (!manpage) {
log_error("command_mangen(): unable to open %s for writing: %s", filename, g_strerror(errno));
curr = g_list_next(curr);
continue;
}
fprintf(manpage, "%s\n", header);
fputs(".SH NAME\n", manpage);

View File

@@ -184,7 +184,7 @@ _string_matches_one_of(const char* what, const char* is, bool is_can_be_null, co
}
va_end(ap);
if (s > 0)
cons_show("%s", errmsg);
cons_show(errmsg);
}
return ret;
}
@@ -418,7 +418,7 @@ cmd_connect(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
port = 0;
options_destroy(options);
@@ -711,7 +711,7 @@ _account_set_port(char* account_name, char* port)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port, &porti, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
} else {
accounts_set_port(account_name, porti);
@@ -903,7 +903,7 @@ _account_set_max_sessions(char* account_name, char* max_sessions_raw)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(max_sessions_raw, &max_sessions, 0, INT_MAX, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
return TRUE;
}
@@ -924,7 +924,7 @@ _account_set_presence_priority(char* account_name, char* presence, char* priorit
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(priority, &intval, -128, 127, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
@@ -1580,7 +1580,7 @@ _cmd_list_commands(GList* commands)
while (curr) {
gchar* cmd = curr->data;
if (count == 5) {
cons_show("%s", cmds->str);
cons_show(cmds->str);
g_string_free(cmds, TRUE);
cmds = g_string_new("");
count = 0;
@@ -1589,7 +1589,7 @@ _cmd_list_commands(GList* commands)
curr = g_list_next(curr);
count++;
}
cons_show("%s", cmds->str);
cons_show(cmds->str);
g_string_free(cmds, TRUE);
g_list_free(curr);
@@ -2407,7 +2407,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
}
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
@@ -2467,7 +2467,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster contact indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else {
@@ -2501,7 +2501,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster resource indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else if (g_strcmp0(args[1], "join") == 0) {
@@ -2527,7 +2527,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster presence indent set to: %d", intval);
rosterwin_roster();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
}
} else {
@@ -4371,7 +4371,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
wins_resize_all();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
return TRUE;
}
}
@@ -4391,7 +4391,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
occupantswin_occupants_all();
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
}
@@ -4970,8 +4970,8 @@ cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
alt_scheme = OMEMO_AESGCM_URL_SCHEME;
alt_fragment = _add_omemo_stream(&fd, &fh, &err);
if (err != NULL) {
cons_show_error("%s", err);
win_println(window, THEME_ERROR, "-", "%s", err);
cons_show_error(err);
win_println(window, THEME_ERROR, "-", err);
goto out;
}
#endif
@@ -5836,7 +5836,7 @@ cmd_inpblock(ProfWin* window, const char* const command, gchar** args)
prefs_set_inpblock(intval);
inp_nonblocking(FALSE);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6063,7 +6063,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
return TRUE;
}
@@ -6094,7 +6094,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize();
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
return TRUE;
}
@@ -6254,7 +6254,7 @@ cmd_log(ProfWin* window, const char* const command, gchar** args)
prefs_set_max_log_size(intval);
cons_show("Log maximum size set to %d bytes", intval);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
}
@@ -6304,7 +6304,7 @@ cmd_reconnect(ProfWin* window, const char* const command, gchar** args)
cons_show("Reconnect interval set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6330,7 +6330,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping interval set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6346,7 +6346,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping timeout set to %d seconds.", intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_bad_cmd_usage(command);
}
@@ -6416,7 +6416,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
cons_show("Auto away time set to: %d minutes.", minutesval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6439,7 +6439,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
}
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6506,7 +6506,7 @@ cmd_priority(ProfWin* window, const char* const command, gchar** args)
cl_ev_presence_send(last_presence, 0);
cons_show("Priority set to %d.", intval);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -6576,7 +6576,7 @@ cmd_tray(ProfWin* window, const char* const command, gchar** args)
tray_set_timer(intval);
}
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
return TRUE;
@@ -7181,21 +7181,22 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
}
cons_show("PGP keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
cons_show(" %s", pgp_key->name);
cons_show(" ID : %s", pgp_key->id);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
cons_show(" %s", key->name);
cons_show(" ID : %s", key->id);
auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) {
if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE");
} else {
cons_show(" Type : PUBLIC");
}
curr = g_list_next(curr);
}
g_list_free(keylist);
p_gpg_free_keys(keys);
return TRUE;
}
@@ -7236,24 +7237,25 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
GHashTable* pubkeys = p_gpg_pubkeys();
if (!pubkeys || g_hash_table_size(pubkeys) == 0) {
GList* jids = g_hash_table_get_keys(pubkeys);
if (!jids) {
cons_show("No contacts found with PGP public keys assigned.");
return TRUE;
}
cons_show("Assigned PGP public keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, pubkeys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* jid = (char*)key;
ProfPGPPubKeyId* pubkeyid = (ProfPGPPubKeyId*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
ProfPGPPubKeyId* pubkeyid = g_hash_table_lookup(pubkeys, jid);
if (pubkeyid->received) {
cons_show(" %s: %s (received)", jid, pubkeyid->id);
} else {
cons_show(" %s: %s (stored)", jid, pubkeyid->id);
}
curr = g_list_next(curr);
}
g_list_free(jids);
return TRUE;
}
@@ -7466,21 +7468,22 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
}
cons_show("OpenPGP keys:");
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
cons_show(" %s", pgp_key->name);
cons_show(" ID : %s", pgp_key->id);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
cons_show(" %s", key->name);
cons_show(" ID : %s", key->id);
auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) {
if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE");
} else {
cons_show(" Type : PUBLIC");
}
curr = g_list_next(curr);
}
g_list_free(keylist);
p_gpg_free_keys(keys);
return TRUE;
}
@@ -7488,8 +7491,8 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
else if (g_strcmp0(args[0], "contacts") == 0) {
GHashTable* keys = ox_gpg_public_keys();
cons_show("OpenPGP keys:");
GHashTableIter iter;
gpointer key, value;
GList* keylist = g_hash_table_get_keys(keys);
GList* curr = keylist;
GSList* roster_list = NULL;
jabber_conn_status_t conn_status = connection_get_status();
@@ -7499,16 +7502,15 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
roster_list = roster_get_contacts(ROSTER_ORD_NAME);
}
g_hash_table_iter_init(&iter, keys);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
PContact contact = NULL;
if (roster_list) {
GSList* curr_c = roster_list;
while (!contact && curr_c) {
contact = curr_c->data;
auto_gchar gchar* xmppuri = g_strdup_printf("xmpp:%s", p_contact_barejid(contact));
if (g_strcmp0(pgp_key->name, xmppuri)) {
if (g_strcmp0(key->name, xmppuri)) {
contact = NULL;
}
curr_c = g_slist_next(curr_c);
@@ -7516,10 +7518,11 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
}
if (contact) {
cons_show("%s - %s", pgp_key->fp, pgp_key->name);
cons_show("%s - %s", key->fp, key->name);
} else {
cons_show("%s - %s (not in roster)", pgp_key->fp, pgp_key->name);
cons_show("%s - %s (not in roster)", key->fp, key->name);
}
curr = g_list_next(curr);
}
} else if (g_strcmp0(args[0], "start") == 0) {
@@ -9609,7 +9612,7 @@ cmd_register(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) {
cons_show("%s", err_msg);
cons_show(err_msg);
cons_show("");
port = 0;
options_destroy(options);
@@ -9679,7 +9682,7 @@ cmd_strophe(ProfWin* window, const char* const command, gchar** args)
prefs_set_string(PREF_STROPHE_VERBOSITY, args[1]);
return TRUE;
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
} else if (g_strcmp0(args[0], "sm") == 0) {
if (g_strcmp0(args[1], "no-resend") == 0) {

View File

@@ -143,7 +143,7 @@ auto_close_gfd(gint* fd)
return;
if (close(*fd) == EOF)
log_error("%s", g_strerror(errno));
log_error(g_strerror(errno));
}
/**
@@ -158,7 +158,7 @@ auto_close_FILE(FILE** fd)
return;
if (fclose(*fd) == EOF)
log_error("%s", g_strerror(errno));
log_error(g_strerror(errno));
}
static gboolean

View File

@@ -65,38 +65,6 @@ static gboolean _check_available_space_for_db_migration(char* path_to_db);
static const int latest_version = 2;
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
static void
_db_teardown(const char* ctx)
{
if (g_chatlog_database) {
int rc = sqlite3_close_v2(g_chatlog_database);
if (rc != SQLITE_OK) {
log_warning("sqlite3_close_v2 in %s returned %d; database may still have active statements.",
ctx ? ctx : "db_teardown", rc);
}
g_chatlog_database = NULL;
}
// Safe to call unconditionally; no-op if not initialized.
// See: https://www.sqlite.org/c3ref/initialize.html
sqlite3_shutdown();
}
// Helper: prepare a statement and log a contextual error on failure
static gboolean
_db_prepare_ctx(const char* query, sqlite3_stmt** stmt, const char* ctx)
{
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in %s: (error code: %d) %s",
ctx ? ctx : "sqlite3_prepare_v2",
rc,
sqlite3_errmsg(g_chatlog_database));
return FALSE;
}
return TRUE;
}
static char*
_db_strdup(const char* str)
{
@@ -130,19 +98,17 @@ log_database_init(ProfAccount* account)
auto_char char* filename = _get_db_filename(account);
if (!filename) {
sqlite3_shutdown();
return FALSE;
}
ret = sqlite3_open(filename, &g_chatlog_database);
if (ret != SQLITE_OK) {
const char* err_msg = g_chatlog_database ? sqlite3_errmsg(g_chatlog_database) : "(no handle)";
const char* err_msg = sqlite3_errmsg(g_chatlog_database);
log_error("Error opening SQLite database: %s", err_msg);
_db_teardown("log_database_init(open)");
return FALSE;
}
char* err_msg = NULL;
char* err_msg;
int db_version = _get_db_version();
if (db_version == latest_version) {
@@ -250,7 +216,6 @@ out:
} else {
log_error("Unknown SQLite error in log_database_init().");
}
_db_teardown("log_database_init(out)");
return FALSE;
}
@@ -258,7 +223,11 @@ void
log_database_close(void)
{
log_debug("log_database_close() called");
_db_teardown("log_database_close");
if (g_chatlog_database) {
sqlite3_close(g_chatlog_database);
sqlite3_shutdown();
g_chatlog_database = NULL;
}
}
void
@@ -312,15 +281,8 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
{
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
// Always return a valid ProfMessage to avoid NULL dereferences in callers
ProfMessage* msg = message_init();
if (!myjid || !myjid->str) {
// If caller requested the last message and we have no context, fall back to now
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
if (!myjid->str)
return NULL;
const char* order = is_last ? "DESC" : "ASC";
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
@@ -331,19 +293,17 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
if (!query) {
log_error("Could not allocate memory for SQL query in log_database_get_limits_info()");
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
return NULL;
}
if (!_db_prepare_ctx(query, &stmt, "log_database_get_limits_info()")) {
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Unknown SQLite error in log_database_get_last_info().");
return NULL;
}
ProfMessage* msg = message_init();
if (sqlite3_step(stmt) == SQLITE_ROW) {
char* archive_id = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
@@ -353,11 +313,6 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
}
sqlite3_finalize(stmt);
// If nothing was found and caller expects the last message, provide a sane default
if (!msg->timestamp && is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
@@ -402,7 +357,9 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const gchar*
return DB_RESPONSE_ERROR;
}
if (!_db_prepare_ctx(query, &stmt, "log_database_get_previous_chat()")) {
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in log_database_get_previous_chat(): (error code: %d) %s", rc, sqlite3_errmsg(g_chatlog_database));
return DB_RESPONSE_ERROR;
}
@@ -546,7 +503,9 @@ _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Ji
}
sqlite3_stmt* lmc_stmt = NULL;
if (!_db_prepare_ctx(replace_check_query, &lmc_stmt, "_add_to_db(replace_check)")) {
if (SQLITE_OK != sqlite3_prepare_v2(g_chatlog_database, replace_check_query, -1, &lmc_stmt, NULL)) {
log_error("SQLite error in _add_to_db() on selecting original message: %s", sqlite3_errmsg(g_chatlog_database));
return;
}
@@ -583,7 +542,8 @@ _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Ji
}
sqlite3_stmt* stmt;
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, duplicate_check_query, -1, &stmt, NULL)) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
@@ -639,7 +599,8 @@ _get_db_version(void)
int current_version = -1;
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
sqlite3_stmt* statement;
if (_db_prepare_ctx(query, &statement, "_get_db_version()")) {
if (sqlite3_prepare_v2(g_chatlog_database, query, -1, &statement, NULL) == SQLITE_OK) {
if (sqlite3_step(statement) == SQLITE_ROW) {
current_version = sqlite3_column_int(statement, 0);
}

View File

@@ -532,13 +532,11 @@ omemo_set_device_list(const char* const from, GList* device_list)
for (device_id = device_list; device_id != NULL; device_id = device_id->next) {
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid->barejid);
if (known_identities) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, known_identities);
while (g_hash_table_iter_next(&iter, &key, &value)) {
if (device_id->data == value) {
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(key));
omemo_trust(jid->barejid, omemo_format_fingerprint(key));
GList* fp = NULL;
for (fp = g_hash_table_get_keys(known_identities); fp != NULL; fp = fp->next) {
if (device_id->data == g_hash_table_lookup(known_identities, fp->data)) {
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(fp->data));
omemo_trust(jid->barejid, omemo_format_fingerprint(fp->data));
}
}
}

View File

@@ -345,13 +345,14 @@ p_gpg_list_keys(void)
// TODO: move autocomplete in other place
autocomplete_clear(key_ac);
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, result);
while (g_hash_table_iter_next(&iter, &key, &value)) {
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
autocomplete_add(key_ac, pgp_key->id);
GList* ids = g_hash_table_get_keys(result);
GList* curr = ids;
while (curr) {
ProfPGPKey* key = g_hash_table_lookup(result, curr->data);
autocomplete_add(key_ac, key->id);
curr = curr->next;
}
g_list_free(ids);
return result;
}

View File

@@ -141,16 +141,18 @@ autocompleters_complete(const char* const input, gboolean previous)
while (curr_hash) {
GHashTable* key_to_ac = curr_hash->data;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, key_to_ac);
while (g_hash_table_iter_next(&iter, &key, &value)) {
result = autocomplete_param_with_ac(input, key, value, TRUE, previous);
GList* keys = g_hash_table_get_keys(key_to_ac);
GList* curr = keys;
while (curr) {
result = autocomplete_param_with_ac(input, curr->data, g_hash_table_lookup(key_to_ac, curr->data), TRUE, previous);
if (result) {
g_list_free(ac_hashes);
g_list_free(keys);
return result;
}
curr = g_list_next(curr);
}
g_list_free(keys);
curr_hash = g_list_next(curr_hash);
}
@@ -160,19 +162,22 @@ autocompleters_complete(const char* const input, gboolean previous)
curr_hash = filepath_hashes;
while (curr_hash) {
GHashTable* prefixes_hash = curr_hash->data;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, prefixes_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* prefix = (char*)key;
GList* prefixes = g_hash_table_get_keys(prefixes_hash);
GList* curr_prefix = prefixes;
while (curr_prefix) {
char* prefix = curr_prefix->data;
if (g_str_has_prefix(input, prefix)) {
result = cmd_ac_complete_filepath(input, prefix, previous);
if (result) {
g_list_free(filepath_hashes);
g_list_free(prefixes);
return result;
}
}
curr_prefix = g_list_next(curr_prefix);
}
g_list_free(prefixes);
curr_hash = g_list_next(curr_hash);
}

View File

@@ -149,14 +149,15 @@ callbacks_remove(const char* const plugin_name)
{
GHashTable* command_hash = g_hash_table_lookup(p_commands, plugin_name);
if (command_hash) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, command_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* command = (char*)key;
GList* commands = g_hash_table_get_keys(command_hash);
GList* curr = commands;
while (curr) {
char* command = curr->data;
cmd_ac_remove(command);
cmd_ac_remove_help(&command[1]);
curr = g_list_next(curr);
}
g_list_free(commands);
}
g_hash_table_remove(p_commands, plugin_name);
@@ -164,12 +165,13 @@ callbacks_remove(const char* const plugin_name)
GHashTable* tag_to_win_cb_hash = g_hash_table_lookup(p_window_callbacks, plugin_name);
if (tag_to_win_cb_hash) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, tag_to_win_cb_hash);
while (g_hash_table_iter_next(&iter, &key, &value)) {
wins_close_plugin(key);
GList* tags = g_hash_table_get_keys(tag_to_win_cb_hash);
GList* curr = tags;
while (curr) {
wins_close_plugin(curr->data);
curr = g_list_next(curr);
}
g_list_free(tags);
}
g_hash_table_remove(p_window_callbacks, plugin_name);

View File

@@ -107,11 +107,10 @@ disco_remove_features(const char* plugin_name)
return;
}
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, plugin_features_set);
while (g_hash_table_iter_next(&iter, &key, &value)) {
char* feature = (char*)key;
GList* plugin_feature_list = g_hash_table_get_keys(plugin_features_set);
GList* curr = plugin_feature_list;
while (curr) {
char* feature = curr->data;
if (g_hash_table_contains(features, feature)) {
void* refcountp = g_hash_table_lookup(features, feature);
int refcount = GPOINTER_TO_INT(refcountp);
@@ -122,7 +121,10 @@ disco_remove_features(const char* plugin_name)
g_hash_table_replace(features, strdup(feature), GINT_TO_POINTER(refcount));
}
}
curr = g_list_next(curr);
}
g_list_free(plugin_feature_list);
}
GList*

View File

@@ -887,8 +887,8 @@ _python_undefined_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook);
g_string_append(err_msg, "(): return value undefined, expected ");
g_string_append(err_msg, type);
log_error("%s", err_msg->str);
cons_show_error("%s", err_msg->str);
log_error(err_msg->str);
cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE);
}
@@ -901,8 +901,8 @@ _python_type_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook);
g_string_append(err_msg, "(): incorrect return type, expected ");
g_string_append(err_msg, type);
log_error("%s", err_msg->str);
cons_show_error("%s", err_msg->str);
log_error(err_msg->str);
cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE);
}

View File

@@ -135,7 +135,7 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
*/
min_runtime += waittime;
} else {
log_error("%s", err_msg);
log_error(err_msg);
g_free(err_msg);
commands = NULL;
}
@@ -245,7 +245,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
if (prof_log_level == PROF_LEVEL_DEBUG) {
ProfWin* console = wins_get_console();
win_println(console, THEME_DEFAULT, "-", "Debug mode enabled! Logging to: ");
win_println(console, THEME_DEFAULT, "-", "%s", get_log_file_location());
win_println(console, THEME_DEFAULT, "-", get_log_file_location());
}
session_init();
cmd_init();

View File

@@ -311,7 +311,7 @@ http_file_put(void* userdata)
}
win_update_entry_message(upload->window, upload->put_url, err_msg);
}
cons_show_error("%s", err_msg);
cons_show_error(err_msg);
} else {
if (!upload->cancel) {
auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': 100%%", upload->filename);
@@ -327,7 +327,7 @@ http_file_put(void* userdata)
if (!fail_msg) {
fail_msg = g_strdup(FALLBACK_MSG);
}
cons_show_error("%s", fail_msg);
cons_show_error(fail_msg);
} else {
switch (upload->window->type) {
case WIN_CHAT:

View File

@@ -140,10 +140,6 @@ void
buffer_remove_entry(ProfBuff buffer, int entry)
{
GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
// Index out of range; nothing to remove
return;
}
ProfBuffEntry* e = node->data;
buffer->lines -= e->_lines;
_free_entry(e);
@@ -172,9 +168,6 @@ ProfBuffEntry*
buffer_get_entry(ProfBuff buffer, int entry)
{
GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
return NULL;
}
return node->data;
}
@@ -196,7 +189,6 @@ buffer_get_entry_by_id(ProfBuff buffer, const char* const id)
static ProfBuffEntry*
_create_entry(const char* show_char, int pad_indent, GDateTime* time, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message, DeliveryReceipt* receipt, const char* const id, int y_start_pos, int y_end_pos)
{
assert(time != NULL);
ProfBuffEntry* e = malloc(sizeof(struct prof_buff_entry_t));
e->show_char = STRDUP_OR_NULL(show_char);
e->pad_indent = pad_indent;

View File

@@ -592,11 +592,8 @@ chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* e
{
auto_gchar gchar* _end_time = NULL;
if (!end_time && buffer_size(((ProfWin*)chatwin)->layout->buffer) > 0) {
ProfBuffEntry* first = buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0);
if (first && first->time) {
_end_time = g_date_time_format_iso8601(first->time);
end_time = _end_time;
}
_end_time = g_date_time_format_iso8601(buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0)->time);
end_time = _end_time;
}
GSList* history = NULL;

View File

@@ -152,7 +152,7 @@ cons_bad_cmd_usage(const char* const cmd)
g_string_printf(msg, "Invalid usage, see '/help %s' for details.", &cmd[1]);
cons_show("");
cons_show("%s", msg->str);
cons_show(msg->str);
g_string_free(msg, TRUE);
}
@@ -773,7 +773,7 @@ cons_show_disco_info(const char* jid, GSList* identities, GSList* features)
if (identity->category) {
identity_str = g_string_append(identity_str, identity->category);
}
cons_show("%s", identity_str->str);
cons_show(identity_str->str);
g_string_free(identity_str, TRUE);
identities = g_slist_next(identities);
}
@@ -938,7 +938,7 @@ cons_show_account_list(gchar** accounts)
theme_item_t presence_colour = theme_main_presence_attrs(string_from_resource_presence(presence));
win_println(console, presence_colour, "-", "%s", accounts[i]);
} else {
cons_show("%s", accounts[i]);
cons_show(accounts[i]);
}
}
cons_show("");
@@ -1019,7 +1019,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", manual->str);
cons_show(manual->str);
g_string_free(manual, TRUE);
}
if (g_list_length(account->otr_opportunistic) > 0) {
@@ -1032,7 +1032,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", opportunistic->str);
cons_show(opportunistic->str);
g_string_free(opportunistic, TRUE);
}
if (g_list_length(account->otr_always) > 0) {
@@ -1045,7 +1045,7 @@ cons_show_account(ProfAccount* account)
}
curr = curr->next;
}
cons_show("%s", always->str);
cons_show(always->str);
g_string_free(always, TRUE);
}
@@ -2297,7 +2297,7 @@ cons_show_themes(GSList* themes)
} else {
cons_show("Available themes:");
while (themes) {
cons_show("%s", themes->data);
cons_show(themes->data);
themes = g_slist_next(themes);
}
}
@@ -2315,7 +2315,7 @@ cons_show_scripts(GSList* scripts)
} else {
cons_show("Scripts:");
while (scripts) {
cons_show("%s", scripts->data);
cons_show(scripts->data);
scripts = g_slist_next(scripts);
}
}

View File

@@ -445,7 +445,7 @@ ui_handle_error(const char* const err_msg)
GString* msg = g_string_new("");
g_string_printf(msg, "Error %s", err_msg);
cons_show_error("%s", msg->str);
cons_show_error(msg->str);
g_string_free(msg, TRUE);
}
@@ -461,7 +461,7 @@ ui_invalid_command_usage(const char* const cmd, void (*setting_func)(void))
(*setting_func)();
} else {
cons_show("");
cons_show("%s", msg->str);
cons_show(msg->str);
ProfWin* current = wins_get_current();
if (current->type == WIN_CHAT) {
win_println(current, THEME_DEFAULT, "-", "%s", msg->str);

View File

@@ -144,14 +144,10 @@ void
create_input_window(void)
{
/* MB_CUR_MAX is evaluated at runtime depending on the current
* locale; ensure our own compiled-in maximum is sufficient.
* Fail gracefully instead of aborting in production.
* locale, therefore we check that our own version is big enough
* and bail out if it isn't.
*/
if (MB_CUR_MAX > PROF_MB_CUR_MAX) {
log_error("Locale MB_CUR_MAX (%zu) exceeds compiled limit (%d)", (size_t)MB_CUR_MAX, PROF_MB_CUR_MAX);
cons_show_error("Unsupported locale. Before running, execute in terminal: export LC_ALL=C.UTF-8");
return;
}
assert(MB_CUR_MAX <= PROF_MB_CUR_MAX);
#ifdef NCURSES_REENTRANT
set_escdelay(25);
#else
@@ -167,10 +163,6 @@ create_input_window(void)
rl_callback_handler_install(NULL, _inp_rl_linehandler);
inp_win = newpad(1, INP_WIN_MAX);
if (!inp_win) {
log_error("Failed to allocate input window pad");
return;
}
wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT));
keypad(inp_win, TRUE);
wmove(inp_win, 0, 0);
@@ -246,9 +238,6 @@ inp_readline(void)
void
inp_win_resize(void)
{
if (!inp_win) {
return;
}
int col = getcurx(inp_win);
int wcols = getmaxx(stdscr);
@@ -296,10 +285,8 @@ void
inp_close(void)
{
rl_callback_handler_remove();
if (inp_win) {
delwin(inp_win);
inp_win = NULL;
}
delwin(inp_win);
inp_win = NULL;
fclose(discard);
discard = NULL;
}
@@ -307,9 +294,6 @@ inp_close(void)
char*
inp_get_line(void)
{
if (!inp_win) {
return NULL;
}
werase(inp_win);
wmove(inp_win, 0, 0);
_inp_win_update_virtual();
@@ -334,9 +318,6 @@ inp_set_line(const char* const new_line)
char*
inp_get_password(void)
{
if (!inp_win) {
return NULL;
}
werase(inp_win);
wmove(inp_win, 0, 0);
_inp_win_update_virtual();

View File

@@ -40,8 +40,6 @@
#include <string.h>
#include <stdlib.h>
#include "log.h"
#ifdef HAVE_NCURSESW_NCURSES_H
#include <ncursesw/ncurses.h>
#elif HAVE_NCURSES_H
@@ -113,24 +111,16 @@ status_bar_init(void)
int row = screen_statusbar_row();
int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_init: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
statusbar_win = newwin(1, cols, row, 0);
if (statusbar_win) {
status_bar_draw();
}
status_bar_draw();
}
void
status_bar_close(void)
{
if (statusbar_win) {
delwin(statusbar_win);
statusbar_win = NULL;
}
delwin(statusbar_win);
statusbar_win = NULL;
if (statusbar) {
if (statusbar->time) {
g_free(statusbar->time);
@@ -155,14 +145,7 @@ status_bar_close(void)
void
status_bar_resize(void)
{
if (!statusbar_win) {
return;
}
int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_resize: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
werase(statusbar_win);
int row = screen_statusbar_row();
wresize(statusbar_win, 1, cols);
@@ -302,9 +285,6 @@ status_bar_clear_fulljid(void)
void
status_bar_draw(void)
{
if (!statusbar_win) {
return;
}
werase(statusbar_win);
wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT));
@@ -694,13 +674,8 @@ _display_name(StatusBarTab* tab)
fullname = g_strconcat(mucwin_title, " conf", NULL);
} else if (tab->window_type == WIN_PRIVATE) {
auto_jid Jid* jid = jid_create(tab->identifier);
if (jid) {
auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE);
fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
} else {
// Fallback: use identifier directly if JID parsing failed
fullname = strdup(tab->identifier);
}
auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE);
fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
} else {
fullname = strdup("window");
}

View File

@@ -73,16 +73,9 @@ void
create_title_bar(void)
{
int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
int row = screen_titlebar_row();
win = newwin(1, cols, row, 0);
if (!win) {
// Failed to create title bar window; skip initialization to avoid NULL deref
return;
}
wbkgd(win, theme_attrs(THEME_TITLE_TEXT));
title_bar_console();
title_bar_set_presence(CONTACT_OFFLINE);
@@ -95,18 +88,13 @@ create_title_bar(void)
void
free_title_bar(void)
{
if (win) {
delwin(win);
win = NULL;
}
delwin(win);
win = NULL;
}
void
title_bar_update_virtual(void)
{
if (!win) {
return;
}
ProfWin* window = wins_get_current();
if (window->type != WIN_CONSOLE) {
if (typing_elapsed) {
@@ -126,13 +114,7 @@ title_bar_update_virtual(void)
void
title_bar_resize(void)
{
if (!win) {
return;
}
int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
werase(win);
@@ -149,9 +131,6 @@ title_bar_resize(void)
void
title_bar_console(void)
{
if (!win) {
return;
}
werase(win);
if (typing_elapsed) {
g_timer_destroy(typing_elapsed);
@@ -213,9 +192,6 @@ title_bar_set_typing(gboolean is_typing)
static void
_title_bar_draw(void)
{
if (!win) {
return;
}
int pos;
int maxrightpos;
ProfWin* current = wins_get_current();

View File

@@ -75,22 +75,12 @@ static void _win_print_internal(ProfWin* window, const char* show_char, int pad_
int flags, theme_item_t theme_item, const char* const from, const char* const message, DeliveryReceipt* receipt);
static void _win_print_wrapped(WINDOW* win, const char* const message, size_t indent, int pad_indent);
// Helper: clamp a subwindow width to a sane range [1, cols-1] if possible
static int
_check_subwin_width(int cols, int width)
{
return cols <= 1 ? 1 : CLAMP(width, 1, cols - 1);
}
int
win_roster_cols(void)
{
int roster_win_percent = prefs_get_roster_size();
int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * roster_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
return CEILING((((double)cols) / 100) * roster_win_percent);
}
int
@@ -98,10 +88,7 @@ win_occpuants_cols(void)
{
int occupants_win_percent = prefs_get_occupants_size();
int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * occupants_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
return CEILING((((double)cols) / 100) * occupants_win_percent);
}
static ProfLayout*
@@ -157,7 +144,6 @@ win_create_console(void)
ProfWin*
win_create_chat(const char* const barejid)
{
assert(barejid != NULL);
ProfChatWin* new_win = malloc(sizeof(ProfChatWin));
new_win->window.type = WIN_CHAT;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -189,7 +175,6 @@ win_create_chat(const char* const barejid)
ProfWin*
win_create_muc(const char* const roomjid)
{
assert(roomjid != NULL);
ProfMucWin* new_win = malloc(sizeof(ProfMucWin));
int cols = getmaxx(stdscr);
@@ -248,8 +233,6 @@ win_create_muc(const char* const roomjid)
ProfWin*
win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata)
{
assert(roomjid != NULL);
assert(form != NULL);
ProfConfWin* new_win = malloc(sizeof(ProfConfWin));
new_win->window.type = WIN_CONFIG;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -268,7 +251,6 @@ win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback
ProfWin*
win_create_private(const char* const fulljid)
{
assert(fulljid != NULL);
ProfPrivateWin* new_win = malloc(sizeof(ProfPrivateWin));
new_win->window.type = WIN_PRIVATE;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -299,8 +281,6 @@ win_create_xmlconsole(void)
ProfWin*
win_create_plugin(const char* const plugin_name, const char* const tag)
{
assert(plugin_name != NULL);
assert(tag != NULL);
ProfPluginWin* new_win = malloc(sizeof(ProfPluginWin));
new_win->window.type = WIN_PLUGIN;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -317,7 +297,6 @@ win_create_plugin(const char* const plugin_name, const char* const tag)
ProfWin*
win_create_vcard(vCard* vcard)
{
assert(vcard != NULL);
ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin));
new_win->window.type = WIN_VCARD;
new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -369,7 +348,7 @@ win_get_title(ProfWin* window)
const ProfConfWin* confwin = (ProfConfWin*)window;
assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
auto_gchar gchar* mucwin_title = mucwin_generate_title(confwin->roomjid, PREF_TITLEBAR_MUC_TITLE);
if (confwin->form && confwin->form->modified) {
if (confwin->form->modified) {
return g_strconcat(mucwin_title, " config *", NULL);
}
return g_strconcat(mucwin_title, " config", NULL);
@@ -577,25 +556,7 @@ win_show_subwin(ProfWin* window)
}
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// If a subwindow already exists (e.g. repeated call), destroy it to avoid leaks
if (layout->subwin) {
delwin(layout->subwin);
layout->subwin = NULL;
}
// Ensure minimum width to avoid creating a zero-width pad
if (subwin_cols <= 0) {
subwin_cols = 1;
}
layout->subwin = newpad(PAD_SIZE, subwin_cols);
if (layout->subwin == NULL) {
// Failed to allocate subwindow; keep base window resized to full width
log_error("Failed to create subwindow pad (cols=%d)", subwin_cols);
wresize(layout->base.win, PAD_SIZE, cols);
win_redraw(window);
return;
}
wbkgd(layout->subwin, theme_attrs(THEME_TEXT));
wresize(layout->base.win, PAD_SIZE, cols - subwin_cols);
win_redraw(window);
@@ -949,11 +910,6 @@ win_refresh_with_subwin(ProfWin* window)
int row_end = screen_mainwin_row_end();
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// Safety: if subwindow is not active, nothing to refresh
if (layout == NULL || layout->subwin == NULL) {
return;
}
if (window->type == WIN_MUC) {
subwin_cols = win_occpuants_cols();
} else if (window->type == WIN_CONSOLE) {
@@ -2082,14 +2038,7 @@ win_print_loading_history(ProfWin* window)
gboolean is_buffer_empty = buffer_size(window->layout->buffer) == 0;
if (!is_buffer_empty) {
ProfBuffEntry* first = buffer_get_entry(window->layout->buffer, 0);
if (first && first->time) {
timestamp = first->time;
} else {
// Fallback to current time if entry/time is unavailable
timestamp = g_date_time_new_now_local();
is_buffer_empty = TRUE; // ensure we unref fallback timestamp below
}
timestamp = buffer_get_entry(window->layout->buffer, 0)->time;
} else {
timestamp = g_date_time_new_now_local();
}
@@ -2289,7 +2238,7 @@ void
win_handle_command_exec_result_note(ProfWin* window, const char* const type, const char* const value)
{
assert(window != NULL);
win_println(window, THEME_DEFAULT, "!", "%s", value);
win_println(window, THEME_DEFAULT, "!", value);
}
void

View File

@@ -407,10 +407,8 @@ wins_get_by_string(const char* str)
if (barejid) {
ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin) {
free(barejid);
return (ProfWin*)chatwin;
}
free(barejid);
}
}
@@ -605,9 +603,6 @@ wins_new_xmlconsole(void)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_xmlconsole();
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, "xmlconsole");
autocomplete_add(wins_close_ac, "xmlconsole");
@@ -619,9 +614,6 @@ wins_new_chat(const char* const barejid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_chat(barejid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, barejid);
@@ -645,9 +637,6 @@ wins_new_muc(const char* const roomjid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_muc(roomjid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, roomjid);
autocomplete_add(wins_close_ac, roomjid);
@@ -662,9 +651,6 @@ wins_new_config(const char* const roomjid, DataForm* form, ProfConfWinCallback s
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_config(roomjid, form, submit, cancel, userdata);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin;
@@ -675,9 +661,6 @@ wins_new_private(const char* const fulljid)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_private(fulljid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, fulljid);
autocomplete_add(wins_close_ac, fulljid);
@@ -692,9 +675,6 @@ wins_new_plugin(const char* const plugin_name, const char* const tag)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_plugin(plugin_name, tag);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, tag);
autocomplete_add(wins_close_ac, tag);
@@ -706,9 +686,6 @@ wins_new_vcard(vCard* vcard)
{
int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_vcard(vcard);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin;

View File

@@ -171,15 +171,16 @@ caps_get_features(void)
{
GList* result = NULL;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, prof_features);
while (g_hash_table_iter_next(&iter, &key, &value)) {
result = g_list_append(result, strdup((char*)key));
GList* features_as_list = g_hash_table_get_keys(prof_features);
GList* curr = features_as_list;
while (curr) {
result = g_list_append(result, strdup(curr->data));
curr = g_list_next(curr);
}
g_list_free(features_as_list);
GList* plugin_features = plugins_get_disco_features();
GList* curr = plugin_features;
curr = plugin_features;
while (curr) {
result = g_list_append(result, strdup(curr->data));
curr = g_list_next(curr);

View File

@@ -148,7 +148,7 @@ connection_init(void)
if (string_to_verbosity(v, &verbosity, &err_msg)) {
xmpp_ctx_set_verbosity(conn.xmpp_ctx, verbosity);
} else {
cons_show("%s", err_msg);
cons_show(err_msg);
}
conn.xmpp_conn = xmpp_conn_new(conn.xmpp_ctx);
@@ -638,18 +638,22 @@ gboolean
connection_supports(const char* const feature)
{
gboolean ret = FALSE;
GHashTableIter iter;
gpointer key, value;
GList* jids = g_hash_table_get_keys(conn.features_by_jid);
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &key, &value)) {
GHashTable* features = (GHashTable*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) {
ret = TRUE;
break;
}
curr = g_list_next(curr);
}
g_list_free(jids);
return ret;
}
@@ -660,17 +664,22 @@ connection_jid_for_feature(const char* const feature)
return NULL;
}
GHashTableIter iter;
gpointer key, value;
GList* jids = g_hash_table_get_keys(conn.features_by_jid);
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &key, &value)) {
GHashTable* features = (GHashTable*)value;
GList* curr = jids;
while (curr) {
char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) {
return (const char*)key;
g_list_free(jids);
return jid;
}
curr = g_list_next(curr);
}
g_list_free(jids);
return NULL;
}
@@ -1025,7 +1034,7 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
conn.sm_state = xmpp_conn_get_sm_state(conn.xmpp_conn);
if (send_queue_len > 0 && prefs_get_boolean(PREF_STROPHE_SM_RESEND)) {
conn.queued_messages = calloc(send_queue_len + 1, sizeof(*conn.queued_messages));
for (int n = 0; n < send_queue_len; ++n) {
for (int n = 0; n < send_queue_len && conn.queued_messages[n]; ++n) {
conn.queued_messages[n] = xmpp_conn_send_queue_drop_element(conn.xmpp_conn, XMPP_QUEUE_OLDEST);
}
} else if (send_queue_len > 0) {
@@ -1180,13 +1189,12 @@ connection_debug_print_features()
continue;
}
GHashTableIter feature_iter;
gpointer feature_key, feature_value;
g_hash_table_iter_init(&feature_iter, features);
while (g_hash_table_iter_next(&feature_iter, &feature_key, &feature_value)) {
const char* feature = (const char*)feature_key;
GList* feature_keys = g_hash_table_get_keys(features);
for (GList* l = feature_keys; l != NULL; l = l->next) {
const char* feature = (const char*)l->data;
log_debug("%s:\t%s", jid, feature);
}
g_list_free(feature_keys);
}
log_debug("=== End of Features ===");

View File

@@ -437,7 +437,18 @@ form_get_form_type_field(DataForm* form)
gboolean
form_tag_exists(DataForm* form, const char* const tag)
{
return g_hash_table_contains(form->tag_to_var, tag);
GList* tags = g_hash_table_get_keys(form->tag_to_var);
GList* curr = tags;
while (curr) {
if (g_strcmp0(curr->data, tag) == 0) {
g_list_free(tags);
return TRUE;
}
curr = g_list_next(curr);
}
g_list_free(tags);
return FALSE;
}
form_field_type_t

View File

@@ -2532,6 +2532,9 @@ _disco_items_result_handler(xmpp_stanza_t* const stanza)
}
xmpp_stanza_t* child = xmpp_stanza_get_children(query);
if (child == NULL) {
return;
}
while (child) {
const char* stanza_name = xmpp_stanza_get_name(child);

View File

@@ -868,7 +868,7 @@ _handle_error(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str);
log_info(log_msg->str);
g_string_free(log_msg, TRUE);

View File

@@ -326,7 +326,7 @@ _ox_metadata_result(xmpp_stanza_t* const stanza, void* const userdata)
if (fingerprint) {
if (strlen(fingerprint) == KEYID_LENGTH) {
cons_show("%s", fingerprint);
cons_show(fingerprint);
} else {
cons_show("OX: Wrong char size of public key");
log_error("[OX] Wrong chat size of public key %s", fingerprint);

View File

@@ -455,7 +455,7 @@ _presence_error_handler(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str);
log_info(log_msg->str);
g_string_free(log_msg, TRUE);

View File

@@ -15,10 +15,10 @@
* functional tests run less frequently than unit tests.
*
* Tests are organized into groups for better maintainability and parallel execution:
* Group 1: Connect, Ping, Rooms, Software, Last Activity (19 tests)
* Group 2: Message, Receipts, Roster, DB History (22 tests)
* Group 3: Chat Session, Presence, Disconnect (22 tests)
* Group 4: MUC, Carbons (21 tests)
* Group 1: Connect, Ping, Rooms, Software
* Group 2: Message, Receipts, Roster, Chat Session
* Group 3: Presence, Disconnect
* Group 4: MUC, Carbons
*
* Parallel execution:
* ./functionaltests - run all tests sequentially
@@ -52,8 +52,6 @@
#include "test_software.h"
#include "test_muc.h"
#include "test_disconnect.h"
#include "test_lastactivity.h"
#include "test_history.h"
/* Macro to wrap each test with setup/teardown functions */
#define PROF_FUNC_TEST(test) cmocka_unit_test_setup_teardown(test, init_prof_test, close_prof_test)
@@ -78,9 +76,8 @@ main(int argc, char* argv[])
fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group);
/* ============================================================
* GROUP 1: Connect, Ping, Rooms, Software, Last Activity
* GROUP 1: Connect, Ping, Rooms, Software
* Basic XMPP session establishment and server queries
* (19 tests)
* ============================================================ */
const struct CMUnitTest group1_tests[] = {
/* Connection tests - verify login, roster, bookmarks */
@@ -107,16 +104,11 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
/* Last Activity - XEP-0012 */
PROF_FUNC_TEST(responds_to_last_activity_request),
PROF_FUNC_TEST(last_activity_request_to_contact),
};
/* ============================================================
* GROUP 2: Message, Receipts, Roster, DB History
* GROUP 2: Message, Receipts, Roster, Chat Session
* Core messaging and contact management
* (23 tests)
* ============================================================ */
const struct CMUnitTest group2_tests[] = {
/* Basic message send/receive */
@@ -136,29 +128,6 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
/* Database persistence - message write+read round-trip */
PROF_FUNC_TEST(message_db_history_on_reopen),
PROF_FUNC_TEST(message_db_history_multiple),
PROF_FUNC_TEST(message_db_history_contact_isolation),
PROF_FUNC_TEST(message_db_history_special_chars),
PROF_FUNC_TEST(message_db_history_outgoing),
PROF_FUNC_TEST(message_db_history_dialog),
PROF_FUNC_TEST(message_db_history_empty),
PROF_FUNC_TEST(message_db_history_long_message),
PROF_FUNC_TEST(message_db_history_newline),
PROF_FUNC_TEST(message_db_history_service_chars),
#ifdef HAVE_HISTORY_VERIFY
PROF_FUNC_TEST(message_db_history_verify),
#endif
PROF_FUNC_TEST(message_db_history_lmc),
};
/* ============================================================
* GROUP 3: Chat Session, Presence, Disconnect
* Session routing, status management, clean teardown
* (22 tests)
* ============================================================ */
const struct CMUnitTest group3_tests[] = {
/* Chat session management - bare/full JID routing */
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
@@ -166,8 +135,13 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
};
/* Presence - online/away/xa/dnd/chat status management */
/* ============================================================
* GROUP 3: Presence, Disconnect
* Online/away/xa/dnd/chat status management
* ============================================================ */
const struct CMUnitTest group3_tests[] = {
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_away),
@@ -191,7 +165,6 @@ main(int argc, char* argv[])
/* ============================================================
* GROUP 4: MUC, Carbons
* Multi-user chat and message synchronization
* (21 tests)
* ============================================================ */
const struct CMUnitTest group4_tests[] = {
/* MUC room join with various options - XEP-0045 */
@@ -215,10 +188,6 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
/* MUC moderation - XEP-0045 room admin */
PROF_FUNC_TEST(sends_affiliation_list_request),
PROF_FUNC_TEST(sends_kick_request),
/* Message Carbons - XEP-0280 (message sync across devices) */
PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled),
@@ -234,9 +203,9 @@ main(int argc, char* argv[])
const struct CMUnitTest* tests;
size_t count;
} groups[] = {
{ "Group 1: Connect/Ping/Rooms/Software/LastActivity", group1_tests, ARRAY_SIZE(group1_tests) },
{ "Group 2: Message/Receipts/Roster/DBHistory", group2_tests, ARRAY_SIZE(group2_tests) },
{ "Group 3: Session/Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
{ "Group 1: Connect/Ping/Rooms/Software", group1_tests, ARRAY_SIZE(group1_tests) },
{ "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) },
{ "Group 3: Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
{ "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) },
};
const int num_groups = ARRAY_SIZE(groups);

View File

@@ -1,475 +0,0 @@
/*
* test_history.c
*
* Functional test for database message persistence.
* Verifies that a received chat message is written to the database
* and loaded back as history when the chat window is reopened.
*
* These tests only exercise the public log_database API through normal
* profanity UI operations, so they work with any storage backend.
*/
#include <glib.h>
#include <stdio.h>
#include <string.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include "proftest.h"
/*
* Test: message written to DB is loaded as history on chat window reopen.
*
* Flow:
* 1. Connect and enable /history on (enables PREF_CHLOG + PREF_HISTORY)
* 2. Receive a chat message while on the console window — the message
* body is NOT rendered to the terminal (only a console notification
* "<< chat message: ... (win N)" appears), but the message IS
* written to the database via chat_log_msg_in()
* 3. Close the auto-created chat window (destroying its in-memory buffer)
* 4. Reopen the chat window with /msg — chatwin_new() calls
* _chatwin_history() which reads from log_database_get_previous_chat()
* 5. Assert the message body appears in the terminal output, proving
* it was persisted to and read back from the database
*/
void
message_db_history_on_reopen(void **state)
{
prof_connect();
/* Enable chat logging and history display */
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Receive a message while on the console window.
* The body text is NOT printed to the visible terminal — only the
* console notification appears. The message is logged to the DB. */
stbbr_send(
"<message id='hist-db-test-1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>persistence-roundtrip-check-42</body>"
"</message>");
/* Wait for the console notification (proves message was received).
* buddy1@localhost is in the roster as "Buddy1", and PREF_RESOURCE_MESSAGE
* is on by default, so the display name is "Buddy1/phone". */
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
/* Close the chat window that was auto-created for the incoming message.
* This destroys its in-memory buffer so the text is gone from UI. */
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
/* Reopen the chat window — chatwin_new() will load history from DB.
* The message body should now appear on screen for the first time. */
prof_input("/msg buddy1@localhost");
/* The history-loaded message must contain the body we sent earlier.
* Since the body was never rendered to the terminal before (it was
* only in the hidden window's ncurses buffer which was destroyed),
* finding it now proves the database write+read round-trip works. */
assert_true(prof_output_exact("persistence-roundtrip-check-42"));
}
/*
* Test: multiple messages from the same contact are all preserved.
*
* Uses different resources so each console notification is unique and
* we can reliably synchronise on each delivery before proceeding.
*/
void
message_db_history_multiple(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Three messages from different resources → distinct notifications */
stbbr_send(
"<message id='multi1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>db-multi-first-aaa</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
stbbr_send(
"<message id='multi2' to='stabber@localhost' "
"from='buddy1@localhost/laptop' type='chat'>"
"<body>db-multi-second-bbb</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/laptop (win 2)"));
stbbr_send(
"<message id='multi3' to='stabber@localhost' "
"from='buddy1@localhost/tablet' type='chat'>"
"<body>db-multi-third-ccc</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/tablet (win 2)"));
/* Close and reopen */
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
prof_input("/msg buddy1@localhost");
/* All three must be present in history */
assert_true(prof_output_exact("db-multi-first-aaa"));
assert_true(prof_output_exact("db-multi-second-bbb"));
assert_true(prof_output_exact("db-multi-third-ccc"));
}
/*
* Test: messages are stored per-contact — buddy1's history does not
* leak into buddy2's window.
*
* 1. Receive from buddy1 and buddy2 while on console (bodies hidden)
* 2. Close all chat windows
* 3. Open buddy1 → verify buddy1's body present, buddy2's body absent
*/
void
message_db_history_contact_isolation(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Receive from buddy1 */
stbbr_send(
"<message id='iso1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>isolation-buddy1-only-xyzzy</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
/* Receive from buddy2 */
stbbr_send(
"<message id='iso2' to='stabber@localhost' "
"from='buddy2@localhost/phone' type='chat'>"
"<body>isolation-buddy2-only-plugh</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)"));
/* Close all chat windows at once */
prof_input("/close all");
assert_true(prof_output_exact("Closed 2 windows."));
/* Open buddy1 — history should contain ONLY buddy1's message */
prof_input("/msg buddy1@localhost");
assert_true(prof_output_exact("isolation-buddy1-only-xyzzy"));
/* buddy2's body was never printed to the terminal (received on
* console), so finding it now would mean cross-contact leakage */
prof_timeout(3);
assert_false(prof_output_exact("isolation-buddy2-only-plugh"));
prof_timeout_reset();
}
/*
* Test: special characters survive the DB write+read round-trip.
*
* The XMPP body uses XML entities (&amp; &lt; &gt; &quot;) which the
* XML parser decodes before storage. The DB must preserve the decoded
* characters and return them unchanged.
*/
void
message_db_history_special_chars(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
stbbr_send(
"<message id='spec1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>chars: &amp; &lt;tag&gt; &quot;quoted&quot;</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
prof_input("/msg buddy1@localhost");
/* The decoded text must appear exactly as-is */
assert_true(prof_output_exact("chars: & <tag> \"quoted\""));
}
/*
* Test: outgoing message (sent via /msg) persists in DB.
*
* NOTE: The outgoing body IS rendered to the terminal when sent. Since
* prof_output_exact searches the cumulative buffer, the assertion alone
* does not conclusively prove DB persistence. However, combined with
* the incoming tests, this verifies the end-to-end outgoing write path
* and catches crashes in the outgoing DB code.
*/
void
message_db_history_outgoing(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Send outgoing — opens and focuses chat window */
prof_input("/msg buddy1@localhost db-outgoing-sent-42");
assert_true(prof_output_regex("me: .+db-outgoing-sent-42"));
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
/* Reopen — history should load the outgoing message */
prof_input("/msg buddy1@localhost");
assert_true(prof_output_exact("db-outgoing-sent-42"));
}
/*
* Test: a dialog (outgoing + incoming) is fully preserved in history.
*
* Sends an outgoing message, then closes the window. A reply arrives
* while on the console (body never rendered). After reopen, both sides
* of the conversation appear — proving the incoming read from DB works
* and the outgoing write path completed without error.
*/
void
message_db_history_dialog(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Phase 1: send outgoing (renders to terminal) */
prof_input("/msg buddy1@localhost dialog-out-msg-42");
assert_true(prof_output_regex("me: .+dialog-out-msg-42"));
/* Return to console */
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
/* Phase 2: receive reply on console (body NOT in terminal buffer) */
stbbr_send(
"<message id='dlg-in' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>dialog-in-reply-77</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
/* Close the auto-created window */
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
/* Phase 3: reopen — history should have both directions */
prof_input("/msg buddy1@localhost");
/* Incoming body conclusively proves DB round-trip */
assert_true(prof_output_exact("dialog-in-reply-77"));
/* Outgoing body is consistent with DB persistence */
assert_true(prof_output_exact("dialog-out-msg-42"));
}
/*
* Test: opening a chat window for a contact with no prior messages
* does not crash and the window is usable.
*/
void
message_db_history_empty(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* buddy2 has never exchanged messages — history is empty */
prof_input("/msg buddy2@localhost");
assert_true(prof_output_exact("buddy2@localhost"));
/* Verify the window is functional by sending a message */
prof_input("empty-history-smoke-test");
assert_true(prof_output_regex("me: .+empty-history-smoke-test"));
}
/*
* Test: a long message (1000+ characters) is not truncated by the DB.
*/
void
message_db_history_long_message(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Build a 1020-character body: 1000 x 'A' + unique suffix */
char body[1100];
memset(body, 'A', 1000);
strcpy(body + 1000, "-long-db-end-MARKER");
char xml[1500];
snprintf(xml, sizeof(xml),
"<message id='long1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>%s</body>"
"</message>", body);
stbbr_send(xml);
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
prof_input("/msg buddy1@localhost");
assert_true(prof_output_exact("-long-db-end-MARKER"));
}
/*
* Test: message body containing embedded newlines (LF) is stored and
* loaded correctly.
*
* Uses XML character reference &#10; for literal newline in body.
*/
void
message_db_history_newline(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
stbbr_send(
"<message id='nl1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>nl-first-line-7k&#10;nl-second-line-9m&#10;nl-third-line-2p</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
prof_input("/msg buddy1@localhost");
/* Each fragment must survive the round-trip */
assert_true(prof_output_exact("nl-first-line-7k"));
assert_true(prof_output_exact("nl-second-line-9m"));
assert_true(prof_output_exact("nl-third-line-2p"));
}
/*
* Test: characters that could break storage format (backslash, pipe,
* percent, braces, brackets, equals) survive DB round-trip.
*/
void
message_db_history_service_chars(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
stbbr_send(
"<message id='svc1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>svc: a\\b c|d e%f {g} [h] i=j</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
prof_input("/close 2");
assert_true(prof_output_exact("Closed window 2"));
prof_input("/msg buddy1@localhost");
assert_true(prof_output_exact("svc: a\\b c|d e%f {g} [h] i=j"));
}
#ifdef HAVE_HISTORY_VERIFY
/*
* Test: /history verify reports no issues after normal message writes.
*/
void
message_db_history_verify(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Write a few messages to create non-trivial DB state */
stbbr_send(
"<message id='ver1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>verify-msg-one</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
stbbr_send(
"<message id='ver2' to='stabber@localhost' "
"from='buddy2@localhost/phone' type='chat'>"
"<body>verify-msg-two</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)"));
/* Run verify — should find no issues */
prof_input("/history verify");
assert_true(prof_output_exact("Verification complete: no issues found."));
}
#endif /* HAVE_HISTORY_VERIFY */
/*
* Test: LMC (Last Message Correction, XEP-0308) — incoming correction
* replaces the original in DB history.
*
* Both messages arrive while focused on the console so their bodies
* are never rendered to the terminal. After reopen, only the corrected
* text should appear — the original should be absent.
*/
void
message_db_history_lmc(void **state)
{
prof_connect();
prof_input("/history on");
assert_true(prof_output_regex("Chat history enabled\\."));
/* Original message (body never displayed — received on console) */
stbbr_send(
"<message id='lmc-orig-1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>lmc-before-correction-aaa</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy1/phone (win 2)"));
/* Correction replacing the original (XEP-0308).
* Use buddy2 message as sync barrier to ensure correction is processed. */
stbbr_send(
"<message id='lmc-corr-1' to='stabber@localhost' "
"from='buddy1@localhost/phone' type='chat'>"
"<body>lmc-after-correction-zzz</body>"
"<replace id='lmc-orig-1' xmlns='urn:xmpp:message-correct:0'/>"
"</message>");
/* Sync barrier: send from buddy2, wait for its notification */
stbbr_send(
"<message id='lmc-sync' to='stabber@localhost' "
"from='buddy2@localhost/phone' type='chat'>"
"<body>lmc-sync-barrier</body>"
"</message>");
assert_true(prof_output_exact("<< chat message: Buddy2/phone (win 3)"));
/* Close buddy1 and buddy2 windows */
prof_input("/close all");
assert_true(prof_output_exact("Closed 2 windows."));
/* Reopen buddy1 — should have corrected text only */
prof_input("/msg buddy1@localhost");
assert_true(prof_output_exact("lmc-after-correction-zzz"));
/* Original body was never in the terminal buffer (received on console).
* Its absence in history proves the correction was applied in the DB. */
prof_timeout(3);
assert_false(prof_output_exact("lmc-before-correction-aaa"));
prof_timeout_reset();
}

View File

@@ -1,14 +0,0 @@
void message_db_history_on_reopen(void **state);
void message_db_history_multiple(void **state);
void message_db_history_contact_isolation(void **state);
void message_db_history_special_chars(void **state);
void message_db_history_outgoing(void **state);
void message_db_history_dialog(void **state);
void message_db_history_empty(void **state);
void message_db_history_long_message(void **state);
void message_db_history_newline(void **state);
void message_db_history_service_chars(void **state);
#ifdef HAVE_HISTORY_VERIFY
void message_db_history_verify(void **state);
#endif
void message_db_history_lmc(void **state);

View File

@@ -1,63 +0,0 @@
/*
* test_lastactivity.c
* Functional tests for Last Activity (XEP-0012)
*/
#include <glib.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include "proftest.h"
void
responds_to_last_activity_request(void **state)
{
prof_connect();
// Send incoming last activity request
stbbr_send(
"<iq id='last1' type='get' to='stabber@localhost/profanity' from='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
);
// Verify that CProof responds with last activity info
// The 'seconds' attribute indicates idle time
assert_true(stbbr_received(
"<iq id='last1' type='result' to='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last' seconds='*'/>"
"</iq>"
));
}
void
last_activity_request_to_contact(void **state)
{
prof_connect();
stbbr_send(
"<presence to='stabber@localhost' from='buddy1@localhost/mobile'>"
"<priority>10</priority>"
"<status>I'm here</status>"
"</presence>"
);
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
// Register response for last activity query
stbbr_for_query("jabber:iq:last",
"<iq id='*' type='result' from='buddy1@localhost/mobile' to='stabber@localhost/profanity'>"
"<query xmlns='jabber:iq:last' seconds='120'/>"
"</iq>"
);
prof_input("/lastactivity get buddy1@localhost/mobile");
// Verify the request was sent
assert_true(stbbr_received(
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
));
}

View File

@@ -1,7 +0,0 @@
/*
* test_lastactivity.h
* Header for Last Activity tests (XEP-0012)
*/
void responds_to_last_activity_request(void **state);
void last_activity_request_to_contact(void **state);

View File

@@ -393,83 +393,3 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
assert_false(prof_output_regex("testroom@conference\\.localhost \\(win 2\\)"));
prof_timeout_reset();
}
void
sends_affiliation_list_request(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='owner'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: owner"));
prof_input("/affiliation owner list");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item affiliation='owner'/>"
"</query>"
"</iq>"
));
}
void
sends_kick_request(void **state)
{
prof_connect();
// Enable MUC presence messages to see occupant join/leave
prof_input("/presence room all");
assert_true(prof_output_regex("All presence updates will appear"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='admin'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: admin"));
// Simulate another user in the room
stbbr_send(
"<presence to='stabber@localhost/profanity' from='testroom@conference.localhost/baduser'>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='baduser@localhost/phone' affiliation='none'/>"
"</x>"
"</presence>"
);
sleep(1);
assert_true(prof_output_regex("baduser has joined"));
// Register success response for kick
stbbr_for_query("http://jabber.org/protocol/muc#admin",
"<iq id='*' type='result' from='testroom@conference.localhost'/>"
);
prof_input("/kick baduser \"spamming\"");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='set'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item nick='baduser' role='none'>"
"<reason>spamming</reason>"
"</item>"
"</query>"
"</iq>"
));
}

View File

@@ -12,5 +12,3 @@ void shows_me_message_from_self(void **state);
void shows_all_messages_in_console_when_window_not_focussed(void **state);
void shows_first_message_in_console_when_window_not_focussed(void **state);
void shows_no_message_in_console_when_window_not_focussed(void **state);
void sends_affiliation_list_request(void **state);
void sends_kick_request(void **state);