options: rewrite config file parser

The format doesn't change. Some details are different, though. For
example, it will now accept option values with spaces even if they're
not quoted. (I see no reason why the user should be forced to add
quotes.)

The code is now smaller and should be much easier to extend. It also
can load config from in-memory buffers, which might be helpful in the
future.

read_file() should eventually be replaced with stream_read_complete().
But since the latter function may access options under various
circumstances, and also needs access to the mpv_global struct, there
is a separate implementation for now.
This commit is contained in:
wm4 2015-04-04 00:02:34 +02:00
parent bf3e0bc1da
commit 9ea0590371
2 changed files with 165 additions and 225 deletions

View File

@ -42,6 +42,8 @@ static const union m_option_value default_value;
// Profiles allow to predefine some sets of options that can then
// be applied later on with the internal -profile option.
#define MAX_PROFILE_DEPTH 20
// Maximal include depth.
#define MAX_RECURSION_DEPTH 8
struct m_profile {
struct m_profile *next;
@ -66,8 +68,14 @@ static int parse_include(struct m_config *config, struct bstr param, bool set,
return M_OPT_MISSING_PARAM;
if (!set)
return 1;
if (config->recursion_depth >= MAX_RECURSION_DEPTH) {
MP_ERR(config, "Maximum 'include' nesting depth exceeded.\n");
return M_OPT_INVALID;
}
char *filename = bstrdup0(NULL, param);
config->recursion_depth += 1;
config->includefunc(config->includefunc_ctx, filename, flags);
config->recursion_depth -= 1;
talloc_free(filename);
return 1;
}

View File

@ -33,242 +33,174 @@
#include "m_option.h"
#include "m_config.h"
/// Maximal include depth.
#define MAX_RECURSION_DEPTH 8
// Skip whitespace and comments (assuming there are no line breaks)
static bool skip_ws(bstr *s)
{
*s = bstr_lstrip(*s);
if (bstr_startswith0(*s, "#"))
s->len = 0;
return s->len;
}
static int m_config_parse(m_config_t *config, const char *location, bstr data,
char *initial_section, int flags)
{
m_profile_t *profile = m_config_add_profile(config, initial_section);
void *tmp = talloc_new(NULL);
int line_no = 0;
int errors = 0;
bstr_eatstart0(&data, "\xEF\xBB\xBF"); // skip BOM
while (data.len) {
talloc_free_children(tmp);
bool ok = false;
line_no++;
char loc[512];
snprintf(loc, sizeof(loc), "%s:%d:", location, line_no);
bstr line = bstr_strip_linebreaks(bstr_getline(data, &data));
if (!skip_ws(&line))
continue;
// Profile declaration
if (bstr_eatstart0(&line, "[")) {
bstr profilename;
if (!bstr_split_tok(line, "]", &profilename, &line)) {
MP_ERR(config, "%s missing closing ]\n", loc);
goto error;
}
if (skip_ws(&line)) {
MP_ERR(config, "%s unparseable extra characters: '%.*s'\n",
loc, BSTR_P(line));
goto error;
}
profile = m_config_add_profile(config, bstrto0(tmp, profilename));
continue;
}
bstr_eatstart0(&line, "--");
bstr option = line;
while (line.len && (mp_isalnum(line.start[0]) || line.start[0] == '_' ||
line.start[0] == '-'))
line = bstr_cut(line, 1);
option.len = option.len - line.len;
skip_ws(&line);
bstr value = {0};
if (bstr_eatstart0(&line, "=")) {
skip_ws(&line);
if (line.len && (line.start[0] == '"' || line.start[0] == '\'')) {
// Simple quoting, like "value"
char term[2] = {line.start[0], 0};
line = bstr_cut(line, 1);
if (!bstr_split_tok(line, term, &value, &line)) {
MP_ERR(config, "%s unterminated quote\n", loc);
goto error;
}
} else if (bstr_eatstart0(&line, "%")) {
// Quoting with length, like %5%value
bstr rest;
long long len = bstrtoll(line, &rest, 10);
if (rest.len == line.len || !bstr_eatstart0(&rest, "%") ||
len > rest.len)
{
MP_ERR(config, "%s broken escaping with '%%'\n", loc);
goto error;
}
value = bstr_splice(line, 0, len);
line = bstr_cut(line, len);
} else {
// No quoting; take everything until the comment or end of line
int end = bstrchr(line, '#');
value = bstr_strip(end < 0 ? line : bstr_splice(line, 0, end));
line.len = 0;
}
}
if (skip_ws(&line)) {
MP_ERR(config, "%s unparseable extra characters: '%.*s'\n",
loc, BSTR_P(line));
goto error;
}
int res;
if (profile) {
if (bstr_equals0(option, "profile-desc")) {
m_profile_set_desc(profile, value);
res = 0;
} else {
res = m_config_set_profile_option(config, profile, option, value);
}
} else {
res = m_config_set_option_ext(config, option, value, flags);
}
if (res < 0) {
MP_ERR(config, "%s setting option %.*s='%.*s' failed.\n",
loc, BSTR_P(option), BSTR_P(value));
goto error;
}
ok = true;
error:
if (!ok)
errors++;
if (errors > 16) {
MP_ERR(config, "%s: too many errors, stopping.\n", location);
break;
}
}
talloc_free(tmp);
return 1;
}
static bstr read_file(struct mp_log *log, const char *filename)
{
FILE *f = fopen(filename, "rb");
if (!f) {
mp_verbose(log, "Can't open config file: %s\n", mp_strerror(errno));
return (bstr){0};
}
char *data = talloc_array(NULL, char, 0);
size_t size = 0;
while (1) {
size_t left = talloc_get_size(data) - size;
if (!left) {
MP_TARRAY_GROW(NULL, data, size + 1);
continue;
}
size_t s = fread(data + size, 1, left, f);
if (!s) {
if (ferror(f))
mp_err(log, "Error reading config file.\n");
fclose(f);
MP_TARRAY_APPEND(NULL, data, size, 0);
return (bstr){data, size - 1};
}
size += s;
}
assert(0);
}
// Load options and profiles from from a config file.
// conffile: path to the config file
// initial_section: default section where to add normal options
// flags: M_SETOPT_* bits
// returns: 1 on sucess, -1 on error, 0 if file not accessible.
// returns: 1 on success, -1 on error, 0 if file not accessible.
int m_config_parse_config_file(m_config_t *config, const char *conffile,
char *initial_section, int flags)
{
#define PRINT_LINENUM MP_ERR(config, "%s:%d: ", conffile, line_num)
#define MAX_LINE_LEN 10000
#define MAX_OPT_LEN 1000
#define MAX_PARAM_LEN 1500
FILE *fp = NULL;
char *line = NULL;
char opt[MAX_OPT_LEN + 1];
char param[MAX_PARAM_LEN + 1];
char c; /* for the "" and '' check */
int tmp;
int line_num = 0;
int line_pos; /* line pos */
int opt_pos; /* opt pos */
int param_pos; /* param pos */
int ret = 1;
int errors = 0;
m_profile_t *profile = m_config_add_profile(config, initial_section);
flags = flags | M_SETOPT_FROM_CONFIG_FILE;
MP_VERBOSE(config, "Reading config file %s\n", conffile);
if (config->recursion_depth > MAX_RECURSION_DEPTH) {
MP_ERR(config, "Maximum 'include' nesting depth exceeded.\n");
ret = -1;
goto out;
}
bstr data = read_file(config->log, conffile);
if (!data.start)
return 0;
if ((line = malloc(MAX_LINE_LEN + 1)) == NULL) {
ret = -1;
goto out;
} else
MP_VERBOSE(config, "\n");
if ((fp = fopen(conffile, "r")) == NULL) {
MP_VERBOSE(config, "Can't open config file: %s\n", mp_strerror(errno));
ret = 0;
goto out;
}
while (fgets(line, MAX_LINE_LEN, fp)) {
if (errors >= 16) {
MP_FATAL(config, "too many errors\n");
goto out;
}
line_num++;
line_pos = 0;
/* skip BOM */
if (strncmp(line, "\xEF\xBB\xBF", 3) == 0)
line_pos += 3;
/* skip whitespaces */
while (mp_isspace(line[line_pos]))
++line_pos;
/* EOL / comment */
if (line[line_pos] == '\0' || line[line_pos] == '#')
continue;
/* read option. */
for (opt_pos = 0; mp_isprint(line[line_pos]) &&
line[line_pos] != ' ' &&
line[line_pos] != '#' &&
line[line_pos] != '='; /* NOTHING */) {
opt[opt_pos++] = line[line_pos++];
if (opt_pos >= MAX_OPT_LEN) {
PRINT_LINENUM;
MP_ERR(config, "option name too long\n");
errors++;
ret = -1;
goto nextline;
}
}
if (opt_pos == 0) {
PRINT_LINENUM;
MP_ERR(config, "parse error\n");
ret = -1;
errors++;
continue;
}
opt[opt_pos] = '\0';
/* Profile declaration */
if (opt_pos > 2 && opt[0] == '[' && opt[opt_pos - 1] == ']') {
opt[opt_pos - 1] = '\0';
profile = m_config_add_profile(config, opt + 1);
continue;
}
/* skip whitespaces */
while (mp_isspace(line[line_pos]))
++line_pos;
param_pos = 0;
bool param_set = false;
/* check '=' */
if (line[line_pos] == '=') {
line_pos++;
param_set = true;
/* whitespaces... */
while (mp_isspace(line[line_pos]))
++line_pos;
/* read the parameter */
if (line[line_pos] == '"' || line[line_pos] == '\'') {
c = line[line_pos];
++line_pos;
for (param_pos = 0; line[line_pos] != c; /* NOTHING */) {
if (!line[line_pos]) {
PRINT_LINENUM;
MP_ERR(config, "unterminated quotes\n");
ret = -1;
errors++;
goto nextline;
}
param[param_pos++] = line[line_pos++];
if (param_pos >= MAX_PARAM_LEN) {
PRINT_LINENUM;
MP_ERR(config, "option %s has a too long parameter\n", opt);
ret = -1;
errors++;
goto nextline;
}
}
line_pos++; /* skip the closing " or ' */
goto param_done;
}
if (line[line_pos] == '%') {
char *start = &line[line_pos + 1];
char *end = start;
unsigned long len = strtoul(start, &end, 10);
if (start != end && end[0] == '%') {
if (len >= MAX_PARAM_LEN - 1 ||
strlen(end + 1) < len)
{
PRINT_LINENUM;
MP_ERR(config, "bogus %% length\n");
ret = -1;
errors++;
goto nextline;
}
param_pos = snprintf(param, sizeof(param), "%.*s",
(int)len, end + 1);
line_pos += 1 + (end - start) + 1 + len;
goto param_done;
}
}
for (param_pos = 0; mp_isprint(line[line_pos])
&& !mp_isspace(line[line_pos])
&& line[line_pos] != '#'; /* NOTHING */) {
param[param_pos++] = line[line_pos++];
if (param_pos >= MAX_PARAM_LEN) {
PRINT_LINENUM;
MP_ERR(config, "too long parameter\n");
ret = -1;
errors++;
goto nextline;
}
}
param_done:
while (mp_isspace(line[line_pos]))
++line_pos;
}
param[param_pos] = '\0';
/* EOL / comment */
if (line[line_pos] != '\0' && line[line_pos] != '#') {
PRINT_LINENUM;
MP_ERR(config, "extra characters: %s\n", line + line_pos);
ret = -1;
}
bstr bopt = bstr0(opt);
bstr bparam = bstr0(param);
if (bopt.len >= 3)
bstr_eatstart0(&bopt, "--");
if (profile && bstr_equals0(bopt, "profile-desc")) {
m_profile_set_desc(profile, bparam);
goto nextline;
}
bool need_param = m_config_option_requires_param(config, bopt) > 0;
if (need_param && !param_set) {
PRINT_LINENUM;
MP_ERR(config, "error parsing option %.*s=%.*s: %s\n",
BSTR_P(bopt), BSTR_P(bparam),
m_option_strerror(M_OPT_MISSING_PARAM));
continue;
}
if (profile) {
tmp = m_config_set_profile_option(config, profile, bopt, bparam);
} else {
tmp = m_config_set_option_ext(config, bopt, bparam, flags);
}
if (tmp < 0) {
PRINT_LINENUM;
MP_ERR(config, "setting option %.*s='%.*s' failed.\n",
BSTR_P(bopt), BSTR_P(bparam));
continue;
/* break */
}
nextline:
;
}
out:
free(line);
if (fp)
fclose(fp);
config->recursion_depth -= 1;
if (ret < 0) {
MP_FATAL(config, "Error loading config file %s.\n",
conffile);
}
return ret;
int r = m_config_parse(config, conffile, data, initial_section, flags);
talloc_free(data.start);
return r;
}