diff options
Diffstat (limited to 'src/basic/env-file.c')
-rw-r--r-- | src/basic/env-file.c | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/src/basic/env-file.c b/src/basic/env-file.c new file mode 100644 index 0000000..c2cbff4 --- /dev/null +++ b/src/basic/env-file.c @@ -0,0 +1,647 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#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" + +typedef int (*push_env_func_t)( + const char *filename, + unsigned line, + const char *key, + char *value, + void *userdata); + +static int parse_env_file_internal( + FILE *f, + const char *fname, + push_env_func_t push, + void *userdata) { + + size_t n_key = 0, n_value = 0, last_value_whitespace = SIZE_MAX, last_key_whitespace = SIZE_MAX; + _cleanup_free_ char *contents = NULL, *key = NULL, *value = NULL; + unsigned line = 1; + 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; + + assert(f || fname); + assert(push); + + if (f) + r = read_full_stream(f, &contents, NULL); + else + r = read_full_file(fname, &contents, NULL); + if (r < 0) + return r; + + for (char *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_MAX; + + if (!GREEDY_REALLOC(key, 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_MAX; + } else { + if (!strchr(WHITESPACE, c)) + last_key_whitespace = SIZE_MAX; + else if (last_key_whitespace == SIZE_MAX) + last_key_whitespace = n_key; + + if (!GREEDY_REALLOC(key, 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_MAX) + key[last_key_whitespace] = 0; + + r = push(fname, line, key, value, userdata); + if (r < 0) + return r; + + n_key = 0; + value = NULL; + 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, 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_MAX) + value[last_value_whitespace] = 0; + + /* strip trailing whitespace from key */ + if (last_key_whitespace != SIZE_MAX) + key[last_key_whitespace] = 0; + + r = push(fname, line, key, value, userdata); + if (r < 0) + return r; + + n_key = 0; + value = NULL; + n_value = 0; + + } else if (c == '\\') { + state = VALUE_ESCAPE; + last_value_whitespace = SIZE_MAX; + } else { + if (!strchr(WHITESPACE, c)) + last_value_whitespace = SIZE_MAX; + else if (last_value_whitespace == SIZE_MAX) + last_value_whitespace = n_value; + + if (!GREEDY_REALLOC(value, 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, n_value+2)) + return -ENOMEM; + + value[n_value++] = c; + } + break; + + case SINGLE_QUOTE_VALUE: + if (c == '\'') + state = PRE_VALUE; + else { + if (!GREEDY_REALLOC(value, 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, 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, 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, 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: + log_debug("The line which doesn't begin with \";\" or \"#\", but follows a comment" \ + " line trailing with escape is now treated as a non comment line since v254."); + if (strchr(NEWLINE, c)) { + state = PRE_KEY; + line++; + } else + 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_MAX) + value[last_value_whitespace] = 0; + + /* strip trailing whitespace from key */ + if (last_key_whitespace != SIZE_MAX) + key[last_key_whitespace] = 0; + + r = push(fname, line, key, value, userdata); + 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) { + + assert(key); + + 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) { + + const char *k; + va_list aq, *ap = userdata; + int r; + + assert(key); + + 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_and_replace(*v, value); + + return 1; + } + } + + va_end(aq); + free(value); + + return 0; +} + +int parse_env_filev( + FILE *f, + const char *fname, + va_list ap) { + + int r; + va_list aq; + + assert(f || fname); + + va_copy(aq, ap); + r = parse_env_file_internal(f, fname, parse_env_file_push, &aq); + va_end(aq); + return r; +} + +int parse_env_file_fdv(int fd, const char *fname, va_list ap) { + _cleanup_fclose_ FILE *f = NULL; + va_list aq; + int r; + + assert(fd >= 0); + + r = fdopen_independent(fd, "re", &f); + if (r < 0) + return r; + + va_copy(aq, ap); + r = parse_env_file_internal(f, fname, parse_env_file_push, &aq); + va_end(aq); + return r; +} + +int parse_env_file_sentinel( + FILE *f, + const char *fname, + ...) { + + va_list ap; + int r; + + assert(f || fname); + + va_start(ap, fname); + r = parse_env_filev(f, fname, ap); + va_end(ap); + + return r; +} + +int parse_env_file_fd_sentinel( + int fd, + const char *fname, /* only used for logging */ + ...) { + + va_list ap; + int r; + + assert(fd >= 0); + + va_start(ap, fname); + r = parse_env_file_fdv(fd, 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) { + + char ***m = userdata; + char *p; + int r; + + assert(key); + + 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_consume(m, p); + if (r < 0) + return r; + + free(value); + return 0; +} + +int load_env_file(FILE *f, const char *fname, char ***ret) { + _cleanup_strv_free_ char **m = NULL; + int r; + + assert(f || fname); + assert(ret); + + r = parse_env_file_internal(f, fname, load_env_file_push, &m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +static int load_env_file_push_pairs( + const char *filename, unsigned line, + const char *key, char *value, + void *userdata) { + + char ***m = ASSERT_PTR(userdata); + int r; + + assert(key); + + r = check_utf8ness_and_warn(filename, line, key, value); + if (r < 0) + return r; + + /* Check if the key is present */ + for (char **t = *m; t && *t; t += 2) + if (streq(t[0], key)) { + if (value) + return free_and_replace(t[1], value); + else + return free_and_strdup(t+1, ""); + } + + r = strv_extend(m, key); + if (r < 0) + return r; + + if (value) + return strv_push(m, value); + else + return strv_extend(m, ""); +} + +int load_env_file_pairs(FILE *f, const char *fname, char ***ret) { + _cleanup_strv_free_ char **m = NULL; + int r; + + assert(f || fname); + assert(ret); + + r = parse_env_file_internal(f, fname, load_env_file_push_pairs, &m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +int load_env_file_pairs_fd(int fd, const char *fname, char ***ret) { + _cleanup_fclose_ FILE *f = NULL; + int r; + + assert(fd >= 0); + + r = fdopen_independent(fd, "re", &f); + if (r < 0) + return r; + + return load_env_file_pairs(f, fname, ret); +} + +static int merge_env_file_push( + const char *filename, unsigned line, + const char *key, char *value, + void *userdata) { + + char ***env = ASSERT_PTR(userdata); + char *expanded_value; + int r; + + assert(key); + + 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; + } + + r = replace_env(value, + *env, + REPLACE_ENV_USE_ENVIRONMENT|REPLACE_ENV_ALLOW_BRACELESS|REPLACE_ENV_ALLOW_EXTENDED, + &expanded_value); + if (r < 0) + return log_error_errno(r, "%s:%u: Failed to expand variable '%s': %m", strna(filename), line, value); + + 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); +} + +int merge_env_file( + char ***env, + FILE *f, + const char *fname) { + + assert(env); + assert(f || 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); +} + +static void write_env_var(FILE *f, const char *v) { + const char *p; + + assert(f); + assert(v); + + 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(int dir_fd, const char *fname, char **headers, char **l) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(dir_fd >= 0 || dir_fd == AT_FDCWD); + assert(fname); + + r = fopen_temporary_at(dir_fd, fname, &f, &p); + if (r < 0) + return r; + + (void) fchmod_umask(fileno(f), 0644); + + STRV_FOREACH(i, headers) { + assert(isempty(*i) || startswith(*i, "#")); + fputs_unlocked(*i, f); + fputc_unlocked('\n', f); + } + + STRV_FOREACH(i, l) + write_env_var(f, *i); + + r = fflush_and_check(f); + if (r >= 0) { + if (renameat(dir_fd, p, dir_fd, fname) >= 0) + return 0; + + r = -errno; + } + + (void) unlinkat(dir_fd, p, 0); + return r; +} + +int write_vconsole_conf(int dir_fd, const char *fname, char **l) { + char **headers = STRV_MAKE( + "# Written by systemd-localed(8) or systemd-firstboot(1), read by systemd-localed", + "# and systemd-vconsole-setup(8). Use localectl(1) to update this file."); + + return write_env_file(dir_fd, fname, headers, l); +} |