glib-aux: add nm_parse_env_file() helpers for parsing systemd's env-files

We write lease files for internal DHCP client ("systemd" and "nettools")
in a systemd-specific format. We want to drop systemd code, so we need
to have our own parsing code.

Granted, nettools only writes a single "ADDRESS=" line, so parsing that
would be easy. On the other hand, systemd's parser is not complicated
either (in particular, if we can steal their implementation). Also, it's
a commonly used format in systemd, so having the parser would allow us
to parse similar formats.

Also, we could opt to choose that format, where it makes sense.
This commit is contained in:
Thomas Haller
2022-04-08 19:37:18 +02:00
parent 7df494bc9a
commit c44b49db6f
3 changed files with 433 additions and 0 deletions

View File

@@ -723,3 +723,288 @@ nm_sd_notify(const char *state)
return 0; 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;
}

View File

@@ -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_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__ */ #endif /* __NM_IO_UTILS_H__ */

View File

@@ -10,6 +10,7 @@
#include "libnm-glib-aux/nm-str-buf.h" #include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-glib-aux/nm-time-utils.h" #include "libnm-glib-aux/nm-time-utils.h"
#include "libnm-glib-aux/nm-ref-string.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" #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(); NMTST_DEFINE();
int 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_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_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_nm_ascii", test_nm_ascii);
g_test_add_func("/general/test_parse_env_file", test_parse_env_file);
return g_test_run(); return g_test_run();
} }