netsurf/utils/nsoption.c
2023-06-17 11:20:09 +01:00

1041 lines
22 KiB
C

/*
* Copyright 2012 Vincent Sanders <vince@netsurf-browser.org>
*
* This file is part of NetSurf, http://www.netsurf-browser.org/
*
* NetSurf 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; version 2 of the License.
*
* NetSurf 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, see <http://www.gnu.org/licenses/>.
*/
/**
* \file
* Option reading and saving (implementation).
*
* Options are stored in the format key:value, one per line.
*
* For bool options, value is "0" or "1".
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "netsurf/inttypes.h"
#include "netsurf/plot_style.h"
#include "utils/errors.h"
#include "utils/log.h"
#include "utils/utils.h"
#include "utils/nsoption.h"
/** Length of buffer used to read lines from input file */
#define NSOPTION_MAX_LINE_LEN 1024
struct nsoption_s *nsoptions = NULL;
struct nsoption_s *nsoptions_default = NULL;
#define NSOPTION_BOOL(NAME, DEFAULT) \
{ #NAME, sizeof(#NAME) - 1, OPTION_BOOL, { .b = DEFAULT } },
#define NSOPTION_STRING(NAME, DEFAULT) \
{ #NAME, sizeof(#NAME) - 1, OPTION_STRING, { .cs = DEFAULT } },
#define NSOPTION_INTEGER(NAME, DEFAULT) \
{ #NAME, sizeof(#NAME) - 1, OPTION_INTEGER, { .i = DEFAULT } },
#define NSOPTION_UINT(NAME, DEFAULT) \
{ #NAME, sizeof(#NAME) - 1, OPTION_UINT, { .u = DEFAULT } },
#define NSOPTION_COLOUR(NAME, DEFAULT) \
{ #NAME, sizeof(#NAME) - 1, OPTION_COLOUR, { .c = DEFAULT } },
/** The table of compiled in default options */
static struct nsoption_s defaults[] = {
#include "desktop/options.h"
#if defined(riscos)
#include "riscos/options.h"
#elif defined(nsgtk)
#include "gtk/options.h"
#elif defined(nsbeos)
#include "beos/options.h"
#elif defined(nsamiga)
#include "amiga/options.h"
#elif defined(nsframebuffer)
#include "framebuffer/options.h"
#elif defined(nsatari)
#include "atari/options.h"
#elif defined(nsmonkey)
#include "monkey/options.h"
#elif defined(nswin32)
#include "windows/options.h"
#endif
{ NULL, 0, OPTION_INTEGER, { 0 } }
};
#undef NSOPTION_BOOL
#undef NSOPTION_STRING
#undef NSOPTION_INTEGER
#undef NSOPTION_UINT
#undef NSOPTION_COLOUR
/**
* Set an option value based on a string
*/
static bool
strtooption(const char *value, struct nsoption_s *option)
{
bool ret = true;
colour rgbcolour; /* RRGGBB */
switch (option->type) {
case OPTION_BOOL:
option->value.b = (value[0] == '1');
break;
case OPTION_INTEGER:
option->value.i = atoi(value);
break;
case OPTION_UINT:
option->value.u = strtoul(value, NULL, 0);
break;
case OPTION_COLOUR:
if (sscanf(value, "%"SCNx32"", &rgbcolour) == 1) {
option->value.c = (((0x000000FF & rgbcolour) << 16) |
((0x0000FF00 & rgbcolour) << 0) |
((0x00FF0000 & rgbcolour) >> 16));
}
break;
case OPTION_STRING:
if (option->value.s != NULL) {
free(option->value.s);
}
if (*value == 0) {
/* do not allow empty strings in text options */
option->value.s = NULL;
} else {
option->value.s = strdup(value);
}
break;
default:
ret = false;
break;
}
return ret;
}
/* validate options to sane values */
static void nsoption_validate(struct nsoption_s *opts, struct nsoption_s *defs)
{
int cloop;
bool black = true;
if (opts[NSOPTION_treeview_font_size].value.i < 50) {
opts[NSOPTION_treeview_font_size].value.i = 50;
}
if (opts[NSOPTION_treeview_font_size].value.i > 1000) {
opts[NSOPTION_treeview_font_size].value.i = 1000;
}
if (opts[NSOPTION_font_size].value.i < 50) {
opts[NSOPTION_font_size].value.i = 50;
}
if (opts[NSOPTION_font_size].value.i > 1000) {
opts[NSOPTION_font_size].value.i = 1000;
}
if (opts[NSOPTION_font_min_size].value.i < 10) {
opts[NSOPTION_font_min_size].value.i = 10;
}
if (opts[NSOPTION_font_min_size].value.i > 500) {
opts[NSOPTION_font_min_size].value.i = 500;
}
if (opts[NSOPTION_memory_cache_size].value.i < 0) {
opts[NSOPTION_memory_cache_size].value.i = 0;
}
/* to aid migration from old, broken, configuration files this
* checks to see if all the system colours are set to black
* and returns them to defaults instead
*/
for (cloop = NSOPTION_SYS_COLOUR_START;
cloop <= NSOPTION_SYS_COLOUR_END;
cloop++) {
if (opts[cloop].value.c != 0) {
black = false;
break;
}
}
if (black == true && defs != NULL) {
for (cloop = NSOPTION_SYS_COLOUR_START;
cloop <= NSOPTION_SYS_COLOUR_END;
cloop++) {
opts[cloop].value.c = defs[cloop].value.c;
}
}
/* To aid migration and ensure that timeouts don't go crazy,
* ensure that (a) we allow at least 1 attempt and
* (b) the total time that we spend should not exceed 60s
*/
if (opts[NSOPTION_max_retried_fetches].value.u == 0)
opts[NSOPTION_max_retried_fetches].value.u = 1;
if (opts[NSOPTION_curl_fetch_timeout].value.u < 5)
opts[NSOPTION_curl_fetch_timeout].value.u = 5;
if (opts[NSOPTION_curl_fetch_timeout].value.u > 60)
opts[NSOPTION_curl_fetch_timeout].value.u = 60;
while (((opts[NSOPTION_curl_fetch_timeout].value.u *
opts[NSOPTION_max_retried_fetches].value.u) > 60) &&
(opts[NSOPTION_max_retried_fetches].value.u > 1))
opts[NSOPTION_max_retried_fetches].value.u--;
/* We ignore the result because we can't fail to validate. Yay */
(void)nslog_set_filter_by_options();
}
/**
* Determines if an option is different between two option tables.
*
* @param opts The first table to compare.
* @param defs The second table to compare.
* @param entry The option to compare.
* @return true if the option differs false if not.
*/
static bool
nsoption_is_set(const struct nsoption_s *opts,
const struct nsoption_s *defs,
const enum nsoption_e entry)
{
bool ret = false;
switch (opts[entry].type) {
case OPTION_BOOL:
if (opts[entry].value.b != defs[entry].value.b) {
ret = true;
}
break;
case OPTION_INTEGER:
if (opts[entry].value.i != defs[entry].value.i) {
ret = true;
}
break;
case OPTION_UINT:
if (opts[entry].value.u != defs[entry].value.u) {
ret = true;
}
break;
case OPTION_COLOUR:
if (opts[entry].value.c != defs[entry].value.c) {
ret = true;
}
break;
case OPTION_STRING:
/* set if:
* - defs is null.
* - default is null but value is not.
* - default and value pointers are different
* (acts as a null check because of previous check)
* and the strings content differ.
*/
if (((defs[entry].value.s == NULL) &&
(opts[entry].value.s != NULL)) ||
((defs[entry].value.s != NULL) &&
(opts[entry].value.s == NULL)) ||
((defs[entry].value.s != opts[entry].value.s) &&
(strcmp(opts[entry].value.s, defs[entry].value.s) != 0))) {
ret = true;
}
break;
}
return ret;
}
/**
* Output choices to file stream
*
* @param fp The file stream to write to.
* @param opts The options table to write.
* @param defs The default value table to compare with.
* @param all Output all entries not just ones changed from defaults
*/
static nserror
nsoption_output(FILE *fp,
struct nsoption_s *opts,
struct nsoption_s *defs,
bool all)
{
unsigned int entry; /* index to option being output */
colour rgbcolour; /* RRGGBB */
for (entry = 0; entry < NSOPTION_LISTEND; entry++) {
if ((all == false) &&
(nsoption_is_set(opts, defs, entry) == false)) {
continue;
}
switch (opts[entry].type) {
case OPTION_BOOL:
fprintf(fp, "%s:%c\n",
opts[entry].key,
opts[entry].value.b ? '1' : '0');
break;
case OPTION_INTEGER:
fprintf(fp, "%s:%i\n",
opts[entry].key,
opts[entry].value.i);
break;
case OPTION_UINT:
fprintf(fp, "%s:%u\n",
opts[entry].key,
opts[entry].value.u);
break;
case OPTION_COLOUR:
rgbcolour = (((0x000000FF & opts[entry].value.c) << 16) |
((0x0000FF00 & opts[entry].value.c) << 0) |
((0x00FF0000 & opts[entry].value.c) >> 16));
fprintf(fp, "%s:%06"PRIx32"\n",
opts[entry].key,
rgbcolour);
break;
case OPTION_STRING:
fprintf(fp, "%s:%s\n",
opts[entry].key,
((opts[entry].value.s == NULL) ||
(*opts[entry].value.s == 0)) ? "" : opts[entry].value.s);
break;
}
}
return NSERROR_OK;
}
/**
* Output an option value into a string, in HTML format.
*
* @param option The option to output the value of.
* @param size The size of the string buffer.
* @param pos The current position in string
* @param string The string in which to output the value.
* @return The number of bytes written to string or -1 on error
*/
static size_t
nsoption_output_value_html(struct nsoption_s *option,
size_t size,
size_t pos,
char *string)
{
size_t slen = 0; /* length added to string */
colour rgbcolour; /* RRGGBB */
switch (option->type) {
case OPTION_BOOL:
slen = snprintf(string + pos,
size - pos,
"%s",
option->value.b ? "true" : "false");
break;
case OPTION_INTEGER:
slen = snprintf(string + pos,
size - pos,
"%i",
option->value.i);
break;
case OPTION_UINT:
slen = snprintf(string + pos,
size - pos,
"%u",
option->value.u);
break;
case OPTION_COLOUR:
rgbcolour = colour_rb_swap(option->value.c);
slen = snprintf(string + pos,
size - pos,
"<span style=\"font-family:Monospace;\">"
"#%06"PRIX32
"</span> "
"<span style=\"background-color: #%06"PRIx32"; "
"border: 1px solid #%06"PRIx32"; "
"display: inline-block; "
"width: 1em; height: 1em;\">"
"</span>",
rgbcolour,
rgbcolour,
colour_to_bw_furthest(rgbcolour));
break;
case OPTION_STRING:
if (option->value.s != NULL) {
slen = snprintf(string + pos, size - pos, "%s",
option->value.s);
} else {
slen = snprintf(string + pos, size - pos,
"<span class=\"null-content\">NULL"
"</span>");
}
break;
}
return slen;
}
/**
* Output an option value into a string, in plain text format.
*
* @param option The option to output the value of.
* @param size The size of the string buffer.
* @param pos The current position in string
* @param string The string in which to output the value.
* @return The number of bytes written to string or -1 on error
*/
static size_t
nsoption_output_value_text(struct nsoption_s *option,
size_t size,
size_t pos,
char *string)
{
size_t slen = 0; /* length added to string */
colour rgbcolour; /* RRGGBB */
switch (option->type) {
case OPTION_BOOL:
slen = snprintf(string + pos,
size - pos,
"%c",
option->value.b ? '1' : '0');
break;
case OPTION_INTEGER:
slen = snprintf(string + pos,
size - pos,
"%i",
option->value.i);
break;
case OPTION_UINT:
slen = snprintf(string + pos,
size - pos,
"%u",
option->value.u);
break;
case OPTION_COLOUR:
rgbcolour = (((0x000000FF & option->value.c) << 16) |
((0x0000FF00 & option->value.c) << 0) |
((0x00FF0000 & option->value.c) >> 16));
slen = snprintf(string + pos, size - pos, "%06"PRIx32, rgbcolour);
break;
case OPTION_STRING:
if (option->value.s != NULL) {
slen = snprintf(string + pos,
size - pos,
"%s",
option->value.s);
}
break;
}
return slen;
}
/**
* Duplicates an option table.
*
* Allocates a new option table and copies an existing one into it.
*
* \param[in] src The source table to copy
* \param[out] pdst The output table
* \return NSERROR_OK on success or appropriate error code.
*/
static nserror
nsoption_dup(struct nsoption_s *src, struct nsoption_s **pdst)
{
struct nsoption_s *dst;
dst = malloc(sizeof(defaults));
if (dst == NULL) {
return NSERROR_NOMEM;
}
*pdst = dst;
/* copy the source table into the destination table */
memcpy(dst, src, sizeof(defaults));
while (src->key != NULL) {
if ((src->type == OPTION_STRING) &&
(src->value.s != NULL)) {
dst->value.s = strdup(src->value.s);
}
src++;
dst++;
}
return NSERROR_OK;
}
/**
* frees an option table.
*
* Iterates through an option table a freeing resources as required
* finally freeing the option table itself.
*
* @param opts The option table to free.
*/
static nserror
nsoption_free(struct nsoption_s *opts)
{
struct nsoption_s *cur; /* option being freed */
if (opts == NULL) {
return NSERROR_BAD_PARAMETER;
}
cur = opts;
while (cur->key != NULL) {
if ((cur->type == OPTION_STRING) && (cur->value.s != NULL)) {
free(cur->value.s);
}
cur++;
}
free(opts);
return NSERROR_OK;
}
/**
* extract key/value from a line of input
*
* \retun NSERROR_OK and key_out and value_out updated
* NSERROR_NOT_FOUND if not a key/value input line
* NSERROR_INVALID if the line is and invalid format (missing colon)
*/
static nserror
get_key_value(char *line, int linelen, char **key_out, char **value_out)
{
char *key;
char *value;
/* skip leading whitespace for start of key */
for (key = line; *key != 0; key++) {
if ((*key != ' ') && (*key != '\t') && (*key != '\n')) {
break;
}
}
/* empty line or only whitespace */
if (*key == 0) {
return NSERROR_NOT_FOUND;
}
/* comment */
if (*key == '#') {
return NSERROR_NOT_FOUND;
}
/* get start of value */
for (value = key; *value != 0; value++) {
if (*value == ':') {
*value = 0;
value++;
break;
}
}
/* missing colon separator */
if (*value == 0) {
return NSERROR_INVALID;
}
/* remove delimiter from value */
if (line[linelen - 1] == '\n') {
linelen--;
line[linelen] = 0;
}
*key_out = key;
*value_out = value;
return NSERROR_OK;
}
/**
* Process a line from a user option file
*/
static nserror optionline(struct nsoption_s *opts, char *line, int linelen)
{
nserror res;
char *key;
char *value;
int idx;
res = get_key_value(line, linelen, &key, &value);
if (res != NSERROR_OK) {
/* skip line as no valid key value pair found */
return res;
}
for (idx = 0; opts[idx].key != NULL; idx++) {
if (strcasecmp(key, opts[idx].key) == 0) {
strtooption(value, &opts[idx]);
break;
}
}
return res;
}
/* exported interface documented in utils/nsoption.h */
nserror
nsoption_init(nsoption_set_default_t *set_defaults,
struct nsoption_s **popts,
struct nsoption_s **pdefs)
{
nserror ret;
struct nsoption_s *defs;
struct nsoption_s *opts;
ret = nsoption_dup(&defaults[0], &defs);
if (ret != NSERROR_OK) {
return ret;
}
/* update the default table */
if (set_defaults != NULL) {
/** @todo it would be better if the frontends actually
* set values in the passed in table instead of
* assuming the global one.
*/
opts = nsoptions;
nsoptions = defs;
ret = set_defaults(defs);
if (ret != NSERROR_OK) {
nsoptions = opts;
nsoption_free(defs);
return ret;
}
}
/* copy the default values into the working set */
ret = nsoption_dup(defs, &opts);
if (ret != NSERROR_OK) {
nsoption_free(defs);
return ret;
}
/* return values if wanted */
if (popts != NULL) {
*popts = opts;
} else {
nsoptions = opts;
}
if (pdefs != NULL) {
*pdefs = defs;
} else {
nsoptions_default = defs;
}
return NSERROR_OK;
}
/* exported interface documented in utils/nsoption.h */
nserror nsoption_finalise(struct nsoption_s *opts, struct nsoption_s *defs)
{
nserror res;
/* check to see if global table selected */
if (opts == NULL) {
res = nsoption_free(nsoptions);
if (res == NSERROR_OK) {
nsoptions = NULL;
}
} else {
res = nsoption_free(opts);
}
if (res != NSERROR_OK) {
return res;
}
/* check to see if global table selected */
if (defs == NULL) {
res = nsoption_free(nsoptions_default);
if (res == NSERROR_OK) {
nsoptions_default = NULL;
}
} else {
res = nsoption_free(defs);
}
return res;
}
/* exported interface documented in utils/nsoption.h */
nserror
nsoption_read(const char *path, struct nsoption_s *opts)
{
char s[NSOPTION_MAX_LINE_LEN];
FILE *fp;
struct nsoption_s *defs;
if (path == NULL) {
return NSERROR_BAD_PARAMETER;
}
/* check to see if global table selected */
if (opts == NULL) {
opts = nsoptions;
}
/**
* @todo is this an API bug not being a parameter
*/
defs = nsoptions_default;
if ((opts == NULL) || (defs == NULL)) {
return NSERROR_BAD_PARAMETER;
}
fp = fopen(path, "r");
if (!fp) {
NSLOG(netsurf, INFO, "Failed to open file '%s'", path);
return NSERROR_NOT_FOUND;
}
NSLOG(netsurf, INFO, "Successfully opened '%s' for Options file", path);
while (fgets(s, NSOPTION_MAX_LINE_LEN, fp)) {
optionline(opts, s, strlen(s));
}
fclose(fp);
nsoption_validate(opts, defs);
return NSERROR_OK;
}
/* exported interface documented in utils/nsoption.h */
nserror
nsoption_write(const char *path,
struct nsoption_s *opts,
struct nsoption_s *defs)
{
FILE *fp;
nserror ret;
if (path == NULL) {
return NSERROR_BAD_PARAMETER;
}
/* check to see if global table selected */
if (opts == NULL) {
opts = nsoptions;
}
/* check to see if global table selected */
if (defs == NULL) {
defs = nsoptions_default;
}
if ((opts == NULL) || (defs == NULL)) {
return NSERROR_BAD_PARAMETER;
}
fp = fopen(path, "w");
if (!fp) {
NSLOG(netsurf, INFO, "failed to open file '%s' for writing",
path);
return NSERROR_NOT_FOUND;
}
ret = nsoption_output(fp, opts, defs, false);
fclose(fp);
return ret;
}
/* exported interface documented in utils/nsoption.h */
nserror
nsoption_dump(FILE *outf, struct nsoption_s *opts)
{
if (outf == NULL) {
return NSERROR_BAD_PARAMETER;
}
/* check to see if global table selected and available */
if (opts == NULL) {
opts = nsoptions;
}
if (opts == NULL) {
return NSERROR_BAD_PARAMETER;
}
return nsoption_output(outf, opts, NULL, true);
}
/* exported interface documented in utils/nsoption.h */
nserror
nsoption_commandline(int *pargc, char **argv, struct nsoption_s *opts)
{
char *arg;
char *val;
int arglen;
int idx = 1;
int mv_loop;
unsigned int entry_loop;
if ((pargc == NULL) || (argv == NULL)) {
return NSERROR_BAD_PARAMETER;
}
/* check to see if global table selected and available */
if (opts == NULL) {
opts = nsoptions;
}
if (opts == NULL) {
return NSERROR_BAD_PARAMETER;
}
while (idx < *pargc) {
arg = argv[idx];
arglen = strlen(arg);
/* check we have an option */
/* option must start -- and be as long as the shortest option*/
if ((arglen < (2+5) ) || (arg[0] != '-') || (arg[1] != '-'))
break;
arg += 2; /* skip -- */
val = strchr(arg, '=');
if (val == NULL) {
/* no equals sign - next parameter is val */
idx++;
if (idx >= *pargc)
break;
val = argv[idx];
} else {
/* equals sign */
arglen = val - arg ;
val++;
}
/* arg+arglen is the option to set, val is the value */
NSLOG(netsurf, INFO, "%.*s = %s", arglen, arg, val);
for (entry_loop = 0;
entry_loop < NSOPTION_LISTEND;
entry_loop++) {
if (strncmp(arg, opts[entry_loop].key, arglen) == 0) {
strtooption(val, opts + entry_loop);
break;
}
}
idx++;
}
/* remove processed options from argv */
for (mv_loop=0; mv_loop < (*pargc - idx); mv_loop++) {
argv[mv_loop + 1] = argv[mv_loop + idx];
}
*pargc -= (idx - 1);
nsoption_validate(opts, nsoptions_default);
return NSERROR_OK;
}
/* exported interface documented in options.h */
int
nsoption_snoptionf(char *string,
size_t size,
enum nsoption_e option_idx,
const char *fmt)
{
size_t slen = 0; /* current output string length */
int fmtc = 0; /* current index into format string */
struct nsoption_s *option;
if (fmt == NULL) {
return -1;
}
if (option_idx >= NSOPTION_LISTEND) {
return -1;
}
if (nsoptions == NULL) {
return -1;
}
option = &nsoptions[option_idx]; /* assume the global table */
if (option == NULL || option->key == NULL) {
return -1;
}
while ((slen < size) && (fmt[fmtc] != 0)) {
if (fmt[fmtc] == '%') {
fmtc++;
switch (fmt[fmtc]) {
case 'k':
slen += snprintf(string + slen,
size - slen,
"%s",
option->key);
break;
case 'p':
if (nsoption_is_set(nsoptions,
nsoptions_default,
option_idx)) {
slen += snprintf(string + slen,
size - slen,
"user");
} else {
slen += snprintf(string + slen,
size - slen,
"default");
}
break;
case 't':
switch (option->type) {
case OPTION_BOOL:
slen += snprintf(string + slen,
size - slen,
"boolean");
break;
case OPTION_INTEGER:
slen += snprintf(string + slen,
size - slen,
"integer");
break;
case OPTION_UINT:
slen += snprintf(string + slen,
size - slen,
"unsigned integer");
break;
case OPTION_COLOUR:
slen += snprintf(string + slen,
size - slen,
"colour");
break;
case OPTION_STRING:
slen += snprintf(string + slen,
size - slen,
"string");
break;
}
break;
case 'V':
slen += nsoption_output_value_html(option,
size,
slen,
string);
break;
case 'v':
slen += nsoption_output_value_text(option,
size,
slen,
string);
break;
}
fmtc++;
} else {
string[slen] = fmt[fmtc];
slen++;
fmtc++;
}
}
/* Ensure that we NUL-terminate the output */
string[min(slen, size - 1)] = '\0';
return slen;
}
/* exported interface documented in options.h */
nserror
nsoption_set_tbl_charp(struct nsoption_s *opts,
enum nsoption_e option_idx,
char *s)
{
struct nsoption_s *option;
option = &opts[option_idx];
/* ensure it is a string option */
if (option->type != OPTION_STRING) {
return NSERROR_BAD_PARAMETER;
}
/* free any existing string */
if (option->value.s != NULL) {
free(option->value.s);
}
option->value.s = s;
/* check for empty string */
if ((option->value.s != NULL) && (*option->value.s == 0)) {
free(option->value.s);
option->value.s = NULL;
}
return NSERROR_OK;
}