Files
NetworkManager/src/libnm-glib-aux/nm-io-utils.c
Thomas Haller c44b49db6f 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.
2022-04-14 14:51:02 +02:00

1011 lines
33 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2018 Red Hat, Inc.
*/
#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
#include "nm-io-utils.h"
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include "nm-str-buf.h"
#include "nm-shared-utils.h"
#include "nm-secret-utils.h"
#include "nm-errno.h"
/*****************************************************************************/
_nm_printf(4, 5) static int _get_contents_error(GError **error,
int errsv,
int *out_errsv,
const char *format,
...)
{
nm_assert(NM_ERRNO_NATIVE(errsv));
if (error) {
gs_free char *msg = NULL;
va_list args;
char bstrerr[NM_STRERROR_BUFSIZE];
va_start(args, format);
msg = g_strdup_vprintf(format, args);
va_end(args);
g_set_error(error,
G_FILE_ERROR,
g_file_error_from_errno(errsv),
"%s: %s",
msg,
nm_strerror_native_r(errsv, bstrerr, sizeof(bstrerr)));
}
nm_assert(errsv > 0);
NM_SET_OUT(out_errsv, errsv);
return FALSE;
}
#define _get_contents_error_errno(error, out_errsv, ...) \
({ \
int _errsv = (errno); \
\
_get_contents_error(error, _errsv, out_errsv, __VA_ARGS__); \
})
/**
* nm_utils_fd_get_contents:
* @fd: open file descriptor to read. The fd will not be closed,
* but don't rely on its state afterwards.
* @close_fd: if %TRUE, @fd will be closed by the function.
* Passing %TRUE here might safe a syscall for dup().
* @max_length: allocate at most @max_length bytes. If the
* file is larger, reading will fail. Set to zero to use
* a very large default.
* WARNING: @max_length is here to avoid a crash for huge/unlimited files.
* For example, stat(/sys/class/net/enp0s25/ifindex) gives a filesize of
* 4K, although the actual real is small. @max_length is the memory
* allocated in the process of reading the file, thus it must be at least
* the size reported by fstat.
* If you set it to 1K, read will fail because fstat() claims the
* file is larger.
* @flags: %NMUtilsFileGetContentsFlags for reading the file.
* @contents: the output buffer with the file read. It is always
* NUL terminated. The buffer is at most @max_length long, including
* the NUL byte. That is, it reads only files up to a length of
* @max_length - 1 bytes.
* @length: optional output argument of the read file size.
* @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
* @error:
*
*
* A reimplementation of g_file_get_contents() with a few differences:
* - accepts an open fd, instead of a path name. This allows you to
* use openat().
* - limits the maximum filesize to max_length.
*
* Returns: TRUE on success.
*/
gboolean
nm_utils_fd_get_contents(int fd,
gboolean close_fd,
gsize max_length,
NMUtilsFileGetContentsFlags flags,
char **contents,
gsize *length,
int *out_errsv,
GError **error)
{
nm_auto_close int fd_keeper = close_fd ? fd : -1;
struct stat stat_buf;
gs_free char *str = NULL;
const bool do_bzero_mem = NM_FLAGS_HAS(flags, NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET);
int errsv;
g_return_val_if_fail(fd >= 0, FALSE);
g_return_val_if_fail(contents && !*contents, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
NM_SET_OUT(length, 0);
if (fstat(fd, &stat_buf) < 0)
return _get_contents_error_errno(error, out_errsv, "failure during fstat");
if (!max_length) {
/* default to a very large size, but not extreme */
max_length = 2 * 1024 * 1024;
}
if (stat_buf.st_size > 0 && S_ISREG(stat_buf.st_mode)) {
const gsize n_stat = stat_buf.st_size;
ssize_t n_read;
if (n_stat > max_length - 1)
return _get_contents_error(error,
EMSGSIZE,
out_errsv,
"file too large (%zu+1 bytes with maximum %zu bytes)",
n_stat,
max_length);
str = g_try_malloc(n_stat + 1);
if (!str)
return _get_contents_error(error,
ENOMEM,
out_errsv,
"failure to allocate buffer of %zu+1 bytes",
n_stat);
n_read = nm_utils_fd_read_loop(fd, str, n_stat, TRUE);
if (n_read < 0) {
if (do_bzero_mem)
nm_explicit_bzero(str, n_stat);
return _get_contents_error(error,
-n_read,
out_errsv,
"error reading %zu bytes from file descriptor",
n_stat);
}
str[n_read] = '\0';
if (n_read < n_stat) {
if (!(str = nm_secret_mem_try_realloc_take(str, do_bzero_mem, n_stat + 1, n_read + 1)))
return _get_contents_error(error,
ENOMEM,
out_errsv,
"failure to reallocate buffer with %zu bytes",
n_read + 1);
}
NM_SET_OUT(length, n_read);
} else {
nm_auto_fclose FILE *f = NULL;
char buf[4096];
gsize n_have, n_alloc;
int fd2;
if (fd_keeper >= 0)
fd2 = nm_steal_fd(&fd_keeper);
else {
fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 0);
if (fd2 < 0)
return _get_contents_error_errno(error, out_errsv, "error during dup");
}
if (!(f = fdopen(fd2, "r"))) {
errsv = errno;
nm_close(fd2);
return _get_contents_error(error, errsv, out_errsv, "failure during fdopen");
}
n_have = 0;
n_alloc = 0;
while (!feof(f)) {
gsize n_read;
n_read = fread(buf, 1, sizeof(buf), f);
errsv = errno;
if (ferror(f)) {
if (do_bzero_mem)
nm_explicit_bzero(buf, sizeof(buf));
return _get_contents_error(error, errsv, out_errsv, "error during fread");
}
if (n_have > G_MAXSIZE - 1 - n_read || n_have + n_read + 1 > max_length) {
if (do_bzero_mem)
nm_explicit_bzero(buf, sizeof(buf));
return _get_contents_error(
error,
EMSGSIZE,
out_errsv,
"file stream too large (%zu+1 bytes with maximum %zu bytes)",
(n_have > G_MAXSIZE - 1 - n_read) ? G_MAXSIZE : n_have + n_read,
max_length);
}
if (n_have + n_read + 1 >= n_alloc) {
gsize old_n_alloc = n_alloc;
if (n_alloc != 0) {
nm_assert(str);
if (n_alloc >= max_length / 2)
n_alloc = max_length;
else
n_alloc *= 2;
} else {
nm_assert(!str);
n_alloc = NM_MIN(n_read + 1, sizeof(buf));
}
if (!(str = nm_secret_mem_try_realloc_take(str,
do_bzero_mem,
old_n_alloc,
n_alloc))) {
if (do_bzero_mem)
nm_explicit_bzero(buf, sizeof(buf));
return _get_contents_error(error,
ENOMEM,
out_errsv,
"failure to allocate buffer of %zu bytes",
n_alloc);
}
}
memcpy(str + n_have, buf, n_read);
n_have += n_read;
}
if (do_bzero_mem)
nm_explicit_bzero(buf, sizeof(buf));
if (n_alloc == 0)
str = g_new0(char, 1);
else {
str[n_have] = '\0';
if (n_have + 1 < n_alloc) {
if (!(str = nm_secret_mem_try_realloc_take(str, do_bzero_mem, n_alloc, n_have + 1)))
return _get_contents_error(error,
ENOMEM,
out_errsv,
"failure to truncate buffer to %zu bytes",
n_have + 1);
}
}
NM_SET_OUT(length, n_have);
}
*contents = g_steal_pointer(&str);
NM_SET_OUT(out_errsv, 0);
return TRUE;
}
/**
* nm_utils_file_get_contents:
* @dirfd: optional file descriptor to use openat(). If negative, use plain open().
* @filename: the filename to open. Possibly relative to @dirfd.
* @max_length: allocate at most @max_length bytes.
* WARNING: see nm_utils_fd_get_contents() hint about @max_length.
* @flags: %NMUtilsFileGetContentsFlags for reading the file.
* @contents: the output buffer with the file read. It is always
* NUL terminated. The buffer is at most @max_length long, including
* the NUL byte. That is, it reads only files up to a length of
* @max_length - 1 bytes.
* @length: optional output argument of the read file size.
* @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
* @error:
*
* A reimplementation of g_file_get_contents() with a few differences:
* - accepts an @dirfd to open @filename relative to that path via openat().
* - limits the maximum filesize to max_length.
* - uses O_CLOEXEC on internal file descriptor
* - optionally returns the native errno on failure.
*
* Returns: TRUE on success.
*/
gboolean
nm_utils_file_get_contents(int dirfd,
const char *filename,
gsize max_length,
NMUtilsFileGetContentsFlags flags,
char **contents,
gsize *length,
int *out_errsv,
GError **error)
{
int fd;
g_return_val_if_fail(filename && filename[0], FALSE);
g_return_val_if_fail(contents && !*contents, FALSE);
NM_SET_OUT(length, 0);
if (dirfd >= 0) {
fd = openat(dirfd, filename, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return _get_contents_error_errno(error,
out_errsv,
"Failed to open file \"%s\" with openat",
filename);
}
} else {
fd = open(filename, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return _get_contents_error_errno(error,
out_errsv,
"Failed to open file \"%s\"",
filename);
}
}
return nm_utils_fd_get_contents(fd,
TRUE,
max_length,
flags,
contents,
length,
out_errsv,
error);
}
/*****************************************************************************/
/*
* Copied from GLib's g_file_set_contents() et al., but allows
* specifying a mode for the new file and optionally the last access
* and last modification times.
*/
gboolean
nm_utils_file_set_contents(const char *filename,
const char *contents,
gssize length,
mode_t mode,
const struct timespec *times,
int *out_errsv,
GError **error)
{
gs_free char *tmp_name = NULL;
struct stat statbuf;
int errsv;
gssize s;
int fd;
g_return_val_if_fail(filename, FALSE);
g_return_val_if_fail(contents || !length, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
g_return_val_if_fail(length >= -1, FALSE);
if (length == -1)
length = strlen(contents);
tmp_name = g_strdup_printf("%s.XXXXXX", filename);
fd = g_mkstemp_full(tmp_name, O_RDWR | O_CLOEXEC, mode);
if (fd < 0) {
return _get_contents_error_errno(error, out_errsv, "failed to create file %s", tmp_name);
}
while (length > 0) {
s = write(fd, contents, length);
if (s < 0) {
errsv = NM_ERRNO_NATIVE(errno);
if (errsv == EINTR)
continue;
nm_close(fd);
unlink(tmp_name);
return _get_contents_error(error,
errsv,
out_errsv,
"failed to write to file %s",
tmp_name);
}
g_assert(s <= length);
contents += s;
length -= s;
}
/* If the final destination exists and is > 0 bytes, we want to sync the
* newly written file to ensure the data is on disk when we rename over
* the destination. Otherwise, if we get a system crash we can lose both
* the new and the old file on some filesystems. (I.E. those that don't
* guarantee the data is written to the disk before the metadata.)
*/
if (lstat(filename, &statbuf) == 0 && statbuf.st_size > 0) {
if (fsync(fd) != 0) {
errsv = NM_ERRNO_NATIVE(errno);
nm_close(fd);
unlink(tmp_name);
return _get_contents_error(error, errsv, out_errsv, "failed to fsync %s", tmp_name);
}
}
if (times && futimens(fd, times) != 0) {
errsv = NM_ERRNO_NATIVE(errno);
nm_close(fd);
unlink(tmp_name);
return _get_contents_error(error,
errsv,
out_errsv,
"failed to set atime and mtime on %s",
tmp_name);
}
nm_close(fd);
if (rename(tmp_name, filename)) {
errsv = NM_ERRNO_NATIVE(errno);
unlink(tmp_name);
return _get_contents_error(error,
errsv,
out_errsv,
"failed rename %s to %s",
tmp_name,
filename);
}
return TRUE;
}
/**
* nm_utils_file_stat:
* @filename: the filename to stat.
* @out_st: (allow-none) (out): if given, this will be passed to stat().
*
* Just wraps stat() and gives the errno number as function result instead
* of setting the errno (though, errno is also set). It's only for convenience
* with
*
* if (nm_utils_file_stat (filename, NULL) == -ENOENT) {
* }
*
* Returns: 0 on success a negative errno on failure. */
int
nm_utils_file_stat(const char *filename, struct stat *out_st)
{
struct stat st;
if (stat(filename, out_st ?: &st) != 0)
return -NM_ERRNO_NATIVE(errno);
return 0;
}
/**
* nm_utils_fd_read:
* @fd: the fd to read from.
* @out_string: (out): output string where read bytes will be stored.
*
* Returns: <0 on failure, which is -(errno).
* 0 on EOF.
* >0 on success, which is the number of bytes read. */
gssize
nm_utils_fd_read(int fd, NMStrBuf *out_string)
{
gsize buf_available;
gssize n_read;
int errsv;
g_return_val_if_fail(fd >= 0, -1);
g_return_val_if_fail(out_string, -1);
/* If the buffer size is 0, we allocate NM_UTILS_GET_NEXT_REALLOC_SIZE_1000 (1000 bytes)
* the first time. Afterwards, the buffer grows exponentially.
*
* Note that with @buf_available, we always would read as much buffer as we actually
* have reserved. */
nm_str_buf_maybe_expand(out_string, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
buf_available = out_string->allocated - out_string->len;
n_read = read(fd, &((nm_str_buf_get_str_unsafe(out_string))[out_string->len]), buf_available);
if (n_read < 0) {
errsv = errno;
return -NM_ERRNO_NATIVE(errsv);
}
if (n_read > 0) {
nm_assert((gsize) n_read <= buf_available);
nm_str_buf_set_size(out_string, out_string->len + (gsize) n_read, TRUE, FALSE);
}
return n_read;
}
/*****************************************************************************/
typedef struct {
GSubprocess *subprocess;
GSource *timeout_source;
} SubprocessTerminateData;
static void
_subprocess_terminate_wait_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
SubprocessTerminateData *term_data = user_data;
g_subprocess_wait_finish(G_SUBPROCESS(source), result, NULL);
nm_clear_g_source_inst(&term_data->timeout_source);
g_object_unref(term_data->subprocess);
nm_g_slice_free(term_data);
}
static gboolean
_subprocess_terminate_timeout_cb(gpointer user_data)
{
SubprocessTerminateData *term_data = user_data;
nm_clear_g_source_inst(&term_data->timeout_source);
g_subprocess_send_signal(term_data->subprocess, SIGKILL);
return G_SOURCE_REMOVE;
}
void
nm_g_subprocess_terminate_in_background(GSubprocess *subprocess, int timeout_msec_before_kill)
{
SubprocessTerminateData *term_data;
GMainContext *main_context;
nm_assert(timeout_msec_before_kill > 0);
/* The GSubprocess stays alive until the child is reaped (an internal reference is held).
*
* This function first sends SIGTERM to the process right away, and after a
* timeout "timeout_msec_before_kill" send a SIGKILL.
*
* Otherwise, it does nothing, it does not log, there is no notification when the process
* completes and there is no way to abort the thing.
*
* It honors the current g_main_context_get_thread_default(). */
if (!subprocess)
return;
g_return_if_fail(G_IS_SUBPROCESS(subprocess));
main_context = g_main_context_get_thread_default();
term_data = g_slice_new(SubprocessTerminateData);
*term_data = (SubprocessTerminateData){
.subprocess = g_object_ref(subprocess),
.timeout_source = NULL,
};
g_subprocess_send_signal(subprocess, SIGTERM);
g_subprocess_wait_async(subprocess, NULL, _subprocess_terminate_wait_cb, term_data);
term_data->timeout_source =
nm_g_source_attach(nm_g_timeout_source_new(timeout_msec_before_kill,
G_PRIORITY_DEFAULT,
_subprocess_terminate_timeout_cb,
term_data,
NULL),
main_context);
}
/*****************************************************************************/
char **
nm_utils_find_mkstemp_files(const char *dirname, const char *filename)
{
static const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
DIR *dir;
struct dirent *entry;
GPtrArray *arr = NULL;
gsize l;
/* We write files with g_file_set_contents() and nm_utils_file_set_contents().
* These create temporary files using g_mkstemp_full(), with a random .XXXXXX suffix.
*
* If NetworkManager crashes while writing the file, then those temporary files are
* left over. We might want to find and delete such files.
*
* Beware: only delete such files if you are in full control about which files are
* supposed to be in the directory. For example, NetworkManager controls
* /var/lib/NetworkManager/timestamps files, and it thus takes the right to delete
* all files /var/lib/NetworkManager/timestamps.XXXXXX. That may not be appropriate
* in other cases! */
if (!dirname || !filename || !filename[0])
return NULL;
dir = opendir(dirname);
if (!dir)
return NULL;
l = strlen(filename);
while ((entry = readdir(dir))) {
const char *f = entry->d_name;
guint i;
if (strncmp(f, filename, l) != 0)
goto next;
if (f[l] != '.')
goto next;
for (i = 1; i <= 6; i++) {
/* @letters is also what g_mkstemp_full() does! */
if (!memchr(letters, f[l + i], G_N_ELEMENTS(letters)))
goto next;
}
if (f[l + 7] != '\0')
goto next;
if (!arr)
arr = g_ptr_array_new();
g_ptr_array_add(arr, g_strdup(f));
next:;
}
closedir(dir);
if (!arr)
return NULL;
g_ptr_array_add(arr, NULL);
return (char **) g_ptr_array_free(arr, FALSE);
}
/*****************************************************************************/
/* taken from systemd's sockaddr_un_set_path(). */
int
nm_io_sockaddr_un_set(struct sockaddr_un *ret, NMOptionBool is_abstract, const char *path)
{
gsize l;
g_return_val_if_fail(ret, -EINVAL);
g_return_val_if_fail(path, -EINVAL);
nm_assert_is_ternary(is_abstract);
if (is_abstract == NM_OPTION_BOOL_DEFAULT)
is_abstract = nm_io_sockaddr_un_path_is_abstract(path, &path);
l = strlen(path);
if (l < 1)
return -EINVAL;
if (l > sizeof(ret->sun_path) - 1)
return -EINVAL;
if (!is_abstract) {
if (path[0] != '/') {
/* non-abstract paths must be absolute. */
return -EINVAL;
}
}
memset(ret, 0, nm_offsetof(struct sockaddr_un, sun_path));
ret->sun_family = AF_UNIX;
if (is_abstract) {
ret->sun_path[0] = '\0';
memcpy(&ret->sun_path[1], path, NM_MIN(l + 1, sizeof(ret->sun_path) - 1));
} else
memcpy(&ret->sun_path, path, l + 1);
/* For pathname addresses, we return the size with the trailing NUL.
* For abstract addresses, we return the size without the trailing NUL
* (which may not be even written). But as abstract sockets also have
* a NUL at the beginning of sun_path, the total length is always
* calculated the same. */
return (nm_offsetof(struct sockaddr_un, sun_path) + 1) + l;
}
/*****************************************************************************/
/* taken from systemd's sd_notify(). */
int
nm_sd_notify(const char *state)
{
struct sockaddr_un sockaddr;
struct iovec iovec;
struct msghdr msghdr = {
.msg_iov = &iovec,
.msg_iovlen = 1,
.msg_name = &sockaddr,
};
nm_auto_close int fd = -1;
const char *e;
int r;
if (!state)
g_return_val_if_reached(-EINVAL);
e = getenv("NOTIFY_SOCKET");
if (!e)
return 0;
r = nm_io_sockaddr_un_set(&sockaddr, NM_OPTION_BOOL_DEFAULT, e);
if (r < 0)
return r;
msghdr.msg_namelen = r;
fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (fd < 0)
return -NM_ERRNO_NATIVE(errno);
/* systemd calls here fd_set_sndbuf(fd, SNDBUF_SIZE) .We don't bother. */
iovec = (struct iovec){
.iov_base = (gpointer) state,
.iov_len = strlen(state),
};
/* systemd sends ucred, if geteuid()/getegid() does not match getuid()/getgid(). We don't bother. */
if (sendmsg(fd, &msghdr, MSG_NOSIGNAL) < 0)
return -NM_ERRNO_NATIVE(errno);
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;
}