summaryrefslogtreecommitdiff
path: root/src/libnm-glib-aux/nm-io-utils.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libnm-glib-aux/nm-io-utils.c')
-rw-r--r--src/libnm-glib-aux/nm-io-utils.c480
1 files changed, 480 insertions, 0 deletions
diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c
new file mode 100644
index 0000000000..e02049af1a
--- /dev/null
+++ b/src/libnm-glib-aux/nm-io-utils.c
@@ -0,0 +1,480 @@
+/* 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 <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.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.
+ */
+gboolean
+nm_utils_file_set_contents(const char *filename,
+ const char *contents,
+ gssize length,
+ mode_t mode,
+ 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);
+ }
+ }
+
+ 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;
+}