diff options
Diffstat (limited to 'src/basic/env-util.c')
-rw-r--r-- | src/basic/env-util.c | 904 |
1 files changed, 904 insertions, 0 deletions
diff --git a/src/basic/env-util.c b/src/basic/env-util.c new file mode 100644 index 0000000..55ac11a --- /dev/null +++ b/src/basic/env-util.c @@ -0,0 +1,904 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <limits.h> +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "env-util.h" +#include "errno-util.h" +#include "escape.h" +#include "extract-word.h" +#include "macro.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "utf8.h" + +/* We follow bash for the character set. Different shells have different rules. */ +#define VALID_BASH_ENV_NAME_CHARS \ + DIGITS LETTERS \ + "_" + +static bool env_name_is_valid_n(const char *e, size_t n) { + if (!e) + return false; + + if (n <= 0) + return false; + + if (ascii_isdigit(e[0])) + return false; + + /* POSIX says the overall size of the environment block cannot + * be > ARG_MAX, an individual assignment hence cannot be + * either. Discounting the equal sign and trailing NUL this + * hence leaves ARG_MAX-2 as longest possible variable + * name. */ + if (n > (size_t) sysconf(_SC_ARG_MAX) - 2) + return false; + + for (const char *p = e; p < e + n; p++) + if (!strchr(VALID_BASH_ENV_NAME_CHARS, *p)) + return false; + + return true; +} + +bool env_name_is_valid(const char *e) { + return env_name_is_valid_n(e, strlen_ptr(e)); +} + +bool env_value_is_valid(const char *e) { + if (!e) + return false; + + if (!utf8_is_valid(e)) + return false; + + /* Note that variable *values* may contain control characters, in particular NL, TAB, BS, DEL, ESC… + * When printing those variables with show-environment, we'll escape them. Make sure to print + * environment variables carefully! */ + + /* POSIX says the overall size of the environment block cannot be > ARG_MAX, an individual assignment + * hence cannot be either. Discounting the shortest possible variable name of length 1, the equal + * sign and trailing NUL this hence leaves ARG_MAX-3 as longest possible variable value. */ + if (strlen(e) > sc_arg_max() - 3) + return false; + + return true; +} + +bool env_assignment_is_valid(const char *e) { + const char *eq; + + eq = strchr(e, '='); + if (!eq) + return false; + + if (!env_name_is_valid_n(e, eq - e)) + return false; + + if (!env_value_is_valid(eq + 1)) + return false; + + /* POSIX says the overall size of the environment block cannot be > ARG_MAX, hence the individual + * variable assignments cannot be either, but let's leave room for one trailing NUL byte. */ + if (strlen(e) > sc_arg_max() - 1) + return false; + + return true; +} + +bool strv_env_is_valid(char **e) { + STRV_FOREACH(p, e) { + size_t k; + + if (!env_assignment_is_valid(*p)) + return false; + + /* Check if there are duplicate assignments */ + k = strcspn(*p, "="); + STRV_FOREACH(q, p + 1) + if (strneq(*p, *q, k) && (*q)[k] == '=') + return false; + } + + return true; +} + +bool strv_env_name_is_valid(char **l) { + STRV_FOREACH(p, l) { + if (!env_name_is_valid(*p)) + return false; + + if (strv_contains(p + 1, *p)) + return false; + } + + return true; +} + +bool strv_env_name_or_assignment_is_valid(char **l) { + STRV_FOREACH(p, l) { + if (!env_assignment_is_valid(*p) && !env_name_is_valid(*p)) + return false; + + if (strv_contains(p + 1, *p)) + return false; + } + + return true; +} + +static int env_append(char **r, char ***k, char **a) { + assert(r); + assert(k); + assert(*k >= r); + + if (!a) + return 0; + + /* Expects the following arguments: 'r' shall point to the beginning of an strv we are going to append to, 'k' + * to a pointer pointing to the NULL entry at the end of the same array. 'a' shall point to another strv. + * + * This call adds every entry of 'a' to 'r', either overriding an existing matching entry, or appending to it. + * + * This call assumes 'r' has enough pre-allocated space to grow by all of 'a''s items. */ + + for (; *a; a++) { + char **j, *c; + size_t n; + + n = strcspn(*a, "="); + if ((*a)[n] == '=') + n++; + + for (j = r; j < *k; j++) + if (strneq(*j, *a, n)) + break; + + c = strdup(*a); + if (!c) + return -ENOMEM; + + if (j >= *k) { /* Append to the end? */ + (*k)[0] = c; + (*k)[1] = NULL; + (*k)++; + } else + free_and_replace(*j, c); /* Override existing item */ + } + + return 0; +} + +char** _strv_env_merge(char **first, ...) { + _cleanup_strv_free_ char **merged = NULL; + char **k; + va_list ap; + + /* Merges an arbitrary number of environment sets */ + + size_t n = strv_length(first); + + va_start(ap, first); + for (;;) { + char **l; + + l = va_arg(ap, char**); + if (l == POINTER_MAX) + break; + + n += strv_length(l); + } + va_end(ap); + + k = merged = new(char*, n + 1); + if (!merged) + return NULL; + merged[0] = NULL; + + if (env_append(merged, &k, first) < 0) + return NULL; + + va_start(ap, first); + for (;;) { + char **l; + + l = va_arg(ap, char**); + if (l == POINTER_MAX) + break; + + if (env_append(merged, &k, l) < 0) { + va_end(ap); + return NULL; + } + } + va_end(ap); + + return TAKE_PTR(merged); +} + +static bool env_match(const char *t, const char *pattern) { + assert(t); + assert(pattern); + + /* pattern a matches string a + * a matches a= + * a matches a=b + * a= matches a= + * a=b matches a=b + * a= does not match a + * a=b does not match a= + * a=b does not match a + * a=b does not match a=c */ + + if (streq(t, pattern)) + return true; + + if (!strchr(pattern, '=')) { + size_t l = strlen(pattern); + + return strneq(t, pattern, l) && t[l] == '='; + } + + return false; +} + +static bool env_entry_has_name(const char *entry, const char *name) { + const char *t; + + assert(entry); + assert(name); + + t = startswith(entry, name); + if (!t) + return false; + + return *t == '='; +} + +char **strv_env_delete(char **x, size_t n_lists, ...) { + size_t n, i = 0; + char **r; + va_list ap; + + /* Deletes every entry from x that is mentioned in the other + * string lists */ + + n = strv_length(x); + + r = new(char*, n+1); + if (!r) + return NULL; + + STRV_FOREACH(k, x) { + va_start(ap, n_lists); + for (size_t v = 0; v < n_lists; v++) { + char **l; + + l = va_arg(ap, char**); + STRV_FOREACH(j, l) + if (env_match(*k, *j)) + goto skip; + } + va_end(ap); + + r[i] = strdup(*k); + if (!r[i]) { + strv_free(r); + return NULL; + } + + i++; + continue; + + skip: + va_end(ap); + } + + r[i] = NULL; + + assert(i <= n); + + return r; +} + +char **strv_env_unset(char **l, const char *p) { + char **f, **t; + + if (!l) + return NULL; + + assert(p); + + /* Drops every occurrence of the env var setting p in the + * string list. Edits in-place. */ + + for (f = t = l; *f; f++) { + + if (env_match(*f, p)) { + free(*f); + continue; + } + + *(t++) = *f; + } + + *t = NULL; + return l; +} + +char **strv_env_unset_many(char **l, ...) { + char **f, **t; + + if (!l) + return NULL; + + /* Like strv_env_unset() but applies many at once. Edits in-place. */ + + for (f = t = l; *f; f++) { + bool found = false; + const char *p; + va_list ap; + + va_start(ap, l); + + while ((p = va_arg(ap, const char*))) { + if (env_match(*f, p)) { + found = true; + break; + } + } + + va_end(ap); + + if (found) { + free(*f); + continue; + } + + *(t++) = *f; + } + + *t = NULL; + return l; +} + +int strv_env_replace_consume(char ***l, char *p) { + const char *t, *name; + int r; + + assert(p); + + /* Replace first occurrence of the env var or add a new one in the string list. Drop other + * occurrences. Edits in-place. Does not copy p and CONSUMES p EVEN ON FAILURE. + * + * p must be a valid key=value assignment. */ + + t = strchr(p, '='); + if (!t) { + free(p); + return -EINVAL; + } + + name = strndupa_safe(p, t - p); + + STRV_FOREACH(f, *l) + if (env_entry_has_name(*f, name)) { + free_and_replace(*f, p); + strv_env_unset(f + 1, *f); + return 0; + } + + /* We didn't find a match, we need to append p or create a new strv */ + r = strv_consume(l, p); + if (r < 0) + return r; + + return 1; +} + +int strv_env_replace_strdup(char ***l, const char *assignment) { + /* Like strv_env_replace_consume(), but copies the argument. */ + + char *p = strdup(assignment); + if (!p) + return -ENOMEM; + + return strv_env_replace_consume(l, p); +} + +int strv_env_replace_strdup_passthrough(char ***l, const char *assignment) { + /* Like strv_env_replace_strdup(), but pulls the variable from the environment of + * the calling program, if a variable name without value is specified. + */ + char *p; + + if (strchr(assignment, '=')) { + if (!env_assignment_is_valid(assignment)) + return -EINVAL; + + p = strdup(assignment); + } else { + if (!env_name_is_valid(assignment)) + return -EINVAL; + + /* If we can't find the variable in our environment, we will use + * the empty string. This way "passthrough" is equivalent to passing + * --setenv=FOO=$FOO in the shell. */ + p = strjoin(assignment, "=", secure_getenv(assignment)); + } + if (!p) + return -ENOMEM; + + return strv_env_replace_consume(l, p); +} + +int strv_env_assign(char ***l, const char *key, const char *value) { + if (!env_name_is_valid(key)) + return -EINVAL; + + /* NULL removes assignment, "" creates an empty assignment. */ + + if (!value) { + strv_env_unset(*l, key); + return 0; + } + + char *p = strjoin(key, "=", value); + if (!p) + return -ENOMEM; + + return strv_env_replace_consume(l, p); +} + +char *strv_env_get_n(char **l, const char *name, size_t k, unsigned flags) { + assert(name); + + if (k <= 0) + return NULL; + + STRV_FOREACH_BACKWARDS(i, l) + if (strneq(*i, name, k) && + (*i)[k] == '=') + return *i + k + 1; + + if (flags & REPLACE_ENV_USE_ENVIRONMENT) { + const char *t; + + t = strndupa_safe(name, k); + return getenv(t); + }; + + return NULL; +} + +char *strv_env_get(char **l, const char *name) { + assert(name); + + return strv_env_get_n(l, name, strlen(name), 0); +} + +char *strv_env_pairs_get(char **l, const char *name) { + char *result = NULL; + + assert(name); + + STRV_FOREACH_PAIR(key, value, l) + if (streq(*key, name)) + result = *value; + + return result; +} + +char **strv_env_clean_with_callback(char **e, void (*invalid_callback)(const char *p, void *userdata), void *userdata) { + int k = 0; + + STRV_FOREACH(p, e) { + size_t n; + bool duplicate = false; + + if (!env_assignment_is_valid(*p)) { + if (invalid_callback) + invalid_callback(*p, userdata); + free(*p); + continue; + } + + n = strcspn(*p, "="); + STRV_FOREACH(q, p + 1) + if (strneq(*p, *q, n) && (*q)[n] == '=') { + duplicate = true; + break; + } + + if (duplicate) { + free(*p); + continue; + } + + e[k++] = *p; + } + + if (e) + e[k] = NULL; + + return e; +} + +char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) { + enum { + WORD, + CURLY, + VARIABLE, + VARIABLE_RAW, + TEST, + DEFAULT_VALUE, + ALTERNATE_VALUE, + } state = WORD; + + const char *e, *word = format, *test_value = NULL; /* test_value is initialized to appease gcc */ + char *k; + _cleanup_free_ char *r = NULL; + size_t i, len = 0; /* len is initialized to appease gcc */ + int nest = 0; + + assert(format); + + for (e = format, i = 0; *e && i < n; e ++, i ++) + switch (state) { + + case WORD: + if (*e == '$') + state = CURLY; + break; + + case CURLY: + if (*e == '{') { + k = strnappend(r, word, e-word-1); + if (!k) + return NULL; + + free_and_replace(r, k); + + word = e-1; + state = VARIABLE; + nest++; + } else if (*e == '$') { + k = strnappend(r, word, e-word); + if (!k) + return NULL; + + free_and_replace(r, k); + + word = e+1; + state = WORD; + + } else if (flags & REPLACE_ENV_ALLOW_BRACELESS && strchr(VALID_BASH_ENV_NAME_CHARS, *e)) { + k = strnappend(r, word, e-word-1); + if (!k) + return NULL; + + free_and_replace(r, k); + + word = e-1; + state = VARIABLE_RAW; + + } else + state = WORD; + break; + + case VARIABLE: + if (*e == '}') { + const char *t; + + t = strv_env_get_n(env, word+2, e-word-2, flags); + + if (!strextend(&r, t)) + return NULL; + + word = e+1; + state = WORD; + nest--; + } else if (*e == ':') { + if (flags & REPLACE_ENV_ALLOW_EXTENDED) { + len = e - word - 2; + state = TEST; + } else + /* Treat this as unsupported syntax, i.e. do no replacement */ + state = WORD; + } + break; + + case TEST: + if (*e == '-') + state = DEFAULT_VALUE; + else if (*e == '+') + state = ALTERNATE_VALUE; + else { + state = WORD; + break; + } + + test_value = e+1; + break; + + case DEFAULT_VALUE: /* fall through */ + case ALTERNATE_VALUE: + assert(flags & REPLACE_ENV_ALLOW_EXTENDED); + + if (*e == '{') { + nest++; + break; + } + + if (*e != '}') + break; + + nest--; + if (nest == 0) { + const char *t; + _cleanup_free_ char *v = NULL; + + t = strv_env_get_n(env, word+2, len, flags); + + if (t && state == ALTERNATE_VALUE) + t = v = replace_env_n(test_value, e-test_value, env, flags); + else if (!t && state == DEFAULT_VALUE) + t = v = replace_env_n(test_value, e-test_value, env, flags); + + if (!strextend(&r, t)) + return NULL; + + word = e+1; + state = WORD; + } + break; + + case VARIABLE_RAW: + assert(flags & REPLACE_ENV_ALLOW_BRACELESS); + + if (!strchr(VALID_BASH_ENV_NAME_CHARS, *e)) { + const char *t; + + t = strv_env_get_n(env, word+1, e-word-1, flags); + + if (!strextend(&r, t)) + return NULL; + + word = e--; + i--; + state = WORD; + } + break; + } + + if (state == VARIABLE_RAW) { + const char *t; + + assert(flags & REPLACE_ENV_ALLOW_BRACELESS); + + t = strv_env_get_n(env, word+1, e-word-1, flags); + return strjoin(r, t); + } else + return strnappend(r, word, e-word); +} + +char **replace_env_argv(char **argv, char **env) { + char **ret; + size_t k = 0, l = 0; + + l = strv_length(argv); + + ret = new(char*, l+1); + if (!ret) + return NULL; + + STRV_FOREACH(i, argv) { + + /* If $FOO appears as single word, replace it by the split up variable */ + if ((*i)[0] == '$' && !IN_SET((*i)[1], '{', '$')) { + char *e; + char **w, **m = NULL; + size_t q; + + e = strv_env_get(env, *i+1); + if (e) { + int r; + + r = strv_split_full(&m, e, WHITESPACE, EXTRACT_RELAX|EXTRACT_UNQUOTE); + if (r < 0) { + ret[k] = NULL; + strv_free(ret); + return NULL; + } + } else + m = NULL; + + q = strv_length(m); + l = l + q - 1; + + w = reallocarray(ret, l + 1, sizeof(char *)); + if (!w) { + ret[k] = NULL; + strv_free(ret); + strv_free(m); + return NULL; + } + + ret = w; + if (m) { + memcpy(ret + k, m, q * sizeof(char*)); + free(m); + } + + k += q; + continue; + } + + /* If ${FOO} appears as part of a word, replace it by the variable as-is */ + ret[k] = replace_env(*i, env, 0); + if (!ret[k]) { + strv_free(ret); + return NULL; + } + k++; + } + + ret[k] = NULL; + return ret; +} + +int getenv_bool(const char *p) { + const char *e; + + e = getenv(p); + if (!e) + return -ENXIO; + + return parse_boolean(e); +} + +int getenv_bool_secure(const char *p) { + const char *e; + + e = secure_getenv(p); + if (!e) + return -ENXIO; + + return parse_boolean(e); +} + +int getenv_uint64_secure(const char *p, uint64_t *ret) { + const char *e; + + assert(p); + + e = secure_getenv(p); + if (!e) + return -ENXIO; + + return safe_atou64(e, ret); +} + +int set_unset_env(const char *name, const char *value, bool overwrite) { + assert(name); + + if (value) + return RET_NERRNO(setenv(name, value, overwrite)); + + return RET_NERRNO(unsetenv(name)); +} + +int putenv_dup(const char *assignment, bool override) { + const char *e, *n; + + e = strchr(assignment, '='); + if (!e) + return -EINVAL; + + n = strndupa_safe(assignment, e - assignment); + + /* This is like putenv(), but uses setenv() so that our memory doesn't become part of environ[]. */ + return RET_NERRNO(setenv(n, e + 1, override)); +} + +int setenv_systemd_exec_pid(bool update_only) { + char str[DECIMAL_STR_MAX(pid_t)]; + const char *e; + + /* Update $SYSTEMD_EXEC_PID=pid except when '*' is set for the variable. */ + + e = secure_getenv("SYSTEMD_EXEC_PID"); + if (!e && update_only) + return 0; + + if (streq_ptr(e, "*")) + return 0; + + xsprintf(str, PID_FMT, getpid_cached()); + + if (setenv("SYSTEMD_EXEC_PID", str, 1) < 0) + return -errno; + + return 1; +} + +int getenv_path_list(const char *name, char ***ret_paths) { + _cleanup_strv_free_ char **l = NULL; + const char *e; + int r; + + assert(name); + assert(ret_paths); + + e = secure_getenv(name); + if (!e) + return -ENXIO; + + r = strv_split_full(&l, e, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return log_debug_errno(r, "Failed to parse $%s: %m", name); + + STRV_FOREACH(p, l) { + if (!path_is_absolute(*p)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Path '%s' is not absolute, refusing.", *p); + + if (!path_is_normalized(*p)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Path '%s' is not normalized, refusing.", *p); + + if (path_equal(*p, "/")) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Path '%s' is the root fs, refusing.", *p); + } + + if (strv_isempty(l)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "No paths specified, refusing."); + + *ret_paths = TAKE_PTR(l); + return 1; +} + +int getenv_steal_erase(const char *name, char **ret) { + _cleanup_(erase_and_freep) char *a = NULL; + char *e; + + assert(name); + + /* Reads an environment variable, makes a copy of it, erases its memory in the environment block and removes + * it from there. Usecase: reading passwords from the env block (which is a bad idea, but useful for + * testing, and given that people are likely going to misuse this, be thorough) */ + + e = getenv(name); + if (!e) { + if (ret) + *ret = NULL; + return 0; + } + + if (ret) { + a = strdup(e); + if (!a) + return -ENOMEM; + } + + string_erase(e); + + if (unsetenv(name) < 0) + return -errno; + + if (ret) + *ret = TAKE_PTR(a); + + return 1; +} |