Systemd/src/basic/env-file.c
Lennart Poettering de008e537d env-file: bring our decoding of double-quoted strings in env files in line with shell
In shell, inside of double quotes only a select few chars should be
escaped. If other chars are escaped this has no effect. Correct the list
of chars that need such escaping.

Also, make sure we can read back the stuff we wrote out without loss.

Fixes: #16788
2020-09-14 11:08:43 +02:00

568 lines
17 KiB
C

/* SPDX-License-Identifier: LGPL-2.1+ */
#include "alloc-util.h"
#include "env-file.h"
#include "env-util.h"
#include "escape.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
#include "string-util.h"
#include "strv.h"
#include "tmpfile-util.h"
#include "utf8.h"
static int parse_env_file_internal(
FILE *f,
const char *fname,
int (*push) (const char *filename, unsigned line,
const char *key, char *value, void *userdata, int *n_pushed),
void *userdata,
int *n_pushed) {
size_t key_alloc = 0, n_key = 0, value_alloc = 0, n_value = 0, last_value_whitespace = (size_t) -1, last_key_whitespace = (size_t) -1;
_cleanup_free_ char *contents = NULL, *key = NULL, *value = NULL;
unsigned line = 1;
char *p;
int r;
enum {
PRE_KEY,
KEY,
PRE_VALUE,
VALUE,
VALUE_ESCAPE,
SINGLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE_ESCAPE,
COMMENT,
COMMENT_ESCAPE
} state = PRE_KEY;
if (f)
r = read_full_stream(f, &contents, NULL);
else
r = read_full_file(fname, &contents, NULL);
if (r < 0)
return r;
for (p = contents; *p; p++) {
char c = *p;
switch (state) {
case PRE_KEY:
if (strchr(COMMENTS, c))
state = COMMENT;
else if (!strchr(WHITESPACE, c)) {
state = KEY;
last_key_whitespace = (size_t) -1;
if (!GREEDY_REALLOC(key, key_alloc, n_key+2))
return -ENOMEM;
key[n_key++] = c;
}
break;
case KEY:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
n_key = 0;
} else if (c == '=') {
state = PRE_VALUE;
last_value_whitespace = (size_t) -1;
} else {
if (!strchr(WHITESPACE, c))
last_key_whitespace = (size_t) -1;
else if (last_key_whitespace == (size_t) -1)
last_key_whitespace = n_key;
if (!GREEDY_REALLOC(key, key_alloc, n_key+2))
return -ENOMEM;
key[n_key++] = c;
}
break;
case PRE_VALUE:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
key[n_key] = 0;
if (value)
value[n_value] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != (size_t) -1)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata, n_pushed);
if (r < 0)
return r;
n_key = 0;
value = NULL;
value_alloc = n_value = 0;
} else if (c == '\'')
state = SINGLE_QUOTE_VALUE;
else if (c == '"')
state = DOUBLE_QUOTE_VALUE;
else if (c == '\\')
state = VALUE_ESCAPE;
else if (!strchr(WHITESPACE, c)) {
state = VALUE;
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case VALUE:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
key[n_key] = 0;
if (value)
value[n_value] = 0;
/* Chomp off trailing whitespace from value */
if (last_value_whitespace != (size_t) -1)
value[last_value_whitespace] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != (size_t) -1)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata, n_pushed);
if (r < 0)
return r;
n_key = 0;
value = NULL;
value_alloc = n_value = 0;
} else if (c == '\\') {
state = VALUE_ESCAPE;
last_value_whitespace = (size_t) -1;
} else {
if (!strchr(WHITESPACE, c))
last_value_whitespace = (size_t) -1;
else if (last_value_whitespace == (size_t) -1)
last_value_whitespace = n_value;
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case VALUE_ESCAPE:
state = VALUE;
if (!strchr(NEWLINE, c)) {
/* Escaped newlines we eat up entirely */
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case SINGLE_QUOTE_VALUE:
if (c == '\'')
state = PRE_VALUE;
else {
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case DOUBLE_QUOTE_VALUE:
if (c == '"')
state = PRE_VALUE;
else if (c == '\\')
state = DOUBLE_QUOTE_VALUE_ESCAPE;
else {
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case DOUBLE_QUOTE_VALUE_ESCAPE:
state = DOUBLE_QUOTE_VALUE;
if (strchr(SHELL_NEED_ESCAPE, c)) {
/* If this is a char that needs escaping, just unescape it. */
if (!GREEDY_REALLOC(value, value_alloc, n_value+2))
return -ENOMEM;
value[n_value++] = c;
} else if (c != '\n') {
/* If other char than what needs escaping, keep the "\" in place, like the
* real shell does. */
if (!GREEDY_REALLOC(value, value_alloc, n_value+3))
return -ENOMEM;
value[n_value++] = '\\';
value[n_value++] = c;
}
/* Escaped newlines (aka "continuation lines") are eaten up entirely */
break;
case COMMENT:
if (c == '\\')
state = COMMENT_ESCAPE;
else if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
}
break;
case COMMENT_ESCAPE:
state = COMMENT;
break;
}
}
if (IN_SET(state,
PRE_VALUE,
VALUE,
VALUE_ESCAPE,
SINGLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE_ESCAPE)) {
key[n_key] = 0;
if (value)
value[n_value] = 0;
if (state == VALUE)
if (last_value_whitespace != (size_t) -1)
value[last_value_whitespace] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != (size_t) -1)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata, n_pushed);
if (r < 0)
return r;
value = NULL;
}
return 0;
}
static int check_utf8ness_and_warn(
const char *filename, unsigned line,
const char *key, char *value) {
if (!utf8_is_valid(key)) {
_cleanup_free_ char *p = NULL;
p = utf8_escape_invalid(key);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s:%u: invalid UTF-8 in key '%s', ignoring.",
strna(filename), line, p);
}
if (value && !utf8_is_valid(value)) {
_cleanup_free_ char *p = NULL;
p = utf8_escape_invalid(value);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.",
strna(filename), line, key, p);
}
return 0;
}
static int parse_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata,
int *n_pushed) {
const char *k;
va_list aq, *ap = userdata;
int r;
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
va_copy(aq, *ap);
while ((k = va_arg(aq, const char *))) {
char **v;
v = va_arg(aq, char **);
if (streq(key, k)) {
va_end(aq);
free(*v);
*v = value;
if (n_pushed)
(*n_pushed)++;
return 1;
}
}
va_end(aq);
free(value);
return 0;
}
int parse_env_filev(
FILE *f,
const char *fname,
va_list ap) {
int r, n_pushed = 0;
va_list aq;
va_copy(aq, ap);
r = parse_env_file_internal(f, fname, parse_env_file_push, &aq, &n_pushed);
va_end(aq);
if (r < 0)
return r;
return n_pushed;
}
int parse_env_file_sentinel(
FILE *f,
const char *fname,
...) {
va_list ap;
int r;
va_start(ap, fname);
r = parse_env_filev(f, fname, ap);
va_end(ap);
return r;
}
static int load_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata,
int *n_pushed) {
char ***m = userdata;
char *p;
int r;
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
p = strjoin(key, "=", value);
if (!p)
return -ENOMEM;
r = strv_env_replace(m, p);
if (r < 0) {
free(p);
return r;
}
if (n_pushed)
(*n_pushed)++;
free(value);
return 0;
}
int load_env_file(FILE *f, const char *fname, char ***rl) {
char **m = NULL;
int r;
r = parse_env_file_internal(f, fname, load_env_file_push, &m, NULL);
if (r < 0) {
strv_free(m);
return r;
}
*rl = m;
return 0;
}
static int load_env_file_push_pairs(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata,
int *n_pushed) {
char ***m = userdata;
int r;
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
r = strv_extend(m, key);
if (r < 0)
return -ENOMEM;
if (!value) {
r = strv_extend(m, "");
if (r < 0)
return -ENOMEM;
} else {
r = strv_push(m, value);
if (r < 0)
return r;
}
if (n_pushed)
(*n_pushed)++;
return 0;
}
int load_env_file_pairs(FILE *f, const char *fname, char ***rl) {
char **m = NULL;
int r;
r = parse_env_file_internal(f, fname, load_env_file_push_pairs, &m, NULL);
if (r < 0) {
strv_free(m);
return r;
}
*rl = m;
return 0;
}
static int merge_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata,
int *n_pushed) {
char ***env = userdata;
char *expanded_value;
assert(env);
if (!value) {
log_error("%s:%u: invalid syntax (around \"%s\"), ignoring.", strna(filename), line, key);
return 0;
}
if (!env_name_is_valid(key)) {
log_error("%s:%u: invalid variable name \"%s\", ignoring.", strna(filename), line, key);
free(value);
return 0;
}
expanded_value = replace_env(value, *env,
REPLACE_ENV_USE_ENVIRONMENT|
REPLACE_ENV_ALLOW_BRACELESS|
REPLACE_ENV_ALLOW_EXTENDED);
if (!expanded_value)
return -ENOMEM;
free_and_replace(value, expanded_value);
log_debug("%s:%u: setting %s=%s", filename, line, key, value);
return load_env_file_push(filename, line, key, value, env, n_pushed);
}
int merge_env_file(
char ***env,
FILE *f,
const char *fname) {
/* NOTE: this function supports braceful and braceless variable expansions,
* plus "extended" substitutions, unlike other exported parsing functions.
*/
return parse_env_file_internal(f, fname, merge_env_file_push, env, NULL);
}
static void write_env_var(FILE *f, const char *v) {
const char *p;
p = strchr(v, '=');
if (!p) {
/* Fallback */
fputs_unlocked(v, f);
fputc_unlocked('\n', f);
return;
}
p++;
fwrite_unlocked(v, 1, p-v, f);
if (string_has_cc(p, NULL) || chars_intersect(p, WHITESPACE SHELL_NEED_QUOTES)) {
fputc_unlocked('"', f);
for (; *p; p++) {
if (strchr(SHELL_NEED_ESCAPE, *p))
fputc_unlocked('\\', f);
fputc_unlocked(*p, f);
}
fputc_unlocked('"', f);
} else
fputs_unlocked(p, f);
fputc_unlocked('\n', f);
}
int write_env_file(const char *fname, char **l) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
char **i;
int r;
assert(fname);
r = fopen_temporary(fname, &f, &p);
if (r < 0)
return r;
(void) fchmod_umask(fileno(f), 0644);
STRV_FOREACH(i, l)
write_env_var(f, *i);
r = fflush_and_check(f);
if (r >= 0) {
if (rename(p, fname) >= 0)
return 0;
r = -errno;
}
(void) unlink(p);
return r;
}