blob: dc92b13a6f922305fad661d752d8e9d1c41f03c1 [file] [log] [blame] [raw]
/* 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;
}