mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-24 03:06:21 +00:00
merge: sync upstream profanity-im/profanity
Sync with upstream profanity-im/profanity.
Major upstream changes incorporated:
Memory management
- Replace malloc+memset with g_new0 throughout codebase
- Adopt auto_gchar / auto_gcharv / auto_gerror / auto_jid cleanup macros
- Replace free() with g_free() for GAlloc'd memory
Editor rewrite
- Remove pthread-based async editor; use GChildWatch callback API
- New launch_editor(initial_content, callback, user_data) interface
- Proper signal handling (SIGINT, SIGTSTP, SIGPIPE reset in child)
- ui_suspend()/ui_resume() integration for TTY management
OMEMO improvements
- Dual backend support: libsignal-protocol-c and libomemo-c
- Proper pre-key removal after use (XEP-0384 compliance)
- Automatic pre-key regeneration when store drops below threshold
- New functions: omemo_is_device_active(), omemo_is_jid_trusted()
- omemo_get_jid_untrusted_fingerprints() for better error messages
- Fingerprint notifications on new device identity discovery
- Deterministic pre-key ID generation tracking max_pre_key_id
- omemo_trust_changed() UI updates on trust state changes
JID validation
- RFC 6122-compliant validation in jid_is_valid()
- Character-level checks (RFC 6122 forbidden chars: & ' / : < > @)
- Length limits: 1023 per component, 3071 total
- New jid_is_valid_user_jid() for user vs. service JID distinction
Database
- Schema migration v3: UNIQUE constraint on archive_id for deduplication
- Triggers for corrected message tracking (replaces_db_id / replaced_by_db_id)
- db_history_result_t return type, _truncate_datetime_suffix()
UI / console
- win_warn_needed() / win_warn_sent() warning deduplication hash table
- PAD_MIN_HEIGHT dynamic pad sizing with PAD_THRESHOLD auto-cleanup
- Spellcheck integration in input field with Unicode word detection
- cons_spellcheck_setting() for /settings ui output
- /[command]? shortcut for command help
Account config
- Account name sanitization for GKeyFile special chars ([ ] = # \n \r)
- Replace popen() with g_spawn_sync() for eval_password
- TLS policy: add "direct" option alongside legacy
Connection
- Port validation with g_assert (0–65535)
- SHA-256 certificate fingerprint support (XMPP_CERT_PUBKEY_FINGERPRINT_SHA256)
- "direct" TLS policy alias for legacy SSL
Common utilities
- str_xml_sanitize() for XML 1.0 illegal character removal
- string_matches_one_of() with formatted error messages
- valid_tls_policy_option() helper
- prof_date_time_format_iso8601() utility
- Improved strip_arg_quotes() with backslash unescaping
- prof_occurrences() uses g_slist_prepend + reverse for performance
PGP / OX
- Proper GPGME resource cleanup with goto-cleanup pattern
- g_string_free(xmppuri) leak fix in _ox_key_lookup
CSV export
- Use GString + g_file_set_contents instead of raw write() syscalls
Tests
- Restructured into subdirectories: command/, config/, xmpp/, ui/, omemo/, otr/, pgp/
- New test_cmd_ac.c for autocompleter unit tests
- Updated stubs for new UI suspend/resume functions
License headers
- Migrate to SPDX-3.0 identifiers (GPL-3.0-or-later WITH OpenSSL-exception)
────────────────────────────────────────────────────
cproof-specific preservations:
- XEP-0308 LMC: replace_id ?: id logic in message/stanza/omemo
- Force encryption: cmd_force_encryption, test_forced_encryption
- CWE-134: format string protection (cons_show("%s", ...))
- y_start_pos-based paging in window.c
- db_history_result_t return type, _truncate_datetime_suffix()
Merge-time fixes:
- common.c: format-security (-Werror) — cons_show(errmsg) → cons_show("%s", errmsg)
- database.c: null-deref guard — !msg->timestamp → msg && !msg->timestamp
- console.c: implicit size_t → int cast — (int)(maxlen + 1)
- tlscerts.c: %d for size_t — %zu
Build system:
- Kept autotools (Makefile.am, configure.ac); upstream uses Meson
- Restored deleted files: bootstrap.sh, autogen.sh, ax_valgrind_check.m4, configure-debug
- Updated Makefile.am test paths for subdirectory structure
- Added test_cmd_ac, test_forced_encryption to test sources
Functional tests:
- Use cproof version; upstream requires stbbr_for_xmlns from updated stabber
- Not yet available in devs/stabber fork
Closes #64
Merge author: jabber.developer2
Commits authors:
Michael Vetter <jubalh@iodoru.org>
& Steffen Jaeckel <s@jaeckel.eu>
This commit is contained in:
256
src/common.c
256
src/common.c
@@ -3,35 +3,9 @@
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
||||
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link the code of portions of this program with the OpenSSL library under
|
||||
* certain conditions as described in each individual source file, and
|
||||
* distribute linked combinations including the two.
|
||||
*
|
||||
* You must obey the GNU General Public License in all respects for all of the
|
||||
* code used other than OpenSSL. If you modify file(s) with this exception, you
|
||||
* may extend this exception to your version of the file(s), but you are not
|
||||
* obligated to do so. If you do not wish to do so, delete this exception
|
||||
* statement from your version. If you delete this exception statement from all
|
||||
* source files in the program, then also delete it here.
|
||||
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@@ -64,6 +38,7 @@
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config/files.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
#ifdef HAVE_GIT_VERSION
|
||||
#include "gitversion.h"
|
||||
@@ -161,20 +136,27 @@ auto_close_FILE(FILE** fd)
|
||||
log_error("%s", g_strerror(errno));
|
||||
}
|
||||
|
||||
void
|
||||
auto_free_gerror(GError** err)
|
||||
{
|
||||
if (err == NULL)
|
||||
return;
|
||||
|
||||
PROF_GERROR_FREE(*err);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_load_keyfile(prof_keyfile_t* keyfile)
|
||||
{
|
||||
GError* error = NULL;
|
||||
auto_gerror GError* error = NULL;
|
||||
keyfile->keyfile = g_key_file_new();
|
||||
|
||||
if (g_key_file_load_from_file(keyfile->keyfile, keyfile->filename, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
|
||||
return TRUE;
|
||||
} else if (error->code != G_FILE_ERROR_NOENT) {
|
||||
} else if (error && error->code != G_FILE_ERROR_NOENT) {
|
||||
log_warning("[Keyfile] error loading %s: %s", keyfile->filename, error->message);
|
||||
g_error_free(error);
|
||||
} else {
|
||||
log_warning("[Keyfile] no such file: %s", keyfile->filename);
|
||||
g_error_free(error);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
@@ -212,10 +194,9 @@ load_custom_keyfile(prof_keyfile_t* keyfile, gchar* filename)
|
||||
gboolean
|
||||
save_keyfile(prof_keyfile_t* keyfile)
|
||||
{
|
||||
GError* error = NULL;
|
||||
auto_gerror GError* error = NULL;
|
||||
if (!g_key_file_save_to_file(keyfile->keyfile, keyfile->filename, &error)) {
|
||||
log_error("[Keyfile]: saving file %s failed! %s", keyfile->filename, error->message);
|
||||
g_error_free(error);
|
||||
log_error("[Keyfile]: saving file %s failed! %s", STR_MAYBE_NULL(keyfile->filename), PROF_GERROR_MESSAGE(error));
|
||||
return FALSE;
|
||||
}
|
||||
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
|
||||
@@ -254,11 +235,8 @@ copy_file(const char* const sourcepath, const char* const targetpath, const gboo
|
||||
{
|
||||
GFile* source = g_file_new_for_path(sourcepath);
|
||||
GFile* dest = g_file_new_for_path(targetpath);
|
||||
GError* error = NULL;
|
||||
GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
|
||||
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, &error);
|
||||
if (error != NULL)
|
||||
g_error_free(error);
|
||||
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, NULL);
|
||||
g_object_unref(source);
|
||||
g_object_unref(dest);
|
||||
return success;
|
||||
@@ -335,29 +313,89 @@ gboolean
|
||||
strtoi_range(const char* str, int* saveptr, int min, int max, gchar** err_msg)
|
||||
{
|
||||
char* ptr;
|
||||
int val;
|
||||
long lval;
|
||||
if (str == NULL) {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("'str' input pointer can not be NULL");
|
||||
return FALSE;
|
||||
}
|
||||
errno = 0;
|
||||
val = (int)strtol(str, &ptr, 0);
|
||||
lval = strtol(str, &ptr, 0);
|
||||
if (errno != 0 || *str == '\0' || *ptr != '\0') {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("Could not convert \"%s\" to a number.", str);
|
||||
return FALSE;
|
||||
} else if (val < min || val > max) {
|
||||
} else if (lval < (long)min || lval > (long)max) {
|
||||
if (err_msg)
|
||||
*err_msg = g_strdup_printf("Value %s out of range. Must be in %d..%d.", str, min, max);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*saveptr = val;
|
||||
*saveptr = (int)lval;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...)
|
||||
{
|
||||
gboolean ret = FALSE;
|
||||
va_list ap;
|
||||
const char* cur = first;
|
||||
if (!is)
|
||||
return is_can_be_null;
|
||||
|
||||
va_start(ap, first);
|
||||
while (cur != NULL) {
|
||||
if (g_strcmp0(is, cur) == 0) {
|
||||
ret = TRUE;
|
||||
break;
|
||||
}
|
||||
cur = va_arg(ap, const char*);
|
||||
}
|
||||
va_end(ap);
|
||||
if (!ret && what) {
|
||||
cons_show("Invalid %s: '%s'", what, is);
|
||||
char errmsg[256] = { 0 };
|
||||
size_t sz = 0;
|
||||
int s = snprintf(errmsg, sizeof(errmsg) - sz, "%s must be one of:", what);
|
||||
if (s < 0 || s + sz >= sizeof(errmsg))
|
||||
return ret;
|
||||
sz += s;
|
||||
|
||||
cur = first;
|
||||
va_start(ap, first);
|
||||
while (cur != NULL) {
|
||||
const char* next = va_arg(ap, const char*);
|
||||
if (next) {
|
||||
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " '%s',", cur);
|
||||
} else {
|
||||
/* remove last ',' */
|
||||
sz--;
|
||||
errmsg[sz] = '\0';
|
||||
s = snprintf(errmsg + sz, sizeof(errmsg) - sz, " or '%s'.", cur);
|
||||
}
|
||||
if (s < 0 || s + sz >= sizeof(errmsg)) {
|
||||
log_debug("Error message too long or some other error occurred (%d).", s);
|
||||
s = -1;
|
||||
break;
|
||||
}
|
||||
sz += s;
|
||||
cur = next;
|
||||
}
|
||||
va_end(ap);
|
||||
if (s > 0)
|
||||
cons_show("%s", errmsg);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
gboolean
|
||||
valid_tls_policy_option(const char* is)
|
||||
{
|
||||
return string_matches_one_of("TLS policy", is, TRUE, "force", "allow", "trust", "disable", "legacy", "direct", NULL);
|
||||
}
|
||||
|
||||
int
|
||||
utf8_display_len(const char* const str)
|
||||
{
|
||||
@@ -380,6 +418,33 @@ utf8_display_len(const char* const str)
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes illegal XML 1.0 characters from a string.
|
||||
*
|
||||
* This function creates a new string that excludes characters in the range
|
||||
* U+0000 to U+001F, except for U+0009 (TAB), U+000A (LF), and U+000D (CR).
|
||||
*/
|
||||
gchar*
|
||||
str_xml_sanitize(const char* const str)
|
||||
{
|
||||
if (str == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GString* sanitized = g_string_new_len(NULL, strlen(str));
|
||||
const char* curr = str;
|
||||
|
||||
while (*curr != '\0') {
|
||||
gunichar c = g_utf8_get_char(curr);
|
||||
if ((c >= 0x20) || (c == 0x09) || (c == 0x0A) || (c == 0x0D)) {
|
||||
g_string_append_unichar(sanitized, c);
|
||||
}
|
||||
curr = g_utf8_next_char(curr);
|
||||
}
|
||||
|
||||
return g_string_free(sanitized, FALSE);
|
||||
}
|
||||
|
||||
char*
|
||||
release_get_latest(void)
|
||||
{
|
||||
@@ -407,11 +472,15 @@ release_get_latest(void)
|
||||
}
|
||||
|
||||
gboolean
|
||||
release_is_new(char* found_version)
|
||||
release_is_new(const char* const curr_version, const char* const found_version)
|
||||
{
|
||||
if (!curr_version || !found_version) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;
|
||||
|
||||
int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min,
|
||||
int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min,
|
||||
&curr_patch);
|
||||
int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
|
||||
&found_patch);
|
||||
@@ -474,23 +543,32 @@ _get_file_or_linked(gchar* loc)
|
||||
char*
|
||||
strip_arg_quotes(const char* const input)
|
||||
{
|
||||
char* unquoted = strdup(input);
|
||||
if (input == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Remove starting quote if it exists
|
||||
if (strchr(unquoted, '"')) {
|
||||
if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) {
|
||||
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
|
||||
// Unescape and strip quotes
|
||||
GString* unescaped = g_string_new("");
|
||||
for (const char* p = input; *p; p++) {
|
||||
if (*p == '\\' && (*(p + 1) != '\0')) {
|
||||
p++;
|
||||
g_string_append_c(unescaped, *p);
|
||||
} else if (*p == '"') {
|
||||
// Only strip if it's the first char or preceded by a space
|
||||
if (p == input || *(p - 1) == ' ') {
|
||||
continue;
|
||||
}
|
||||
// Or if it's the last char
|
||||
if (*(p + 1) == '\0') {
|
||||
continue;
|
||||
}
|
||||
g_string_append_c(unescaped, *p);
|
||||
} else {
|
||||
g_string_append_c(unescaped, *p);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ending quote if it exists
|
||||
if (strchr(unquoted, '"')) {
|
||||
if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) {
|
||||
memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
|
||||
}
|
||||
}
|
||||
|
||||
return unquoted;
|
||||
return g_string_free(unescaped, FALSE);
|
||||
}
|
||||
|
||||
gboolean
|
||||
@@ -514,36 +592,48 @@ is_notify_enabled(void)
|
||||
GSList*
|
||||
prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
|
||||
{
|
||||
if (needle == NULL || haystack == NULL) {
|
||||
if (needle == NULL || haystack == NULL || *needle == '\0') {
|
||||
return *result;
|
||||
}
|
||||
|
||||
do {
|
||||
gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset);
|
||||
if (g_str_has_prefix(haystack_curr, needle)) {
|
||||
size_t needle_len = strlen(needle);
|
||||
gchar* p = g_utf8_offset_to_pointer(haystack, offset);
|
||||
GSList* matches = NULL;
|
||||
|
||||
while (p) {
|
||||
if (g_str_has_prefix(p, needle)) {
|
||||
if (whole_word) {
|
||||
gunichar before = 0;
|
||||
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr);
|
||||
gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, p);
|
||||
if (haystack_before_ch) {
|
||||
before = g_utf8_get_char(haystack_before_ch);
|
||||
}
|
||||
|
||||
gunichar after = 0;
|
||||
gchar* haystack_after_ch = haystack_curr + strlen(needle);
|
||||
if (haystack_after_ch[0] != '\0') {
|
||||
gchar* haystack_after_ch = p + needle_len;
|
||||
if (*haystack_after_ch != '\0') {
|
||||
after = g_utf8_get_char(haystack_after_ch);
|
||||
}
|
||||
|
||||
if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) {
|
||||
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
|
||||
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
||||
}
|
||||
} else {
|
||||
*result = g_slist_append(*result, GINT_TO_POINTER(offset));
|
||||
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
||||
}
|
||||
}
|
||||
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
}
|
||||
|
||||
p = g_utf8_next_char(p);
|
||||
offset++;
|
||||
} while (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
*result = g_slist_concat(*result, g_slist_reverse(matches));
|
||||
}
|
||||
|
||||
return *result;
|
||||
}
|
||||
@@ -599,15 +689,15 @@ get_file_paths_recursive(const char* path, GSList** contents)
|
||||
}
|
||||
}
|
||||
|
||||
char*
|
||||
gchar*
|
||||
get_random_string(int length)
|
||||
{
|
||||
GRand* prng;
|
||||
char* rand;
|
||||
char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
gchar* rand;
|
||||
gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
int endrange = sizeof(alphabet) - 1;
|
||||
|
||||
rand = calloc(length + 1, sizeof(char));
|
||||
rand = g_malloc0(length + 1);
|
||||
|
||||
prng = g_rand_new();
|
||||
|
||||
@@ -634,7 +724,7 @@ get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const mes
|
||||
gboolean
|
||||
call_external(gchar** argv)
|
||||
{
|
||||
GError* spawn_error;
|
||||
auto_gerror GError* spawn_error = NULL;
|
||||
gboolean is_successful;
|
||||
|
||||
GSpawnFlags flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL;
|
||||
@@ -647,9 +737,7 @@ call_external(gchar** argv)
|
||||
&spawn_error);
|
||||
if (!is_successful) {
|
||||
auto_gchar gchar* cmd = g_strjoinv(" ", argv);
|
||||
log_error("Spawning '%s' failed with error '%s'", cmd, spawn_error ? spawn_error->message : "Unknown, spawn_error is NULL");
|
||||
|
||||
g_error_free(spawn_error);
|
||||
log_error("Spawning '%s' failed with error '%s'", cmd, PROF_GERROR_MESSAGE(spawn_error));
|
||||
}
|
||||
|
||||
return is_successful;
|
||||
@@ -711,7 +799,7 @@ _unique_filename(const char* filename)
|
||||
return unique;
|
||||
}
|
||||
|
||||
static bool
|
||||
static gboolean
|
||||
_has_directory_suffix(const char* path)
|
||||
{
|
||||
return (g_str_has_suffix(path, ".")
|
||||
@@ -809,6 +897,20 @@ unique_filename_from_url(const char* url, const char* path)
|
||||
return unique_filename;
|
||||
}
|
||||
|
||||
/* This returns a timestamp formatted in ISO8601 format.
|
||||
* Either it returns the current time if `dt` is NULL, or
|
||||
* it returns the formatted time value passed in `dt`.
|
||||
*/
|
||||
gchar*
|
||||
prof_date_time_format_iso8601(GDateTime* dt)
|
||||
{
|
||||
GDateTime* dt_ = (dt == NULL) ? g_date_time_new_now_local() : dt;
|
||||
gchar* ret = g_date_time_format_iso8601(dt_);
|
||||
if (dt == NULL)
|
||||
g_date_time_unref(dt_);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* build profanity version string.
|
||||
* example: 0.13.1dev.master.69d8c1f9
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user