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:
@@ -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;
|
||||||
|
}
|
||||||
|
@@ -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__ */
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user