Files
NetworkManager/shared/nm-utils/nm-io-utils.c
Thomas Haller ff163d9d0d shared: move file-get-contents and file-set-contents helper to shared/
These functions are not specific to "src/". Also, they will be needed
by outside of "src/" soon.
2018-09-04 07:38:30 +02:00

431 lines
12 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* NetworkManager -- Network link manager
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
* (C) Copyright 2018 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-io-utils.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "nm-shared-utils.h"
#include "nm-secret-utils.h"
/*****************************************************************************/
_nm_printf (3, 4)
static int
_get_contents_error (GError **error, int errsv, const char *format, ...)
{
if (errsv < 0)
errsv = -errsv;
else if (!errsv)
errsv = errno;
if (error) {
char *msg;
va_list args;
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, g_strerror (errsv));
g_free (msg);
}
return -errsv;
}
static char *
_mem_realloc (char *old, gboolean do_bzero_mem, gsize cur_len, gsize new_len)
{
char *new;
/* re-allocating to zero bytes is an odd case. We don't need it
* and it's not supported. */
nm_assert (new_len > 0);
/* regardless of success/failure, @old will always be freed/consumed. */
if (do_bzero_mem && cur_len > 0) {
new = g_try_malloc (new_len);
if (new)
memcpy (new, old, NM_MIN (cur_len, new_len));
nm_explicit_bzero (old, cur_len);
g_free (old);
} else {
new = g_try_realloc (old, new_len);
if (!new)
g_free (old);
}
return new;
}
/**
* 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.
*
* 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 maxium filesize to max_length.
*
* Returns: a negative error code on failure.
*/
int
nm_utils_fd_get_contents (int fd,
gboolean close_fd,
gsize max_length,
NMUtilsFileGetContentsFlags flags,
char **contents,
gsize *length,
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);
g_return_val_if_fail (fd >= 0, -EINVAL);
g_return_val_if_fail (contents, -EINVAL);
g_return_val_if_fail (!error || !*error, -EINVAL);
if (fstat (fd, &stat_buf) < 0)
return _get_contents_error (error, 0, "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, "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, "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, "error reading %zu bytes from file descriptor", n_stat);
}
str[n_read] = '\0';
if (n_read < n_stat) {
if (!(str = _mem_realloc (str, do_bzero_mem, n_stat + 1, n_read + 1)))
return _get_contents_error (error, ENOMEM, "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 (error, 0, "error during dup");
}
if (!(f = fdopen (fd2, "r"))) {
nm_close (fd2);
return _get_contents_error (error, 0, "failure during fdopen");
}
n_have = 0;
n_alloc = 0;
while (!feof (f)) {
int errsv;
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, "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, "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 = _mem_realloc (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, "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 = _mem_realloc (str, do_bzero_mem, n_alloc, n_have + 1)))
return _get_contents_error (error, ENOMEM, "failure to truncate buffer to %zu bytes", n_have + 1);
}
}
NM_SET_OUT (length, n_have);
}
*contents = g_steal_pointer (&str);
return 0;
}
/**
* 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.
*
* 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 maxium filesize to max_length.
* - uses O_CLOEXEC on internal file descriptor
*
* Returns: a negative error code on failure.
*/
int
nm_utils_file_get_contents (int dirfd,
const char *filename,
gsize max_length,
NMUtilsFileGetContentsFlags flags,
char **contents,
gsize *length,
GError **error)
{
int fd;
int errsv;
g_return_val_if_fail (filename && filename[0], -EINVAL);
if (dirfd >= 0) {
fd = openat (dirfd, filename, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
errsv = errno;
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"Failed to open file \"%s\" with openat: %s",
filename,
g_strerror (errsv));
return -errsv;
}
} else {
fd = open (filename, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
errsv = errno;
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"Failed to open file \"%s\": %s",
filename,
g_strerror (errsv));
return -errsv;
}
}
return nm_utils_fd_get_contents (fd,
TRUE,
max_length,
flags,
contents,
length,
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,
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, mode);
if (fd < 0) {
errsv = errno;
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"failed to create file %s: %s",
tmp_name,
g_strerror (errsv));
return FALSE;
}
while (length > 0) {
s = write (fd, contents, length);
if (s < 0) {
errsv = errno;
if (errsv == EINTR)
continue;
nm_close (fd);
unlink (tmp_name);
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"failed to write to file %s: %s",
tmp_name,
g_strerror (errsv));
return FALSE;
}
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
&& fsync (fd) != 0) {
errsv = errno;
nm_close (fd);
unlink (tmp_name);
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"failed to fsync %s: %s",
tmp_name,
g_strerror (errsv));
return FALSE;
}
nm_close (fd);
if (rename (tmp_name, filename)) {
errsv = errno;
unlink (tmp_name);
g_set_error (error,
G_FILE_ERROR,
g_file_error_from_errno (errsv),
"failed to rename %s to %s: %s",
tmp_name,
filename,
g_strerror (errsv));
return FALSE;
}
return TRUE;
}