irssi/src/core/special-vars.c
ailin-nemui 6b93d6e338 implement expando cache
- the expando values need to be stored now that the lines are
  reformattable, otherwise the old values are lost (and they depend on
  context only available at the time the line is initially printed)

- the cache is collected from the special-vars evaluation code

- the cache is controlled by the textbuffer-formats code, and stored in
  the text_buffer_format_rec
2020-04-26 20:42:02 +02:00

843 lines
19 KiB
C

/*
special-vars.c : irssi
Copyright (C) 2000 Timo Sirainen
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "module.h"
#include <irssi/src/core/expandos.h>
#include <irssi/src/core/misc.h>
#include <irssi/src/core/refstrings.h>
#include <irssi/src/core/servers.h>
#include <irssi/src/core/settings.h>
#include <irssi/src/core/signals.h>
#include <irssi/src/core/special-vars.h>
#include <irssi/src/core/utf8.h>
#define isvarchar(c) \
(i_isalnum(c) || (c) == '_')
#define isarg(c) \
(i_isdigit(c) || (c) == '*' || (c) == '~' || (c) == '-')
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION /* fuzzer should not exhaust memory here */
#define ALIGN_MAX 512
#else
#define ALIGN_MAX 222488
#endif
static SPECIAL_HISTORY_FUNC history_func = NULL;
static GSList *special_collector;
static GSList *special_cache;
static char *get_argument(char **cmd, char **arglist)
{
GString *str;
char *ret;
int max, arg, argcount;
arg = 0;
max = -1;
argcount = arglist == NULL ? 0 : g_strv_length(arglist);
if (**cmd == '*') {
/* get all arguments */
} else if (**cmd == '~') {
/* get last argument */
arg = max = argcount-1;
} else {
if (i_isdigit(**cmd)) {
/* first argument */
arg = max = (**cmd)-'0';
(*cmd)++;
}
if (**cmd == '-') {
/* get more than one argument */
(*cmd)++;
if (!i_isdigit(**cmd))
max = -1; /* get all the rest */
else {
max = (**cmd)-'0';
(*cmd)++;
}
}
(*cmd)--;
}
str = g_string_new(NULL);
while (arg >= 0 && arg < argcount && (arg <= max || max == -1)) {
g_string_append(str, arglist[arg]);
g_string_append_c(str, ' ');
arg++;
}
if (str->len > 0) g_string_truncate(str, str->len-1);
ret = str->str;
g_string_free(str, FALSE);
return ret;
}
static char *get_long_variable_value(const char *key, SERVER_REC *server,
void *item, int *free_ret)
{
EXPANDO_FUNC func;
const char *ret;
SETTINGS_REC *rec;
*free_ret = FALSE;
/* expando? */
func = expando_find_long(key);
if (func != NULL) {
current_expando = key;
return func(server, item, free_ret);
}
/* internal setting? */
rec = settings_get_record(key);
if (rec != NULL) {
*free_ret = TRUE;
return settings_get_print(rec);
}
/* environment variable? */
ret = g_getenv(key);
if (ret != NULL)
return (char *) ret;
return NULL;
}
static gboolean cache_find(GSList **cache, const char *var, char **ret)
{
GSList *tmp;
GSList *prev = NULL;
if (cache == NULL)
return FALSE;
for (tmp = *cache; tmp;) {
if (g_strcmp0(var, tmp->data) == 0) {
*ret = tmp->next->data;
if (prev != NULL)
prev->next->next = tmp->next->next;
else
*cache = tmp->next->next;
g_slist_free_1(tmp->next);
g_slist_free_1(tmp);
return TRUE;
}
prev = tmp;
tmp = tmp->next->next;
}
return FALSE;
}
static gboolean cache_find_char(GSList **cache, char var, char **ret)
{
char varn[] = { var, '\0' };
return cache_find(cache, varn, ret);
}
static char *get_long_variable(char **cmd, SERVER_REC *server, void *item, int *free_ret,
int getname, GSList **collector, GSList **cache)
{
char *start, *var, *ret;
/* get variable name */
start = *cmd;
while (isvarchar((*cmd)[1])) (*cmd)++;
var = g_strndup(start, (int) (*cmd-start)+1);
if (getname) {
*free_ret = TRUE;
return var;
}
if (cache_find(cache, var, &ret)) {
g_free(var);
return ret;
}
ret = get_long_variable_value(var, server, item, free_ret);
if (collector != NULL) {
*collector = g_slist_prepend(*collector, g_strdup(ret));
*collector = g_slist_prepend(*collector, i_refstr_intern(var));
}
g_free(var);
return ret;
}
/* return the value of the variable found from `cmd'.
if 'getname' is TRUE, return the name of the variable instead it's value */
static char *get_variable(char **cmd, SERVER_REC *server, void *item, char **arglist, int *free_ret,
int *arg_used, int getname, GSList **collector, GSList **cache)
{
EXPANDO_FUNC func;
if (isarg(**cmd)) {
/* argument */
*free_ret = TRUE;
if (arg_used != NULL) *arg_used = TRUE;
return getname ? g_strdup_printf("%c", **cmd) :
get_argument(cmd, arglist);
}
if (i_isalpha(**cmd) && isvarchar((*cmd)[1])) {
/* long variable name.. */
return get_long_variable(cmd, server, item, free_ret, getname, collector, cache);
}
/* single character variable. */
if (getname) {
*free_ret = TRUE;
return g_strdup_printf("%c", **cmd);
}
*free_ret = FALSE;
{
char *ret;
if (cache_find_char(cache, **cmd, &ret)) {
return ret;
}
}
func = expando_find_char(**cmd);
if (func == NULL)
return NULL;
else {
char str[2];
char *ret;
str[0] = **cmd; str[1] = '\0';
current_expando = str;
ret = func(server, item, free_ret);
if (**cmd != 'Z' && collector != NULL) {
*collector = g_slist_prepend(*collector, g_strdup(ret));
*collector = g_slist_prepend(*collector, i_refstr_intern(str));
}
return ret;
}
}
static char *get_history(char **cmd, void *item, int *free_ret)
{
char *start, *text, *ret;
/* get variable name */
start = ++(*cmd);
while (**cmd != '\0' && **cmd != '!') (*cmd)++;
if (history_func == NULL)
ret = NULL;
else {
text = g_strndup(start, (int) (*cmd-start));
ret = history_func(text, item, free_ret);
g_free(text);
}
if (**cmd == '\0') (*cmd)--;
return ret;
}
static char *get_special_value(char **cmd, SERVER_REC *server, void *item, char **arglist,
int *free_ret, int *arg_used, int flags, GSList **collector,
GSList **cache)
{
char command, *value, *p;
int len;
if ((flags & PARSE_FLAG_ONLY_ARGS) && !isarg(**cmd)) {
*free_ret = TRUE;
return g_strdup_printf("$%c", **cmd);
}
if (**cmd == '!') {
/* find text from command history */
if (flags & PARSE_FLAG_GETNAME)
return "!";
return get_history(cmd, item, free_ret);
}
command = 0;
if (**cmd == '#' || **cmd == '@') {
command = **cmd;
if ((*cmd)[1] != '\0')
(*cmd)++;
else {
/* default to $* */
char *temp_cmd = "*";
if (flags & PARSE_FLAG_GETNAME)
return "*";
*free_ret = TRUE;
return get_argument(&temp_cmd, arglist);
}
}
value = get_variable(cmd, server, item, arglist, free_ret, arg_used,
flags & PARSE_FLAG_GETNAME, collector, cache);
if (flags & PARSE_FLAG_GETNAME)
return value;
if (command == '#') {
/* number of words */
if (value == NULL || *value == '\0') {
if (value != NULL && *free_ret) {
g_free(value);
*free_ret = FALSE;
}
return "0";
}
len = 1;
for (p = value; *p != '\0'; p++) {
if (*p == ' ' && (p[1] != ' ' && p[1] != '\0'))
len++;
}
if (*free_ret) g_free(value);
*free_ret = TRUE;
return g_strdup_printf("%d", len);
}
if (command == '@') {
/* number of characters */
if (value == NULL) return "0";
len = strlen(value);
if (*free_ret) g_free(value);
*free_ret = TRUE;
return g_strdup_printf("%d", len);
}
return value;
}
/* get alignment arguments (inside the []) */
static int get_alignment_args(char **data, int *align, int *flags, char *pad)
{
char *str;
char *endptr;
guint align_;
*align = 0;
*flags = ALIGN_CUT|ALIGN_PAD;
*pad = ' ';
/* '!' = don't cut, '-' = right padding */
str = *data;
while (*str != '\0' && *str != ']' && !i_isdigit(*str)) {
if (*str == '!')
*flags &= ~ALIGN_CUT;
else if (*str == '-')
*flags |= ALIGN_RIGHT;
else if (*str == '.')
*flags &= ~ALIGN_PAD;
str++;
}
if (!i_isdigit(*str))
return FALSE; /* expecting number */
/* get the alignment size */
if (!parse_uint(str, &endptr, 10, &align_)) {
return FALSE;
}
/* alignment larger than supported */
if (align_ > ALIGN_MAX) {
return FALSE;
}
str = endptr;
*align = align_;
/* get the pad character */
while (*str != '\0' && *str != ']') {
*pad = *str;
str++;
}
if (*str++ != ']') return FALSE;
*data = str;
return TRUE;
}
/* return the aligned text */
char *get_alignment(const char *text, int align, int flags, char pad)
{
GString *str;
char *ret;
int policy;
unsigned int cut_bytes;
g_return_val_if_fail(text != NULL, NULL);
policy = string_policy(text);
str = g_string_new(text);
/* cut */
if ((flags & ALIGN_CUT) && align > 0 && string_width(text, policy) > align) {
string_chars_for_width(text, policy, align, &cut_bytes);
g_string_truncate(str, cut_bytes);
}
/* add pad characters */
if (flags & ALIGN_PAD) {
int pad_len = align - string_width(str->str, policy);
if (pad_len > 0) {
char *pad_full = g_strnfill(pad_len, pad);
if (flags & ALIGN_RIGHT)
g_string_prepend(str, pad_full);
else
g_string_append(str, pad_full);
g_free(pad_full);
}
}
ret = str->str;
g_string_free(str, FALSE);
return ret;
}
/* Parse and expand text after '$' character. return value has to be
g_free()'d if `free_ret' is TRUE. */
char *parse_special(char **cmd, SERVER_REC *server, void *item,
char **arglist, int *free_ret, int *arg_used, int flags)
{
static char **nested_orig_cmd = NULL; /* FIXME: KLUDGE! */
char command, *value;
char align_pad = '\0';
int align = 0, align_flags = 0;
char *nest_value;
int brackets, nest_free;
*free_ret = FALSE;
if (**cmd == '\0')
return NULL;
command = **cmd; (*cmd)++;
switch (command) {
case '[':
/* alignment */
if (!get_alignment_args(cmd, &align, &align_flags,
&align_pad) || **cmd == '\0') {
(*cmd)--;
return NULL;
}
break;
default:
command = 0;
(*cmd)--;
}
nest_free = FALSE; nest_value = NULL;
#if 0 /* this code is disabled due to security issues until it is fixed */
if (**cmd == '(' && (*cmd)[1] != '\0') {
/* subvariable */
int toplevel = nested_orig_cmd == NULL;
if (toplevel) nested_orig_cmd = cmd;
(*cmd)++;
if (**cmd != '$') {
/* ... */
nest_value = *cmd;
} else {
(*cmd)++;
nest_value = parse_special(cmd, server, item, arglist,
&nest_free, arg_used,
flags);
}
if (nest_value == NULL || *nest_value == '\0')
return NULL;
while ((*nested_orig_cmd)[1] != '\0') {
(*nested_orig_cmd)++;
if (**nested_orig_cmd == ')')
break;
}
cmd = &nest_value;
if (toplevel) nested_orig_cmd = NULL;
}
#else
if (nested_orig_cmd) nested_orig_cmd = NULL;
#endif
if (**cmd != '{')
brackets = FALSE;
else {
/* special value is inside {...} (foo${test}bar -> fooXXXbar) */
if ((*cmd)[1] == '\0')
return NULL;
(*cmd)++;
brackets = TRUE;
}
value = get_special_value(cmd, server, item, arglist, free_ret, arg_used, flags,
special_collector != NULL ? special_collector->data : NULL,
&special_cache);
if (**cmd == '\0')
g_error("parse_special() : buffer overflow!");
if (value != NULL && *value != '\0' && (flags & PARSE_FLAG_ISSET_ANY))
*arg_used = TRUE;
if (brackets) {
while (**cmd != '}' && (*cmd)[1] != '\0')
(*cmd)++;
}
if (nest_free) g_free(nest_value);
if (command == '[' && (flags & PARSE_FLAG_GETNAME) == 0) {
/* alignment */
char *p;
if (value == NULL) return "";
p = get_alignment(value, align, align_flags, align_pad);
if (*free_ret) g_free(value);
*free_ret = TRUE;
return p;
}
return value;
}
static void gstring_append_escaped(GString *str, const char *text, int flags)
{
char esc[4], *escpos;
escpos = esc;
if (flags & PARSE_FLAG_ESCAPE_VARS)
*escpos++ = '%';
if (flags & PARSE_FLAG_ESCAPE_THEME) {
*escpos++ = '{';
*escpos++ = '}';
}
if (escpos == esc) {
g_string_append(str, text);
return;
}
*escpos = '\0';
while (*text != '\0') {
for (escpos = esc; *escpos != '\0'; escpos++) {
if (*text == *escpos) {
g_string_append_c(str, '%');
break;
}
}
g_string_append_c(str, *text);
text++;
}
}
/* parse the whole string. $ and \ chars are replaced */
char *parse_special_string(const char *cmd, SERVER_REC *server, void *item,
const char *data, int *arg_used, int flags)
{
char code, **arglist, *ret;
GString *str;
int need_free, chr;
g_return_val_if_fail(cmd != NULL, NULL);
g_return_val_if_fail(data != NULL, NULL);
/* create the argument list */
arglist = g_strsplit(data, " ", -1);
if (arg_used != NULL) *arg_used = FALSE;
code = 0;
str = g_string_new(NULL);
while (*cmd != '\0') {
if (code == '\\') {
if (*cmd == ';')
g_string_append_c(str, ';');
else {
chr = expand_escape(&cmd);
g_string_append_c(str, chr != -1 ? chr : *cmd);
}
code = 0;
} else if (code == '$') {
char *ret;
ret = parse_special((char **) &cmd, server, item,
arglist, &need_free, arg_used,
flags);
if (ret != NULL) {
gstring_append_escaped(str, ret, flags);
if (need_free) g_free(ret);
}
code = 0;
} else {
if (*cmd == '\\' || *cmd == '$')
code = *cmd;
else
g_string_append_c(str, *cmd);
}
cmd++;
}
g_strfreev(arglist);
ret = str->str;
g_string_free(str, FALSE);
return ret;
}
#define is_split_char(str, start) \
((str)[0] == ';' && ((start) == (str) || \
((str)[-1] != '\\' && (str)[-1] != '$')))
/* execute the commands in string - commands can be split with ';' */
void eval_special_string(const char *cmd, const char *data,
SERVER_REC *server, void *item)
{
const char *cmdchars;
char *orig, *str, *start, *ret;
int arg_used, arg_used_ever;
GSList *commands;
commands = NULL;
arg_used_ever = FALSE;
cmdchars = settings_get_str("cmdchars");
/* get a list of all the commands to run */
orig = start = str = g_strdup(cmd);
do {
if (is_split_char(str, start)) {
*str++ = '\0';
while (*str == ' ') str++;
} else if (*str != '\0') {
str++;
continue;
}
ret = parse_special_string(start, server, item,
data, &arg_used, 0);
if (*ret != '\0') {
if (arg_used) arg_used_ever = TRUE;
if (strchr(cmdchars, *ret) == NULL) {
/* no command char - let's put it there.. */
char *old = ret;
ret = g_strdup_printf("%c%s", *cmdchars, old);
g_free(old);
}
commands = g_slist_append(commands, ret);
}
start = str;
} while (*start != '\0');
/* run the command, if no arguments were ever used, append all of them
after each command */
while (commands != NULL) {
ret = commands->data;
if (!arg_used_ever && *data != '\0') {
char *old = ret;
ret = g_strconcat(old, " ", data, NULL);
g_free(old);
}
if (server != NULL)
server_ref(server);
signal_emit("send command", 3, ret, server, item);
if (server != NULL && !server_unref(server)) {
/* the server was destroyed */
server = NULL;
item = NULL;
}
/* FIXME: window item would need reference counting as well,
eg. "/EVAL win close;say hello" wouldn't work now.. */
commands = g_slist_remove(commands, commands->data);
g_free(ret);
}
g_free(orig);
}
void special_history_func_set(SPECIAL_HISTORY_FUNC func)
{
history_func = func;
}
void special_push_collector(GSList **list)
{
special_collector = g_slist_prepend(special_collector, list);
}
void special_pop_collector(void)
{
special_collector = g_slist_delete_link(special_collector, special_collector);
}
void special_fill_cache(GSList *list)
{
g_slist_free(special_cache);
special_cache = g_slist_copy(list);
}
static void update_signals_hash(GHashTable **hash, int *signals)
{
void *signal_id;
int arg_type;
if (*hash == NULL) {
*hash = g_hash_table_new((GHashFunc) g_direct_hash,
(GCompareFunc) g_direct_equal);
}
while (*signals != -1) {
signal_id = GINT_TO_POINTER(*signals);
arg_type = GPOINTER_TO_INT(g_hash_table_lookup(*hash, signal_id));
if (arg_type != 0 && arg_type != signals[1]) {
/* same signal is used for different purposes ..
not sure if this should ever happen, but change
the argument type to none so it will at least
work. */
arg_type = EXPANDO_ARG_NONE;
}
if (arg_type == 0) arg_type = signals[1];
g_hash_table_insert(*hash, signal_id,
GINT_TO_POINTER(arg_type));
signals += 2;
}
}
static void get_signal_hash(void *signal_id, void *arg_type, int **pos)
{
(*pos)[0] = GPOINTER_TO_INT(signal_id);
(*pos)[1] = GPOINTER_TO_INT(arg_type);
(*pos) += 2;
}
static int *get_signals_list(GHashTable *hash)
{
int *signals, *pos;
if (hash == NULL) {
/* no expandos in text - never needs updating */
return NULL;
}
pos = signals = g_new(int, g_hash_table_size(hash)*2 + 1);
g_hash_table_foreach(hash, (GHFunc) get_signal_hash, &pos);
*pos = -1;
g_hash_table_destroy(hash);
return signals;
}
#define TASK_BIND 1
#define TASK_UNBIND 2
#define TASK_GET_SIGNALS 3
static int *special_vars_signals_task(const char *text, int funccount,
SIGNAL_FUNC *funcs, int task)
{
GHashTable *signals;
char *expando;
int need_free, *expando_signals;
signals = NULL;
while (*text != '\0') {
if (*text == '\\' && text[1] != '\0') {
/* escape */
text += 2;
} else if (*text == '$' && text[1] != '\0') {
/* expando */
text++;
expando = parse_special((char **) &text, NULL, NULL,
NULL, &need_free, NULL,
PARSE_FLAG_GETNAME);
if (expando == NULL)
continue;
switch (task) {
case TASK_BIND:
expando_bind(expando, funccount, funcs);
break;
case TASK_UNBIND:
expando_unbind(expando, funccount, funcs);
break;
case TASK_GET_SIGNALS:
expando_signals = expando_get_signals(expando);
if (expando_signals != NULL) {
update_signals_hash(&signals,
expando_signals);
g_free(expando_signals);
}
break;
}
if (need_free) g_free(expando);
} else {
/* just a char */
text++;
}
}
if (task == TASK_GET_SIGNALS)
return get_signals_list(signals);
return NULL;
}
void special_vars_add_signals(const char *text,
int funccount, SIGNAL_FUNC *funcs)
{
special_vars_signals_task(text, funccount, funcs, TASK_BIND);
}
void special_vars_remove_signals(const char *text,
int funccount, SIGNAL_FUNC *funcs)
{
special_vars_signals_task(text, funccount, funcs, TASK_UNBIND);
}
int *special_vars_get_signals(const char *text)
{
return special_vars_signals_task(text, 0, NULL, TASK_GET_SIGNALS);
}
void special_vars_init(void)
{
special_cache = NULL;
special_collector = NULL;
}
void special_vars_deinit(void)
{
g_slist_free(special_cache);
g_slist_free(special_collector);
}