diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c index 503f044f3..0823a16c4 100644 --- a/src/libnm-glib-aux/nm-io-utils.c +++ b/src/libnm-glib-aux/nm-io-utils.c @@ -723,3 +723,288 @@ nm_sd_notify(const char *state) return 0; } + +/*****************************************************************************/ + +#define SHELL_NEED_ESCAPE "\"\\`$" + +int +nm_parse_env_file_full( + const char *contents, + int (*push)(unsigned line, const char *key, const char *value, void *userdata), + void *userdata) +{ + gsize last_value_whitespace = G_MAXSIZE; + gsize last_key_whitespace = G_MAXSIZE; + nm_auto_str_buf NMStrBuf key = NM_STR_BUF_INIT(0, FALSE); + nm_auto_str_buf NMStrBuf value = NM_STR_BUF_INIT(0, FALSE); + 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; + + /* Copied and adjusted from systemd's parse_env_file_internal(). + * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L15 */ + + nm_assert(push); + + if (!contents) + return -ENOENT; + + for (const char *p = contents; *p; p++) { + char c = *p; + + switch (state) { + case PRE_KEY: + if (NM_IN_SET(c, '#', ';')) + state = COMMENT; + else if (!nm_ascii_is_whitespace(c)) { + state = KEY; + last_key_whitespace = G_MAXSIZE; + nm_str_buf_append_c(&key, c); + } + break; + + case KEY: + if (nm_ascii_is_newline(c)) { + state = PRE_KEY; + line++; + nm_str_buf_reset(&key); + } else if (c == '=') { + state = PRE_VALUE; + last_value_whitespace = G_MAXSIZE; + } else { + if (!nm_ascii_is_whitespace(c)) + last_key_whitespace = G_MAXSIZE; + else if (last_key_whitespace == G_MAXSIZE) + last_key_whitespace = key.len; + nm_str_buf_append_c(&key, c); + } + break; + + case PRE_VALUE: + if (nm_ascii_is_newline(c)) { + state = PRE_KEY; + line++; + + /* strip trailing whitespace from key */ + if (last_key_whitespace != G_MAXSIZE) + nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0; + + r = push(line, + nm_str_buf_get_str(&key), + nm_str_buf_get_str(&value) ?: "", + userdata); + if (r < 0) + return r; + + nm_str_buf_reset(&key); + nm_str_buf_reset(&value); + } else if (c == '\'') + state = SINGLE_QUOTE_VALUE; + else if (c == '"') + state = DOUBLE_QUOTE_VALUE; + else if (c == '\\') + state = VALUE_ESCAPE; + else if (!nm_ascii_is_whitespace(c)) { + state = VALUE; + nm_str_buf_append_c(&value, c); + } + + break; + + case VALUE: + if (nm_ascii_is_newline(c)) { + state = PRE_KEY; + line++; + + /* Chomp off trailing whitespace from value */ + if (last_value_whitespace != G_MAXSIZE) + nm_str_buf_get_str_unsafe(&value)[last_value_whitespace] = 0; + + /* strip trailing whitespace from key */ + if (last_key_whitespace != G_MAXSIZE) + nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0; + + r = push(line, + nm_str_buf_get_str(&key), + nm_str_buf_get_str(&value) ?: "", + userdata); + if (r < 0) + return r; + + nm_str_buf_reset(&key); + nm_str_buf_reset(&value); + } else if (c == '\\') { + state = VALUE_ESCAPE; + last_value_whitespace = G_MAXSIZE; + } else { + if (!nm_ascii_is_whitespace(c)) + last_value_whitespace = G_MAXSIZE; + else if (last_value_whitespace == G_MAXSIZE) + last_value_whitespace = value.len; + nm_str_buf_append_c(&value, c); + } + break; + + case VALUE_ESCAPE: + state = VALUE; + if (!nm_ascii_is_newline(c)) { + /* Escaped newlines we eat up entirely */ + nm_str_buf_append_c(&value, c); + } + break; + + case SINGLE_QUOTE_VALUE: + if (c == '\'') + state = PRE_VALUE; + else + nm_str_buf_append_c(&value, c); + break; + + case DOUBLE_QUOTE_VALUE: + if (c == '"') + state = PRE_VALUE; + else if (c == '\\') + state = DOUBLE_QUOTE_VALUE_ESCAPE; + else + nm_str_buf_append_c(&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. */ + nm_str_buf_append_c(&value, c); + } else if (c != '\n') { + /* If other char than what needs escaping, keep the "\" in place, like the + * real shell does. */ + nm_str_buf_append_c(&value, '\\', c); + } + /* Escaped newlines (aka "continuation lines") are eaten up entirely */ + break; + + case COMMENT: + if (c == '\\') + state = COMMENT_ESCAPE; + else if (nm_ascii_is_newline(c)) { + state = PRE_KEY; + line++; + } + break; + + case COMMENT_ESCAPE: + state = COMMENT; + break; + } + } + + if (NM_IN_SET(state, + PRE_VALUE, + VALUE, + VALUE_ESCAPE, + SINGLE_QUOTE_VALUE, + DOUBLE_QUOTE_VALUE, + DOUBLE_QUOTE_VALUE_ESCAPE)) { + if (state == VALUE) + if (last_value_whitespace != G_MAXSIZE) + nm_str_buf_get_str_unsafe(&value)[last_value_whitespace] = 0; + + /* strip trailing whitespace from key */ + if (last_key_whitespace != G_MAXSIZE) + nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0; + + r = push(line, nm_str_buf_get_str(&key), nm_str_buf_get_str(&value) ?: "", userdata); + if (r < 0) + return r; + } + + return 0; +} + +/*****************************************************************************/ + +static int +check_utf8ness_and_warn(const char *key, const char *value) +{ + /* Taken from systemd's check_utf8ness_and_warn() + * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L273 */ + + if (!g_utf8_validate(key, -1, NULL)) + return -EINVAL; + + if (!g_utf8_validate(value, -1, NULL)) + return -EINVAL; + + return 0; +} + +static int +parse_env_file_push(unsigned line, const char *key, const char *value, void *userdata) +{ + const char *k; + va_list *ap = userdata; + va_list aq; + int r; + + r = check_utf8ness_and_warn(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 (nm_streq(key, k)) { + va_end(aq); + g_free(*v); + *v = g_strdup(value); + return 1; + } + } + + va_end(aq); + return 0; +} + +int +nm_parse_env_filev(const char *contents, va_list ap) +{ + va_list aq; + int r; + + /* Copied from systemd's parse_env_filev(). + * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L333 */ + + va_copy(aq, ap); + r = nm_parse_env_file_full(contents, parse_env_file_push, &aq); + va_end(aq); + return r; +} + +int +nm_parse_env_file_sentinel(const char *contents, ...) +{ + va_list ap; + int r; + + /* Copied from systemd's parse_env_file_sentinel(). + * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L347 */ + + va_start(ap, contents); + r = nm_parse_env_filev(contents, ap); + va_end(ap); + return r; +} diff --git a/src/libnm-glib-aux/nm-io-utils.h b/src/libnm-glib-aux/nm-io-utils.h index a85039867..ef015153c 100644 --- a/src/libnm-glib-aux/nm-io-utils.h +++ b/src/libnm-glib-aux/nm-io-utils.h @@ -77,4 +77,15 @@ int nm_io_sockaddr_un_set(struct sockaddr_un *ret, NMOptionBool is_abstract, con int nm_sd_notify(const char *state); +/*****************************************************************************/ + +int nm_parse_env_file_full( + const char *contents, + int (*push)(unsigned line, const char *key, const char *value, void *userdata), + void *userdata); + +int nm_parse_env_filev(const char *contents, va_list ap); +int nm_parse_env_file_sentinel(const char *contents, ...) G_GNUC_NULL_TERMINATED; +#define nm_parse_env_file(contents, ...) nm_parse_env_file_sentinel((contents), __VA_ARGS__, NULL) + #endif /* __NM_IO_UTILS_H__ */ diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c index 1fc9b6f0c..4b537f55e 100644 --- a/src/libnm-glib-aux/tests/test-shared-general.c +++ b/src/libnm-glib-aux/tests/test-shared-general.c @@ -10,6 +10,7 @@ #include "libnm-glib-aux/nm-str-buf.h" #include "libnm-glib-aux/nm-time-utils.h" #include "libnm-glib-aux/nm-ref-string.h" +#include "libnm-glib-aux/nm-io-utils.h" #include "libnm-glib-aux/nm-test-utils.h" @@ -1420,6 +1421,141 @@ test_nm_ascii(void) /*****************************************************************************/ +static int +_env_file_push_cb(unsigned line, const char *key, const char *value, void *user_data) +{ + char ***strv = user_data; + char *s_line; + gsize key_l; + gsize strv_l; + gsize i; + + g_assert(strv); + g_assert(key); + g_assert(key[0]); + g_assert(!strchr(key, '=')); + g_assert(value); + + key_l = strlen(key); + + s_line = g_strconcat(key, "=", value, NULL); + + strv_l = 0; + if (*strv) { + const char *s; + + for (i = 0; (s = (*strv)[i]); i++) { + if (g_str_has_prefix(s, key) && s[key_l] == '=') { + g_free((*strv)[i]); + (*strv)[i] = s_line; + return 0; + } + } + strv_l = i; + } + + *strv = g_realloc(*strv, sizeof(char *) * (strv_l + 2)); + (*strv)[strv_l] = s_line; + (*strv)[strv_l + 1] = NULL; + + return 0; +} + +static void +test_parse_env_file(void) +{ + gs_strfreev char **data = NULL; + gs_free char *arg1 = NULL; + gs_free char *arg2 = NULL; + int r; + +#define env_file_1 \ + "a=a\n" \ + "a=b\n" \ + "a=b\n" \ + "a=a\n" \ + "b=b\\\n" \ + "c\n" \ + "d= d\\\n" \ + "e \\\n" \ + "f \n" \ + "g=g\\ \n" \ + "h= ąęół\\ śćńźżµ \n" \ + "i=i\\" + r = nm_parse_env_file_full(env_file_1, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + nmtst_assert_strv(data, "a=a", "b=bc", "d=de f", "g=g ", "h=ąęół śćńźżµ", "i=i"); + nm_clear_pointer(&data, g_strfreev); + + r = nm_parse_env_file(env_file_1, "a", &arg1); + g_assert_cmpint(r, ==, 0); + g_assert_cmpstr(arg1, ==, "a"); + nm_clear_g_free(&arg1); + + r = nm_parse_env_file(env_file_1, "a", &arg1, "d", &arg2); + g_assert_cmpint(r, ==, 0); + g_assert_cmpstr(arg1, ==, "a"); + g_assert_cmpstr(arg2, ==, "de f"); + nm_clear_g_free(&arg1); + nm_clear_g_free(&arg2); + +#define env_file_2 "a=a\\\n" + r = nm_parse_env_file_full(env_file_2, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + nmtst_assert_strv(data, "a=a"); + nm_clear_pointer(&data, g_strfreev); + +#define env_file_3 \ + "#SPAMD_ARGS=\"-d --socketpath=/var/lib/bulwark/spamd \\\n" \ + "#--nouser-config \\\n" \ + "normal=line \\\n" \ + ";normal=ignored \\\n" \ + "normal_ignored \\\n" \ + "normal ignored \\\n" + r = nm_parse_env_file_full(env_file_3, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + g_assert(!data); + +#define env_file_4 \ + "# Generated\n" \ + "\n" \ + "HWMON_MODULES=\"coretemp f71882fg\"\n" \ + "\n" \ + "# For compatibility reasons\n" \ + "\n" \ + "MODULE_0=coretemp\n" \ + "MODULE_1=f71882fg" + r = nm_parse_env_file_full(env_file_4, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + nmtst_assert_strv(data, + "HWMON_MODULES=coretemp f71882fg", + "MODULE_0=coretemp", + "MODULE_1=f71882fg"); + nm_clear_pointer(&data, g_strfreev); + +#define env_file_5 \ + "a=\n" \ + "b=" + r = nm_parse_env_file_full(env_file_5, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + nmtst_assert_strv(data, "a=", "b="); + nm_clear_pointer(&data, g_strfreev); + +#define env_file_6 \ + "a=\\ \\n \\t \\x \\y \\' \n" \ + "b= \\$' \n" \ + "c= ' \\n\\t\\$\\`\\\\\n" \ + "' \n" \ + "d= \" \\n\\t\\$\\`\\\\\n" \ + "\" \n" + r = nm_parse_env_file_full(env_file_6, _env_file_push_cb, &data); + g_assert_cmpint(r, ==, 0); + nmtst_assert_strv(data, "a= n t x y '", "b=$'", "c= \\n\\t\\$\\`\\\\\n", "d= \\n\\t$`\\\n"); + nm_clear_pointer(&data, g_strfreev); +} + +/*****************************************************************************/ + NMTST_DEFINE(); int @@ -1453,6 +1589,7 @@ main(int argc, char **argv) g_test_add_func("/general/test_utils_hashtable_cmp", test_utils_hashtable_cmp); g_test_add_func("/general/test_nm_g_source_sentinel", test_nm_g_source_sentinel); g_test_add_func("/general/test_nm_ascii", test_nm_ascii); + g_test_add_func("/general/test_parse_env_file", test_parse_env_file); return g_test_run(); }