1
0
mirror of https://github.com/rkd77/elinks.git synced 2025-01-03 14:57:44 -05:00
elinks/src/config/conf.c

1385 lines
36 KiB
C
Raw Normal View History

/* Config file manipulation */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h> /* OS/2 needs this after sys/types.h */
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include "elinks.h"
#include "config/conf.h"
#include "config/dialogs.h"
#include "config/domain.h"
#include "config/home.h"
#include "config/kbdbind.h"
#include "config/options.h"
#include "config/opttypes.h"
#include "intl/libintl.h"
#include "osdep/osdep.h"
#include "terminal/terminal.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/qs_parse/qs_parse.h"
#include "util/secsave.h"
#include "util/string.h"
/* Config file has only very simple grammar:
*
* /set *option *= *value/
* /bind *keymap *keystroke *= *action/
* /include *file/
* /#.*$/
*
* Where option consists from any number of categories separated by dots and
* name of the option itself. Both category and option name consists from
* [a-zA-Z0-9_-*] - using uppercase letters is not recommended, though. '*' is
* reserved and is used only as escape character in place of '.' originally in
* option name.
*
* Value can consist from:
* - number (it will be converted to int/long)
* - enum (like on, off; true, fake, last_url; etc ;) - in planning state yet
* - string - "blah blah" (keymap, keystroke and action and file looks like that too)
*
* "set" command is parsed first, and then type-specific function is called,
* with option as one parameter and value as a second. Usually it just assigns
* value to an option, but sometimes you may want to first create the option
* ;). Then this will come handy. */
2016-04-20 14:57:09 -04:00
struct conf_parsing_pos {
/** Points to the next character to be parsed from the
* configuration file. */
char *look;
2016-04-20 14:57:09 -04:00
/** The line number corresponding to #look. This is
* shown in error messages. */
int line;
};
2008-02-02 17:16:38 -05:00
struct conf_parsing_state {
/** This part may be copied to a local variable as a bookmark
* and restored later. So it must not contain any pointers
* that would have to be freed in that situation. */
2016-04-20 14:57:09 -04:00
struct conf_parsing_pos pos;
/** When ELinks is rewriting the configuration file, @c mirrored
* indicates the end of the part that has already been copied
* to the mirror string. Otherwise, @c mirrored is not used.
*
* @invariant @c mirrored @<= @c pos.look */
char *mirrored;
/** File name for error messages. If NULL then do not display
* error messages. */
const char *filename;
2008-02-02 17:16:38 -05:00
};
/** Tell the user about an error in the configuration file.
* @return @a err, for convenience. */
static enum parse_error
show_parse_error(const struct conf_parsing_state *state, enum parse_error err)
{
static const char error_msg[][40] = {
"no error", /* ERROR_NONE */
"unknown command", /* ERROR_COMMAND */
"parse error", /* ERROR_PARSE */
"unknown option", /* ERROR_OPTION */
"bad value", /* ERROR_VALUE */
"no memory left", /* ERROR_NOMEM */
};
if (state->filename) {
fprintf(stderr, "%s:%d: %s\n",
state->filename, state->pos.line, error_msg[err]);
}
return err;
}
2008-02-02 17:16:38 -05:00
/** Skip comments and whitespace. */
static void
skip_white(struct conf_parsing_pos *pos)
{
char *start = pos->look;
2008-02-02 17:16:38 -05:00
while (*start) {
while (isspace((unsigned char)*start)) {
if (*start == '\n') {
2008-02-02 17:16:38 -05:00
pos->line++;
}
start++;
}
if (*start == '#') {
start += strcspn(start, "\n");
} else {
2008-02-02 17:16:38 -05:00
pos->look = start;
return;
}
}
2008-02-02 17:16:38 -05:00
pos->look = start;
}
/** Skip a quoted string.
* This function allows "mismatching quotes' because str_rd() does so. */
static void
skip_quoted(struct conf_parsing_pos *pos)
{
assert(isquote(*pos->look));
if_assert_failed return;
pos->look++;
for (;;) {
if (!*pos->look)
return;
if (isquote(*pos->look)) {
pos->look++;
return;
}
if (*pos->look == '\\' && pos->look[1])
pos->look++;
if (*pos->look == '\n')
pos->line++;
pos->look++;
}
}
/** Skip the value of an option.
*
* This job is normally done by the reader function that corresponds
* to the type of the option. However, if ELinks does not recognize
* the name of the option, it cannot look up the type and has to use
* this function instead. */
static void
skip_option_value(struct conf_parsing_pos *pos)
{
if (isquote(*pos->look)) {
/* Looks like OPT_STRING, OPT_CODEPAGE, OPT_LANGUAGE,
* or OPT_COLOR. */
skip_quoted(pos);
} else {
/* Looks like OPT_BOOL, OPT_INT, or OPT_LONG. */
while (isasciialnum(*pos->look) || *pos->look == '.'
|| *pos->look == '+' || *pos->look == '-')
pos->look++;
}
}
/** Skip to the next newline or comment that is not part of a quoted
* string. When ELinks hits a parse error in the configuration file,
* it calls this in order to find the place where is should resume
* parsing. This is intended to prevent ELinks from treating words
* in strings as commands. */
static void
skip_to_unquoted_newline_or_comment(struct conf_parsing_pos *pos)
{
while (*pos->look && *pos->look != '#' && *pos->look != '\n') {
if (isquote(*pos->look))
skip_quoted(pos);
else
pos->look++;
}
}
/* Parse a command. Returns error code. */
/* If dynamic string credentials are supplied, we will mirror the command at
* the end of the string; however, we won't load the option value to the tree,
* and we will even write option value from the tree to the output string.
* We will only possibly set or clear OPT_MUST_SAVE flag in the option. */
static enum parse_error
parse_set_common(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf, int want_domain)
{
const char *domain_orig = NULL;
size_t domain_len = 0;
char *domain_copy = NULL;
const char *optname_orig;
size_t optname_len;
char *optname_copy;
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
if (want_domain) {
domain_orig = state->pos.look;
while (isident(*state->pos.look) || *state->pos.look == '*'
|| *state->pos.look == '.' || *state->pos.look == '+')
state->pos.look++;
domain_len = state->pos.look - domain_orig;
skip_white(&state->pos);
}
/* Option name */
optname_orig = state->pos.look;
while (is_option_name_char(*state->pos.look)
|| *state->pos.look == '.')
2008-02-02 17:16:38 -05:00
state->pos.look++;
optname_len = state->pos.look - optname_orig;
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
/* Equal sign */
if (*state->pos.look != '=')
return show_parse_error(state, ERROR_PARSE);
2008-02-02 17:16:38 -05:00
state->pos.look++; /* '=' */
skip_white(&state->pos);
if (!*state->pos.look)
return show_parse_error(state, ERROR_VALUE);
optname_copy = memacpy(optname_orig, optname_len);
if (!optname_copy) return show_parse_error(state, ERROR_NOMEM);
if (want_domain) {
domain_copy = memacpy(domain_orig, domain_len);
if (!domain_copy) {
mem_free(optname_copy);
return show_parse_error(state, ERROR_NOMEM);
}
}
/* Option value */
{
struct option *opt;
char *val;
const struct conf_parsing_pos pos_before_value = state->pos;
if (want_domain && *domain_copy) {
struct option *domain_tree;
2007-09-14 09:14:34 -04:00
domain_tree = get_domain_tree(domain_copy);
if (!domain_tree) {
mem_free(domain_copy);
mem_free(optname_copy);
skip_option_value(&state->pos);
return show_parse_error(state, ERROR_NOMEM);
}
if (mirror) {
opt = get_opt_rec_real(domain_tree,
optname_copy);
} else {
opt = get_opt_rec(opt_tree, optname_copy);
if (opt) {
opt = get_option_shadow(opt, opt_tree,
domain_tree);
if (!opt) {
mem_free(domain_copy);
mem_free(optname_copy);
skip_option_value(&state->pos);
return show_parse_error(state,
ERROR_NOMEM);
}
}
}
} else {
opt = mirror
? get_opt_rec_real(opt_tree, optname_copy)
: get_opt_rec(opt_tree, optname_copy);
}
if (want_domain)
mem_free(domain_copy);
domain_copy = NULL;
mem_free(optname_copy);
optname_copy = NULL;
if (!opt || (opt->flags & OPT_HIDDEN)) {
show_parse_error(state, ERROR_OPTION);
skip_option_value(&state->pos);
return ERROR_OPTION;
/* TODO: Distinguish between two scenarios:
* - A newer version of ELinks has saved an
* option that this version does not recognize.
* The option must be preserved. (This works.)
* - The user has added an option, saved
* elinks.conf, restarted ELinks, deleted the
* option, and is now saving elinks.conf again.
* The option should be rewritten to "unset".
* (This does not work yet.)
* In both cases, ELinks has no struct option
* for that name. Possible fixes:
* - If the tree has OPT_AUTOCREATE, then
* assume the user had created that option,
* and rewrite it to "unset". Otherwise,
* keep it.
* - When the user deletes an option, just mark
* it with OPT_DELETED, and keep it in memory
* as long as OPT_TOUCHED is set. */
}
if (!option_types[opt->type].read2) {
show_parse_error(state, ERROR_VALUE);
skip_option_value(&state->pos);
return ERROR_VALUE;
}
val = option_types[opt->type].read2(opt, &state->pos.look,
2008-02-02 17:16:38 -05:00
&state->pos.line);
if (!val) {
/* The reader function failed. Jump back to
* the beginning of the value and skip it with
* the generic code. For the error message,
* use the line number at the beginning of the
* value, because the ending position is not
* interesting if there is an unclosed quote. */
state->pos = pos_before_value;
show_parse_error(state, ERROR_VALUE);
skip_option_value(&state->pos);
return ERROR_VALUE;
}
if (!mirror) {
/* loading a configuration file */
if (!option_types[opt->type].set
|| !option_types[opt->type].set(opt, val)) {
mem_free(val);
return show_parse_error(state, ERROR_VALUE);
}
} else if (is_system_conf) {
/* scanning a file that will not be rewritten */
struct option *flagsite = indirect_option(opt);
if (!(flagsite->flags & OPT_DELETED)
&& option_types[opt->type].equals
&& option_types[opt->type].equals(opt, val))
flagsite->flags &= ~OPT_MUST_SAVE;
else
flagsite->flags |= OPT_MUST_SAVE;
} else {
/* rewriting a configuration file */
struct option *flagsite = indirect_option(opt);
if (flagsite->flags & OPT_DELETED) {
/* Replace the "set" command with an
* "unset" command. */
add_to_string(mirror, "unset ");
add_bytes_to_string(mirror, optname_orig,
optname_len);
state->mirrored = state->pos.look;
} else if (option_types[opt->type].write2) {
add_bytes_to_string(mirror, state->mirrored,
pos_before_value.look
- state->mirrored);
option_types[opt->type].write2(opt, mirror);
state->mirrored = state->pos.look;
}
/* Remember that the option need not be
* written to the end of the file. */
flagsite->flags &= ~OPT_MUST_SAVE;
}
mem_free(val);
}
return ERROR_NONE;
}
static enum parse_error
parse_set_domain(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
return parse_set_common(opt_tree, state, mirror, is_system_conf, 1);
}
static enum parse_error
parse_set(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
return parse_set_common(opt_tree, state, mirror, is_system_conf, 0);
}
static enum parse_error
2008-02-02 17:16:38 -05:00
parse_unset(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
const char *optname_orig;
size_t optname_len;
char *optname_copy;
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
/* Option name */
optname_orig = state->pos.look;
while (is_option_name_char(*state->pos.look)
|| *state->pos.look == '.')
2008-02-02 17:16:38 -05:00
state->pos.look++;
optname_len = state->pos.look - optname_orig;
optname_copy = memacpy(optname_orig, optname_len);
if (!optname_copy) return show_parse_error(state, ERROR_NOMEM);
{
struct option *opt;
opt = get_opt_rec_real(opt_tree, optname_copy);
mem_free(optname_copy);
optname_copy = NULL;
if (!opt || (opt->flags & OPT_HIDDEN)) {
/* The user wanted to delete the option, and
* it has already been deleted; this is not an
* error. This might happen if a version of
* ELinks has a built-in URL rewriting rule,
* the user disables it, and a later version
* no longer has it. */
return ERROR_NONE;
}
if (!mirror) {
/* loading a configuration file */
if (opt->flags & OPT_ALLOC) delete_option(opt);
else mark_option_as_deleted(opt);
} else if (is_system_conf) {
/* scanning a file that will not be rewritten */
struct option *flagsite = indirect_option(opt);
if (flagsite->flags & OPT_DELETED)
flagsite->flags &= ~OPT_MUST_SAVE;
else
flagsite->flags |= OPT_MUST_SAVE;
} else {
/* rewriting a configuration file */
struct option *flagsite = indirect_option(opt);
if (flagsite->flags & OPT_DELETED) {
/* The "unset" command is already in the file,
* and unlike with "set", there is no value
* to be updated. */
} else if (option_types[opt->type].write2) {
/* Replace the "unset" command with a
* "set" command. */
add_to_string(mirror, "set ");
add_bytes_to_string(mirror, optname_orig,
optname_len);
add_to_string(mirror, " = ");
option_types[opt->type].write2(opt, mirror);
state->mirrored = state->pos.look;
}
/* Remember that the option need not be
* written to the end of the file. */
flagsite->flags &= ~OPT_MUST_SAVE;
}
}
return ERROR_NONE;
}
static enum parse_error
2008-02-02 17:16:38 -05:00
parse_bind(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
char *keymap, *keystroke, *action;
enum parse_error err = ERROR_NONE;
struct conf_parsing_pos before_error;
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
/* Keymap */
before_error = state->pos;
keymap = option_types[OPT_STRING].read2(NULL, &state->pos.look,
2008-02-02 17:16:38 -05:00
&state->pos.line);
skip_white(&state->pos);
if (!keymap || !*state->pos.look) {
state->pos = before_error;
return show_parse_error(state, ERROR_PARSE);
}
/* Keystroke */
before_error = state->pos;
keystroke = option_types[OPT_STRING].read2(NULL, &state->pos.look,
2008-02-02 17:16:38 -05:00
&state->pos.line);
skip_white(&state->pos);
if (!keystroke || !*state->pos.look) {
2008-02-02 21:17:21 -05:00
mem_free(keymap); mem_free_if(keystroke);
state->pos = before_error;
return show_parse_error(state, ERROR_PARSE);
}
/* Equal sign */
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (*state->pos.look != '=') {
mem_free(keymap); mem_free(keystroke);
return show_parse_error(state, ERROR_PARSE);
}
2008-02-02 17:16:38 -05:00
state->pos.look++; /* '=' */
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (!*state->pos.look) {
mem_free(keymap); mem_free(keystroke);
return show_parse_error(state, ERROR_PARSE);
}
/* Action */
before_error = state->pos;
action = option_types[OPT_STRING].read2(NULL, &state->pos.look,
2008-02-02 17:16:38 -05:00
&state->pos.line);
if (!action) {
mem_free(keymap); mem_free(keystroke);
state->pos = before_error;
return show_parse_error(state, ERROR_PARSE);
}
if (!mirror) {
/* loading a configuration file */
/* We don't bother to bind() if -default-keys. */
if (!get_cmd_opt_bool("default-keys")
&& bind_do(keymap, keystroke, action, is_system_conf)) {
/* bind_do() tried but failed. */
err = show_parse_error(state, ERROR_VALUE);
} else {
err = ERROR_NONE;
}
} else if (is_system_conf) {
/* scanning a file that will not be rewritten */
/* TODO */
} else {
/* rewriting a configuration file */
/* Mirror what we already have. If the keystroke has
* been unbound, then act_str is simply "none" and
* this does not require special handling. */
char *act_str = bind_act(keymap, keystroke);
if (act_str) {
add_bytes_to_string(mirror, state->mirrored,
before_error.look - state->mirrored);
add_to_string(mirror, act_str);
mem_free(act_str);
state->mirrored = state->pos.look;
} else {
err = show_parse_error(state, ERROR_VALUE);
}
}
mem_free(keymap); mem_free(keystroke); mem_free(action);
return err;
}
2022-01-30 08:32:48 -05:00
static int load_config_file(const char *, const char *, struct option *,
struct string *, int);
static enum parse_error
2008-02-02 17:16:38 -05:00
parse_include(struct option *opt_tree, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
char *fname;
struct string dumbstring;
struct conf_parsing_pos before_error;
if (!init_string(&dumbstring))
return show_parse_error(state, ERROR_NOMEM);
2008-02-02 17:16:38 -05:00
skip_white(&state->pos);
if (!*state->pos.look) {
done_string(&dumbstring);
return show_parse_error(state, ERROR_PARSE);
}
/* File name */
before_error = state->pos;
fname = option_types[OPT_STRING].read2(NULL, &state->pos.look,
2008-02-02 17:16:38 -05:00
&state->pos.line);
if (!fname) {
done_string(&dumbstring);
state->pos = before_error;
return show_parse_error(state, ERROR_PARSE);
}
/* We want load_config_file() to watermark stuff, but not to load
* anything, polluting our beloved options tree - thus, we will feed it
* with some dummy string which we will destroy later; still better
* than cloning whole options tree or polluting interface with another
* rarely-used option ;). */
/* XXX: We should try CONFDIR/<file> when proceeding
* CONFDIR/<otherfile> ;). --pasky */
if (load_config_file(fname[0] == '/' ? (char *) ""
: elinks_home,
fname, opt_tree,
mirror ? &dumbstring : NULL, 1)) {
done_string(&dumbstring);
mem_free(fname);
return show_parse_error(state, ERROR_VALUE);
}
done_string(&dumbstring);
mem_free(fname);
return ERROR_NONE;
}
struct parse_handler {
const char *command;
enum parse_error (*handler)(struct option *opt_tree,
2008-02-02 17:16:38 -05:00
struct conf_parsing_state *state,
struct string *mirror, int is_system_conf);
};
static const struct parse_handler parse_handlers[] = {
{ "set_domain", parse_set_domain },
{ "set", parse_set },
{ "unset", parse_unset },
{ "bind", parse_bind },
{ "include", parse_include },
{ NULL, NULL }
};
static enum parse_error
2008-02-02 17:16:38 -05:00
parse_config_command(struct option *options, struct conf_parsing_state *state,
struct string *mirror, int is_system_conf)
{
const struct parse_handler *handler;
/* If we're mirroring, then everything up to this point must
* have already been mirrored. */
assert(mirror == NULL || state->mirrored == state->pos.look);
if_assert_failed return show_parse_error(state, ERROR_PARSE);
for (handler = parse_handlers; handler->command;
handler++) {
int cmdlen = strlen(handler->command);
2008-02-02 17:16:38 -05:00
if (!strncmp(state->pos.look, handler->command, cmdlen)
&& isspace((unsigned char)state->pos.look[cmdlen])) {
enum parse_error err;
2008-02-02 17:16:38 -05:00
state->pos.look += cmdlen;
err = handler->handler(options, state, mirror,
is_system_conf);
if (mirror) {
/* Mirror any characters that the handler
* consumed but did not already mirror. */
add_bytes_to_string(mirror, state->mirrored,
state->pos.look - state->mirrored);
state->mirrored = state->pos.look;
}
return err;
}
}
return show_parse_error(state, ERROR_COMMAND);
}
enum parse_error
parse_config_exmode_command(char *cmd)