Filter out control characters (U+0000 to U+001F) from outgoing messages, as they are illegal in XML 1.0 (except for \t, \n, and \r). This prevents XMPP servers from closing the connection when such characters are accidentally or intentionally included in a message. Fixes: https://github.com/profanity-im/profanity/issues/1437
924 lines
24 KiB
C
924 lines
24 KiB
C
/*
|
|
* common.c
|
|
* vim: expandtab:ts=4:sts=4:sw=4
|
|
*
|
|
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
|
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <errno.h>
|
|
#include <sys/select.h>
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <curl/curl.h>
|
|
#include <curl/easy.h>
|
|
#include <glib.h>
|
|
#include <gio/gio.h>
|
|
#include <glib/gstdio.h>
|
|
|
|
#ifdef HAVE_NCURSESW_NCURSES_H
|
|
#include <ncursesw/ncurses.h>
|
|
#elif HAVE_NCURSES_H
|
|
#include <ncurses.h>
|
|
#elif HAVE_CURSES_H
|
|
#include <curses.h>
|
|
#endif
|
|
|
|
#include "log.h"
|
|
#include "common.h"
|
|
#include "config/files.h"
|
|
#include "ui/ui.h"
|
|
|
|
#ifdef HAVE_GIT_VERSION
|
|
#include "gitversion.h"
|
|
#endif
|
|
|
|
struct curl_data_t
|
|
{
|
|
char* buffer;
|
|
size_t size;
|
|
};
|
|
|
|
static size_t _data_callback(void* ptr, size_t size, size_t nmemb, void* data);
|
|
static gchar* _get_file_or_linked(gchar* loc);
|
|
|
|
/**
|
|
* Frees the memory allocated for a gchar* string.
|
|
*
|
|
* @param str Pointer to the gchar* string to be freed. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_free_gchar(gchar** str)
|
|
{
|
|
if (str == NULL)
|
|
return;
|
|
g_free(*str);
|
|
}
|
|
|
|
/**
|
|
* Frees the memory allocated for a guchar* string.
|
|
*
|
|
* @param str Pointer to the guchar* string to be freed. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_free_guchar(guchar** ptr)
|
|
{
|
|
if (ptr == NULL) {
|
|
return;
|
|
}
|
|
g_free(*ptr);
|
|
}
|
|
|
|
/**
|
|
* Frees the memory allocated for a gchar** string array.
|
|
*
|
|
* @param args Pointer to the gchar** string array to be freed. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_free_gcharv(gchar*** args)
|
|
{
|
|
if (args == NULL)
|
|
return;
|
|
g_strfreev(*args);
|
|
}
|
|
|
|
/**
|
|
* Frees the memory allocated for a char* string.
|
|
*
|
|
* @param str Pointer to the char* string to be freed. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_free_char(char** str)
|
|
{
|
|
if (str == NULL)
|
|
return;
|
|
free(*str);
|
|
}
|
|
|
|
/**
|
|
* Closes the file descriptor.
|
|
*
|
|
* @param fd file descriptor handle. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_close_gfd(gint* fd)
|
|
{
|
|
if (fd == NULL)
|
|
return;
|
|
|
|
if (close(*fd) == EOF)
|
|
log_error(g_strerror(errno));
|
|
}
|
|
|
|
/**
|
|
* Closes file stream
|
|
*
|
|
* @param fd file descriptor handle opened with fopen. If NULL, no action is taken.
|
|
*/
|
|
void
|
|
auto_close_FILE(FILE** fd)
|
|
{
|
|
if (fd == NULL)
|
|
return;
|
|
|
|
if (fclose(*fd) == EOF)
|
|
log_error(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)
|
|
{
|
|
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 && error->code != G_FILE_ERROR_NOENT) {
|
|
log_warning("[Keyfile] error loading %s: %s", keyfile->filename, error->message);
|
|
} else {
|
|
log_warning("[Keyfile] no such file: %s", keyfile->filename);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
load_data_keyfile(prof_keyfile_t* keyfile, const char* filename)
|
|
{
|
|
auto_gchar gchar* loc = files_get_data_path(filename);
|
|
return load_custom_keyfile(keyfile, _get_file_or_linked(loc));
|
|
}
|
|
|
|
gboolean
|
|
load_config_keyfile(prof_keyfile_t* keyfile, const char* filename)
|
|
{
|
|
auto_gchar gchar* loc = files_get_config_path(filename);
|
|
return load_custom_keyfile(keyfile, _get_file_or_linked(loc));
|
|
}
|
|
|
|
gboolean
|
|
load_custom_keyfile(prof_keyfile_t* keyfile, gchar* filename)
|
|
{
|
|
keyfile->filename = filename;
|
|
|
|
if (g_file_test(keyfile->filename, G_FILE_TEST_EXISTS)) {
|
|
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
|
|
} else {
|
|
int fno = g_creat(keyfile->filename, S_IRUSR | S_IWUSR);
|
|
if (fno != -1)
|
|
g_close(fno, NULL);
|
|
}
|
|
|
|
return _load_keyfile(keyfile);
|
|
}
|
|
|
|
gboolean
|
|
save_keyfile(prof_keyfile_t* keyfile)
|
|
{
|
|
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", STR_MAYBE_NULL(keyfile->filename), PROF_GERROR_MESSAGE(error));
|
|
return FALSE;
|
|
}
|
|
g_chmod(keyfile->filename, S_IRUSR | S_IWUSR);
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
free_keyfile(prof_keyfile_t* keyfile)
|
|
{
|
|
log_debug("[Keyfile]: free %s", STR_MAYBE_NULL(keyfile->filename));
|
|
if (keyfile->keyfile)
|
|
g_key_file_free(keyfile->keyfile);
|
|
keyfile->keyfile = NULL;
|
|
g_free(keyfile->filename);
|
|
keyfile->filename = NULL;
|
|
}
|
|
|
|
gboolean
|
|
string_to_verbosity(const char* cmd, int* verbosity, gchar** err_msg)
|
|
{
|
|
return strtoi_range(cmd, verbosity, 0, 3, err_msg);
|
|
}
|
|
|
|
gboolean
|
|
create_dir(const char* name)
|
|
{
|
|
if (g_mkdir_with_parents(name, S_IRWXU) != 0) {
|
|
log_error("Failed to create directory at '%s' with error '%s'", name, strerror(errno));
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
copy_file(const char* const sourcepath, const char* const targetpath, const gboolean overwrite_existing)
|
|
{
|
|
GFile* source = g_file_new_for_path(sourcepath);
|
|
GFile* dest = g_file_new_for_path(targetpath);
|
|
GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
|
|
gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, NULL);
|
|
g_object_unref(source);
|
|
g_object_unref(dest);
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* @brief Replaces all occurrences of a substring with a replacement in a string.
|
|
*
|
|
* @param string The input string to perform replacement on.
|
|
* @param substr The substring to be replaced.
|
|
* @param replacement The replacement string.
|
|
* @return The modified string with replacements, or NULL on failure.
|
|
*
|
|
* @note Remember to free returned value using `auto_char` or `free()` when it is no longer needed.
|
|
* @note If 'string' is NULL, the function returns NULL.
|
|
* @note If 'substr' or 'replacement' is NULL or an empty string, a duplicate of 'string' is returned.
|
|
*/
|
|
char*
|
|
str_replace(const char* string, const char* substr,
|
|
const char* replacement)
|
|
{
|
|
const char* head = NULL;
|
|
const char* tok = NULL;
|
|
char* newstr = NULL;
|
|
char* wr = NULL;
|
|
size_t num_substr = 0;
|
|
size_t len_string, len_substr, len_replacement, len_string_remains, len_newstr;
|
|
|
|
if (string == NULL)
|
|
return NULL;
|
|
|
|
if (substr == NULL || replacement == NULL || (strcmp(substr, "") == 0))
|
|
return strdup(string);
|
|
|
|
tok = string;
|
|
while ((tok = strstr(tok, substr))) {
|
|
num_substr++;
|
|
tok++;
|
|
}
|
|
|
|
if (num_substr == 0)
|
|
return strdup(string);
|
|
|
|
len_string = strlen(string);
|
|
len_substr = strlen(substr);
|
|
len_replacement = strlen(replacement);
|
|
len_newstr = len_string - (num_substr * len_substr) + (num_substr * len_replacement);
|
|
newstr = malloc(len_newstr + 1);
|
|
if (newstr == NULL) {
|
|
return NULL;
|
|
}
|
|
len_string_remains = len_string;
|
|
|
|
head = string;
|
|
wr = newstr;
|
|
|
|
while ((tok = strstr(head, substr))) {
|
|
size_t l = tok - head;
|
|
memcpy(wr, head, l);
|
|
wr += l;
|
|
memcpy(wr, replacement, len_replacement);
|
|
wr += len_replacement;
|
|
len_string_remains -= len_substr + l;
|
|
|
|
head = tok + len_substr;
|
|
}
|
|
memcpy(wr, head, len_string_remains);
|
|
newstr[len_newstr] = '\0';
|
|
|
|
return newstr;
|
|
}
|
|
|
|
gboolean
|
|
strtoi_range(const char* str, int* saveptr, int min, int max, gchar** err_msg)
|
|
{
|
|
char* ptr;
|
|
int val;
|
|
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);
|
|
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) {
|
|
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;
|
|
|
|
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(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)
|
|
{
|
|
if (!str) {
|
|
return 0;
|
|
}
|
|
|
|
int len = 0;
|
|
gchar* curr = g_utf8_offset_to_pointer(str, 0);
|
|
while (*curr != '\0') {
|
|
gunichar curru = g_utf8_get_char(curr);
|
|
if (g_unichar_iswide(curru)) {
|
|
len += 2;
|
|
} else {
|
|
len++;
|
|
}
|
|
curr = g_utf8_next_char(curr);
|
|
}
|
|
|
|
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)
|
|
{
|
|
char* url = "https://profanity-im.github.io/profanity_version.txt";
|
|
|
|
CURL* handle = curl_easy_init();
|
|
struct curl_data_t output;
|
|
output.buffer = NULL;
|
|
output.size = 0;
|
|
|
|
curl_easy_setopt(handle, CURLOPT_URL, url);
|
|
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, _data_callback);
|
|
curl_easy_setopt(handle, CURLOPT_TIMEOUT, (long)2);
|
|
curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)&output);
|
|
|
|
curl_easy_perform(handle);
|
|
curl_easy_cleanup(handle);
|
|
|
|
if (output.buffer) {
|
|
output.buffer[output.size++] = '\0';
|
|
return output.buffer;
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
gboolean
|
|
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(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);
|
|
|
|
if (parse_found == 3 && parse_curr == 3) {
|
|
if (found_maj > curr_maj) {
|
|
return TRUE;
|
|
} else if (found_maj == curr_maj && found_min > curr_min) {
|
|
return TRUE;
|
|
} else if (found_maj == curr_maj && found_min == curr_min
|
|
&& found_patch > curr_patch) {
|
|
return TRUE;
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static size_t
|
|
_data_callback(void* ptr, size_t size, size_t nmemb, void* data)
|
|
{
|
|
size_t realsize = size * nmemb;
|
|
struct curl_data_t* mem = (struct curl_data_t*)data;
|
|
mem->buffer = realloc(mem->buffer, mem->size + realsize + 1);
|
|
|
|
if (mem->buffer) {
|
|
memcpy(&(mem->buffer[mem->size]), ptr, realsize);
|
|
mem->size += realsize;
|
|
mem->buffer[mem->size] = 0;
|
|
}
|
|
|
|
return realsize;
|
|
}
|
|
|
|
static gchar*
|
|
_get_file_or_linked(gchar* loc)
|
|
{
|
|
auto_gchar gchar* basedir = g_path_get_dirname(loc);
|
|
gchar* true_loc = g_strdup(loc);
|
|
|
|
// check for symlink
|
|
while (g_file_test(true_loc, G_FILE_TEST_IS_SYMLINK)) {
|
|
gchar* tmp = g_file_read_link(true_loc, NULL);
|
|
g_free(true_loc);
|
|
true_loc = tmp;
|
|
|
|
// if relative, add basedir
|
|
if (!g_str_has_prefix(true_loc, "/") && !g_str_has_prefix(true_loc, "~")) {
|
|
tmp = g_strdup_printf("%s/%s", basedir, true_loc);
|
|
g_free(true_loc);
|
|
true_loc = tmp;
|
|
}
|
|
}
|
|
|
|
return true_loc;
|
|
}
|
|
|
|
char*
|
|
strip_arg_quotes(const char* const input)
|
|
{
|
|
char* unquoted = strdup(input);
|
|
if (unquoted == 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, '"'));
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
gboolean
|
|
is_notify_enabled(void)
|
|
{
|
|
gboolean notify_enabled = FALSE;
|
|
|
|
#ifdef HAVE_OSXNOTIFY
|
|
notify_enabled = TRUE;
|
|
#endif
|
|
#ifdef HAVE_LIBNOTIFY
|
|
notify_enabled = TRUE;
|
|
#endif
|
|
#ifdef PLATFORM_CYGWIN
|
|
notify_enabled = TRUE;
|
|
#endif
|
|
|
|
return notify_enabled;
|
|
}
|
|
|
|
GSList*
|
|
prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
|
|
{
|
|
if (needle == NULL || haystack == NULL || *needle == '\0') {
|
|
return *result;
|
|
}
|
|
|
|
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, p);
|
|
if (haystack_before_ch) {
|
|
before = g_utf8_get_char(haystack_before_ch);
|
|
}
|
|
|
|
gunichar after = 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)) {
|
|
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
|
}
|
|
} else {
|
|
matches = g_slist_prepend(matches, GINT_TO_POINTER(offset));
|
|
}
|
|
}
|
|
|
|
if (*p == '\0') {
|
|
break;
|
|
}
|
|
|
|
p = g_utf8_next_char(p);
|
|
offset++;
|
|
}
|
|
|
|
if (matches) {
|
|
*result = g_slist_concat(*result, g_slist_reverse(matches));
|
|
}
|
|
|
|
return *result;
|
|
}
|
|
|
|
int
|
|
is_regular_file(const char* path)
|
|
{
|
|
struct stat st;
|
|
int ret = stat(path, &st);
|
|
if (ret != 0) {
|
|
perror(NULL);
|
|
return 0;
|
|
}
|
|
return S_ISREG(st.st_mode);
|
|
}
|
|
|
|
int
|
|
is_dir(const char* path)
|
|
{
|
|
struct stat st;
|
|
int ret = stat(path, &st);
|
|
if (ret != 0) {
|
|
perror(NULL);
|
|
return 0;
|
|
}
|
|
return S_ISDIR(st.st_mode);
|
|
}
|
|
|
|
void
|
|
get_file_paths_recursive(const char* path, GSList** contents)
|
|
{
|
|
if (!is_dir(path)) {
|
|
return;
|
|
}
|
|
|
|
GDir* directory = g_dir_open(path, 0, NULL);
|
|
const gchar* entry = g_dir_read_name(directory);
|
|
gchar* full;
|
|
while (entry) {
|
|
if (g_str_has_suffix(path, "/")) {
|
|
full = g_strdup_printf("%s%s", path, entry);
|
|
} else {
|
|
full = g_strdup_printf("%s/%s", path, entry);
|
|
}
|
|
|
|
if (is_dir(full)) {
|
|
get_file_paths_recursive(full, contents);
|
|
} else if (is_regular_file(full)) {
|
|
*contents = g_slist_append(*contents, full);
|
|
}
|
|
|
|
entry = g_dir_read_name(directory);
|
|
}
|
|
}
|
|
|
|
gchar*
|
|
get_random_string(int length)
|
|
{
|
|
GRand* prng;
|
|
gchar* rand;
|
|
gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
int endrange = sizeof(alphabet) - 1;
|
|
|
|
rand = g_malloc0(length + 1);
|
|
|
|
prng = g_rand_new();
|
|
|
|
for (int i = 0; i < length; i++) {
|
|
rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
|
|
}
|
|
g_rand_free(prng);
|
|
|
|
return rand;
|
|
}
|
|
|
|
GSList*
|
|
get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const message, const char* const nick)
|
|
{
|
|
GSList* mentions = NULL;
|
|
auto_gchar gchar* message_search = case_sensitive ? g_strdup(message) : g_utf8_strdown(message, -1);
|
|
auto_gchar gchar* mynick_search = case_sensitive ? g_strdup(nick) : g_utf8_strdown(nick, -1);
|
|
|
|
mentions = prof_occurrences(mynick_search, message_search, 0, whole_word, &mentions);
|
|
|
|
return mentions;
|
|
}
|
|
|
|
gboolean
|
|
call_external(gchar** argv)
|
|
{
|
|
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;
|
|
|
|
is_successful = g_spawn_async(NULL, // Inherit the parent PWD
|
|
argv,
|
|
NULL, // Inherit the parent environment
|
|
flags,
|
|
NULL, NULL, NULL,
|
|
&spawn_error);
|
|
if (!is_successful) {
|
|
auto_gchar gchar* cmd = g_strjoinv(" ", argv);
|
|
log_error("Spawning '%s' failed with error '%s'", cmd, PROF_GERROR_MESSAGE(spawn_error));
|
|
}
|
|
|
|
return is_successful;
|
|
}
|
|
|
|
/**
|
|
* @brief Formats an argument vector for calling an external command with placeholders.
|
|
*
|
|
* This function constructs an argument vector (argv) based on the provided template string, replacing placeholders ("%u" and "%p") with the provided URL and filename, respectively.
|
|
*
|
|
* @param template The template string with placeholders.
|
|
* @param url The URL to replace "%u" (or NULL to skip).
|
|
* @param filename The filename to replace "%p" (or NULL to skip).
|
|
* @return The constructed argument vector (argv) as a null-terminated array of strings.
|
|
*
|
|
* @note Remember to free the returned argument vector using `auto_gcharv` or `g_strfreev()`.
|
|
*/
|
|
gchar**
|
|
format_call_external_argv(const char* template, const char* url, const char* filename)
|
|
{
|
|
gchar** argv = g_strsplit(template, " ", 0);
|
|
|
|
guint num_args = 0;
|
|
while (argv[num_args]) {
|
|
if (0 == g_strcmp0(argv[num_args], "%u") && url != NULL) {
|
|
g_free(argv[num_args]);
|
|
argv[num_args] = g_strdup(url);
|
|
} else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
|
|
g_free(argv[num_args]);
|
|
argv[num_args] = strdup(filename);
|
|
}
|
|
num_args++;
|
|
}
|
|
|
|
return argv;
|
|
}
|
|
|
|
static gchar*
|
|
_unique_filename(const char* filename)
|
|
{
|
|
gchar* unique = g_strdup(filename);
|
|
|
|
unsigned int i = 0;
|
|
while (g_file_test(unique, G_FILE_TEST_EXISTS)) {
|
|
g_free(unique);
|
|
|
|
if (i > 1000) { // Give up after 1000 attempts.
|
|
return NULL;
|
|
}
|
|
|
|
unique = g_strdup_printf("%s.%u", filename, i);
|
|
if (!unique) {
|
|
return NULL;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return unique;
|
|
}
|
|
|
|
static gboolean
|
|
_has_directory_suffix(const char* path)
|
|
{
|
|
return (g_str_has_suffix(path, ".")
|
|
|| g_str_has_suffix(path, "..")
|
|
|| g_str_has_suffix(path, G_DIR_SEPARATOR_S));
|
|
}
|
|
|
|
/**
|
|
* @brief Extracts the basename from a given URL.
|
|
*
|
|
* @param url The URL to extract the basename from.
|
|
* @return The extracted basename or a default name ("index") if the basename has a directory suffix.
|
|
*
|
|
* @note The returned string must be freed by the caller using `auto_free` or `free()`.
|
|
*/
|
|
char*
|
|
basename_from_url(const char* url)
|
|
{
|
|
const char* default_name = "index";
|
|
|
|
GFile* file = g_file_new_for_commandline_arg(url);
|
|
auto_gchar gchar* basename = g_file_get_basename(file);
|
|
g_object_unref(file);
|
|
|
|
if (_has_directory_suffix(basename)) {
|
|
return strdup(default_name);
|
|
}
|
|
|
|
return strdup(basename);
|
|
}
|
|
|
|
/**
|
|
* Expands the provided path by resolving special characters like '~' and 'file://'.
|
|
*
|
|
* @param path The path to expand.
|
|
* @return The expanded path.
|
|
*
|
|
* @note Remember to free the returned value using `auto_gchar` or `g_free()`.
|
|
*/
|
|
gchar*
|
|
get_expanded_path(const char* path)
|
|
{
|
|
if (g_str_has_prefix(path, "file://")) {
|
|
path += strlen("file://");
|
|
}
|
|
if (strlen(path) >= 2 && path[0] == '~' && path[1] == '/') {
|
|
return g_strdup_printf("%s/%s", getenv("HOME"), path + 2);
|
|
} else {
|
|
return g_strdup_printf("%s", path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a unique filename based on the provided URL and path.
|
|
*
|
|
* @param url The URL to derive the basename from.
|
|
* @param path The path to prepend to the generated filename. If NULL, the current directory is used.
|
|
* @return A unique filename combining the path and derived basename from the URL.
|
|
*
|
|
* @note Remember to free the returned value using `auto_gchar` or `g_free()`.
|
|
*/
|
|
gchar*
|
|
unique_filename_from_url(const char* url, const char* path)
|
|
{
|
|
auto_gchar gchar* realpath = NULL;
|
|
|
|
// Default to './' as path when none has been provided.
|
|
if (path == NULL) {
|
|
realpath = g_strdup("./");
|
|
} else {
|
|
realpath = get_expanded_path(path);
|
|
}
|
|
|
|
// Resolves paths such as './../.' for path.
|
|
GFile* target = g_file_new_for_commandline_arg(realpath);
|
|
auto_gchar gchar* filename = NULL;
|
|
|
|
if (_has_directory_suffix(realpath) || g_file_test(realpath, G_FILE_TEST_IS_DIR)) {
|
|
// The target should be used as a directory. Assume that the basename
|
|
// should be derived from the URL.
|
|
auto_char char* basename = basename_from_url(url);
|
|
filename = g_build_filename(g_file_peek_path(target), basename, NULL);
|
|
} else {
|
|
// Just use the target as filename.
|
|
filename = g_build_filename(g_file_peek_path(target), NULL);
|
|
}
|
|
|
|
gchar* unique_filename = _unique_filename(filename);
|
|
if (unique_filename == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
g_object_unref(target);
|
|
|
|
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
|
|
*/
|
|
gchar*
|
|
prof_get_version(void)
|
|
{
|
|
if (strcmp(PACKAGE_STATUS, "development") == 0) {
|
|
#ifdef HAVE_GIT_VERSION
|
|
return g_strdup_printf("%sdev.%s.%s", PACKAGE_VERSION, PROF_GIT_BRANCH, PROF_GIT_REVISION);
|
|
#else
|
|
return g_strdup_printf("%sdev", PACKAGE_VERSION);
|
|
#endif
|
|
} else {
|
|
return g_strdup_printf("%s", PACKAGE_VERSION);
|
|
}
|
|
}
|