summaryrefslogtreecommitdiffstats
path: root/src/basic/env-util.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/basic/env-util.c')
-rw-r--r--src/basic/env-util.c904
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;
+}