diff options
Diffstat (limited to 'src/gsystem-file-utils.c')
-rw-r--r-- | src/gsystem-file-utils.c | 1644 |
1 files changed, 1644 insertions, 0 deletions
diff --git a/src/gsystem-file-utils.c b/src/gsystem-file-utils.c new file mode 100644 index 0000000..0260603 --- /dev/null +++ b/src/gsystem-file-utils.c @@ -0,0 +1,1644 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2012 William Jon McCann <mccann@redhat.com> + * Copyright (C) 2012 Colin Walters <walters@verbum.org> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <string.h> + +#define _GSYSTEM_NO_LOCAL_ALLOC +#include "libgsystem.h" +#include "gsystem-glib-compat.h" +#include <glib/gstdio.h> +#include <gio/gunixinputstream.h> +#include <gio/gfiledescriptorbased.h> +#include <gio/gunixoutputstream.h> +#include <glib-unix.h> +#include <limits.h> +#include <dirent.h> +#ifdef GSYSTEM_CONFIG_XATTRS +#include <attr/xattr.h> +#endif + +static int +close_nointr (int fd) +{ + int res; + /* Note this is NOT actually a retry loop. + * See: https://bugzilla.gnome.org/show_bug.cgi?id=682819 + */ + res = close (fd); + /* Just ignore EINTR...on Linux, retrying is wrong. */ + if (res == EINTR) + res = 0; + return res; +} + +static void +close_nointr_noerror (int fd) +{ + (void) close_nointr (fd); +} + +static int +open_nointr (const char *path, int flags, mode_t mode) +{ + int res; + do + res = open (path, flags, mode); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + return res; +} + +static inline void +_set_error_from_errno (GError **error) +{ + int errsv = errno; + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errsv), + g_strerror (errsv)); +} + +/** + * gs_file_openat_noatime: + * @dfd: File descriptor for directory + * @name: Pathname, relative to @dfd + * @ret_fd: (out): Returned file descriptor + * @cancellable: Cancellable + * @error: Error + * + * Wrapper for openat() using %O_RDONLY with %O_NOATIME if available. + */ +gboolean +gs_file_openat_noatime (int dfd, + const char *name, + int *ret_fd, + GCancellable *cancellable, + GError **error) +{ + int fd; + +#ifdef O_NOATIME + do + fd = openat (dfd, name, O_RDONLY | O_NOATIME, 0); + while (G_UNLIKELY (fd == -1 && errno == EINTR)); + /* Only the owner or superuser may use O_NOATIME; so we may get + * EPERM. EINVAL may happen if the kernel is really old... + */ + if (fd == -1 && (errno == EPERM || errno == EINVAL)) +#endif + do + fd = openat (dfd, name, O_RDONLY, 0); + while (G_UNLIKELY (fd == -1 && errno == EINTR)); + + if (fd == -1) + { + _set_error_from_errno (error); + return FALSE; + } + else + { + *ret_fd = fd; + return TRUE; + } +} + +/** + * gs_file_read_noatime: + * @file: a #GFile + * @cancellable: a #GCancellable + * @error: a #GError + * + * Like g_file_read(), but try to avoid updating the file's + * access time. This should be used by background scanning + * components such as search indexers, antivirus programs, etc. + * + * Returns: (transfer full): A new input stream, or %NULL on error + */ +GInputStream * +gs_file_read_noatime (GFile *file, + GCancellable *cancellable, + GError **error) +{ + const char *path = NULL; + int fd; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return NULL; + + path = gs_file_get_path_cached (file); + if (path == NULL) + { + char *uri; + uri = g_file_get_uri (file); + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, + "%s has no associated path", uri); + g_free (uri); + return NULL; + } + + if (!gs_file_openat_noatime (AT_FDCWD, path, &fd, cancellable, error)) + return NULL; + + return g_unix_input_stream_new (fd, TRUE); +} + +/** + * gs_stream_fstat: + * @stream: A stream containing a Unix file descriptor + * @stbuf: Memory location to write stat buffer + * @cancellable: + * @error: + * + * Some streams created via libgsystem are #GUnixInputStream; these do + * not support e.g. g_file_input_stream_query_info(). This function + * allows dropping to the raw unix fstat() call for these types of + * streams, while still conveniently wrapped with the normal GLib + * handling of @cancellable and @error. + */ +gboolean +gs_stream_fstat (GFileDescriptorBased *stream, + struct stat *stbuf, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int fd; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto out; + + fd = g_file_descriptor_based_get_fd (stream); + + if (fstat (fd, stbuf) == -1) + { + _set_error_from_errno (error); + goto out; + } + + ret = TRUE; + out: + return ret; +} + +/** + * gs_file_map_noatime: (skip) + * @file: a #GFile + * @cancellable: a #GCancellable + * @error: a #GError + * + * Like g_mapped_file_new(), but try to avoid updating the file's + * access time. This should be used by background scanning + * components such as search indexers, antivirus programs, etc. + * + * Returns: (transfer full): A new mapped file, or %NULL on error + */ +GMappedFile * +gs_file_map_noatime (GFile *file, + GCancellable *cancellable, + GError **error) +{ + const char *path; + int fd; + GMappedFile *ret; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return NULL; + + path = gs_file_get_path_cached (file); + if (path == NULL) + return NULL; + + if (!gs_file_openat_noatime (AT_FDCWD, path, &fd, cancellable, error)) + return NULL; + + ret = g_mapped_file_new_from_fd (fd, FALSE, error); + close_nointr_noerror (fd); /* Ignore errors - we always want to close */ + + return ret; +} + +#if GLIB_CHECK_VERSION(2,34,0) +/** + * gs_file_map_readonly: + * @file: a #GFile + * @cancellable: + * @error: + * + * Return a #GBytes which references a readonly view of the contents of + * @file. This function uses #GMappedFile internally. + * + * Returns: (transfer full): a newly referenced #GBytes + */ +GBytes * +gs_file_map_readonly (GFile *file, + GCancellable *cancellable, + GError **error) +{ + GMappedFile *mfile; + GBytes *ret; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return NULL; + + mfile = g_mapped_file_new (gs_file_get_path_cached (file), FALSE, error); + if (!mfile) + return NULL; + + ret = g_mapped_file_get_bytes (mfile); + g_mapped_file_unref (mfile); + return ret; +} +#endif + +/** + * gs_file_sync_data: + * @file: a #GFile + * @cancellable: + * @error: + * + * Wraps the UNIX fsync() function (or fdatasync(), if available), which + * ensures that the data in @file is on non-volatile storage. + */ +gboolean +gs_file_sync_data (GFile *file, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int res; + int fd = -1; + + if (!gs_file_openat_noatime (AT_FDCWD, gs_file_get_path_cached (file), &fd, + cancellable, error)) + goto out; + + do + { +#ifdef __linux + res = fdatasync (fd); +#else + res = fsync (fd); +#endif + } + while (G_UNLIKELY (res != 0 && errno == EINTR)); + if (res != 0) + { + _set_error_from_errno (error); + goto out; + } + + res = close_nointr (fd); + if (res != 0) + { + _set_error_from_errno (error); + goto out; + } + fd = -1; + + ret = TRUE; + out: + if (fd != -1) + close_nointr_noerror (fd); + return ret; +} + +/** + * gs_file_create: + * @file: Path to non-existent file + * @mode: Unix access permissions + * @out_stream: (out) (transfer full) (allow-none): Newly created output, or %NULL + * @cancellable: a #GCancellable + * @error: a #GError + * + * Like g_file_create(), except this function allows specifying the + * access mode. This allows atomically creating private files. + */ +gboolean +gs_file_create (GFile *file, + int mode, + GOutputStream **out_stream, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int fd; + GOutputStream *ret_stream = NULL; + + fd = open_nointr (gs_file_get_path_cached (file), O_WRONLY | O_CREAT | O_EXCL, mode); + if (fd < 0) + { + _set_error_from_errno (error); + goto out; + } + + if (fchmod (fd, mode) < 0) + { + close (fd); + _set_error_from_errno (error); + goto out; + } + + ret_stream = g_unix_output_stream_new (fd, TRUE); + + ret = TRUE; + gs_transfer_out_value (out_stream, &ret_stream); + out: + g_clear_object (&ret_stream); + return ret; +} + +static const char * +get_default_tmp_prefix (void) +{ + static char *tmpprefix = NULL; + + if (g_once_init_enter (&tmpprefix)) + { + const char *prgname = g_get_prgname (); + const char *p; + char *prefix; + char *iter; + + if (prgname) + { + p = strrchr (prgname, '/'); + if (p) + prgname = p + 1; + } + else + prgname = ""; + + prefix = g_strdup_printf ("tmp-%s%u-", prgname, getuid ()); + for (iter = prefix; *iter; iter++) + { + char c = *iter; + if (c == ' ') + *iter = '_'; + } + + g_once_init_leave (&tmpprefix, prefix); + } + + return tmpprefix; +} + +/** + * gs_fileutil_gen_tmp_name: + * @prefix: (allow-none): String prepended to the result + * @suffix: (allow-none): String suffixed to the result + * + * Generate a name suitable for use as a temporary file. This + * function does no I/O; it is not guaranteed that a file with that + * name does not exist. + */ +char * +gs_fileutil_gen_tmp_name (const char *prefix, + const char *suffix) +{ + static const char table[] = "ABCEDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789"; + GString *str = g_string_new (""); + guint i; + + if (!prefix) + prefix = get_default_tmp_prefix (); + if (!suffix) + suffix = "tmp"; + + g_string_append (str, prefix); + for (i = 0; i < 8; i++) + { + int offset = g_random_int_range (0, sizeof (table) - 1); + g_string_append_c (str, (guint8)table[offset]); + } + g_string_append_c (str, '.'); + g_string_append (str, suffix); + + return g_string_free (str, FALSE); +} + +/** + * gs_file_open_dir_fd: + * @path: Directory name + * @out_fd: (out): File descriptor for directory + * @cancellable: Cancellable + * @error: Error + * + * On success, sets @out_fd to a file descriptor for the directory + * that can be used with UNIX functions such as openat(). + */ +gboolean +gs_file_open_dir_fd (GFile *path, + int *out_fd, + GCancellable *cancellable, + GError **error) +{ + /* Linux specific probably */ + *out_fd = open (gs_file_get_path_cached (path), O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC); + if (*out_fd == -1) + { + _set_error_from_errno (error); + return FALSE; + } + return TRUE; +} + +/** + * gs_file_open_in_tmpdir_at: + * @tmpdir_fd: Directory to place temporary file + * @mode: Default mode (will be affected by umask) + * @out_name: (out) (transfer full): Newly created file name + * @out_stream: (out) (transfer full) (allow-none): Newly created output stream + * @cancellable: + * @error: + * + * Like g_file_open_tmp(), except the file will be created in the + * provided @tmpdir, and allows specification of the Unix @mode, which + * means private files may be created. Return values will be stored + * in @out_name, and optionally @out_stream. + */ +gboolean +gs_file_open_in_tmpdir_at (int tmpdir_fd, + int mode, + char **out_name, + GOutputStream **out_stream, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + const int max_attempts = 128; + int i; + char *tmp_name = NULL; + int fd; + + /* 128 attempts seems reasonable... */ + for (i = 0; i < max_attempts; i++) + { + g_free (tmp_name); + tmp_name = gs_fileutil_gen_tmp_name (NULL, NULL); + + do + fd = openat (tmpdir_fd, tmp_name, O_WRONLY | O_CREAT | O_EXCL, mode); + while (fd == -1 && errno == EINTR); + if (fd < 0 && errno != EEXIST) + { + _set_error_from_errno (error); + goto out; + } + else if (fd != -1) + break; + } + if (i == max_attempts) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Exhausted attempts to open temporary file"); + goto out; + } + + ret = TRUE; + gs_transfer_out_value (out_name, &tmp_name); + if (out_stream) + *out_stream = g_unix_output_stream_new (fd, TRUE); + else + (void) close (fd); + out: + g_free (tmp_name); + return ret; +} + +/** + * gs_file_open_in_tmpdir: + * @tmpdir: Directory to place temporary file + * @mode: Default mode (will be affected by umask) + * @out_file: (out) (transfer full): Newly created file path + * @out_stream: (out) (transfer full) (allow-none): Newly created output stream + * @cancellable: + * @error: + * + * Like g_file_open_tmp(), except the file will be created in the + * provided @tmpdir, and allows specification of the Unix @mode, which + * means private files may be created. Return values will be stored + * in @out_file, and optionally @out_stream. + */ +gboolean +gs_file_open_in_tmpdir (GFile *tmpdir, + int mode, + GFile **out_file, + GOutputStream **out_stream, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + DIR *d = NULL; + int dfd = -1; + char *tmp_name = NULL; + GOutputStream *ret_stream = NULL; + + d = opendir (gs_file_get_path_cached (tmpdir)); + if (!d) + { + _set_error_from_errno (error); + goto out; + } + dfd = dirfd (d); + + if (!gs_file_open_in_tmpdir_at (dfd, mode, &tmp_name, + out_stream ? &ret_stream : NULL, + cancellable, error)) + goto out; + + ret = TRUE; + *out_file = g_file_get_child (tmpdir, tmp_name); + gs_transfer_out_value (out_stream, &ret_stream); + out: + if (d) (void) closedir (d); + g_clear_object (&ret_stream); + g_free (tmp_name); + return ret; +} + +static gboolean +linkcopy_internal_attempt (GFile *src, + GFile *dest, + GFile *dest_parent, + GFileCopyFlags flags, + gboolean sync_data, + gboolean enable_guestfs_fuse_workaround, + gboolean *out_try_again, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int res; + char *tmp_name = NULL; + GFile *tmp_dest = NULL; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + goto out; + + tmp_name = gs_fileutil_gen_tmp_name (NULL, NULL); + tmp_dest = g_file_get_child (dest_parent, tmp_name); + + res = link (gs_file_get_path_cached (src), gs_file_get_path_cached (tmp_dest)); + if (res == -1) + { + if (errno == EEXIST) + { + /* Nothing, fall through */ + *out_try_again = TRUE; + ret = TRUE; + goto out; + } + else if (errno == EXDEV || errno == EMLINK || errno == EPERM + || (enable_guestfs_fuse_workaround && errno == ENOENT)) + { + if (!g_file_copy (src, tmp_dest, flags, + cancellable, NULL, NULL, error)) + goto out; + } + else + { + _set_error_from_errno (error); + goto out; + } + } + + if (sync_data) + { + /* Now, we need to fsync */ + if (!gs_file_sync_data (tmp_dest, cancellable, error)) + goto out; + } + + if (!gs_file_rename (tmp_dest, dest, cancellable, error)) + goto out; + + ret = TRUE; + *out_try_again = FALSE; + out: + g_clear_pointer (&tmp_name, g_free); + g_clear_object (&tmp_dest); + return ret; +} + +static gboolean +linkcopy_internal (GFile *src, + GFile *dest, + GFileCopyFlags flags, + gboolean sync_data, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + gboolean dest_exists; + int i; + gboolean enable_guestfs_fuse_workaround; + struct stat src_stat; + struct stat dest_stat; + GFile *dest_parent = NULL; + + flags |= G_FILE_COPY_NOFOLLOW_SYMLINKS; + + g_return_val_if_fail ((flags & (G_FILE_COPY_BACKUP | G_FILE_COPY_TARGET_DEFAULT_PERMS)) == 0, FALSE); + + dest_parent = g_file_get_parent (dest); + + if (lstat (gs_file_get_path_cached (src), &src_stat) == -1) + { + int errsv = errno; + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errno), + g_strerror (errsv)); + goto out; + } + + if (lstat (gs_file_get_path_cached (dest), &dest_stat) == -1) + dest_exists = FALSE; + else + dest_exists = TRUE; + + if (((flags & G_FILE_COPY_OVERWRITE) == 0) && dest_exists) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_EXISTS, + "File exists"); + goto out; + } + + /* Work around the behavior of link() where it's a no-op if src and + * dest are the same. + */ + if (dest_exists && + src_stat.st_dev == dest_stat.st_dev && + src_stat.st_ino == dest_stat.st_ino) + { + ret = TRUE; + goto out; + } + + enable_guestfs_fuse_workaround = getenv ("LIBGSYSTEM_ENABLE_GUESTFS_FUSE_WORKAROUND") != NULL; + + /* 128 attempts seems reasonable... */ + for (i = 0; i < 128; i++) + { + gboolean tryagain = FALSE; + + if (!linkcopy_internal_attempt (src, dest, dest_parent, + flags, sync_data, + enable_guestfs_fuse_workaround, + &tryagain, + cancellable, error)) + goto out; + + if (!tryagain) + break; + } + + ret = TRUE; + out: + g_clear_object (&dest_parent); + return ret; + +} + +/** + * gs_file_linkcopy: + * @src: Source file + * @dest: Destination file + * @flags: flags + * @cancellable: + * @error: + * + * First tries to use the UNIX link() call, but if the files are on + * separate devices, fall back to copying via g_file_copy(). + * + * The given @flags have different semantics than those documented + * when hardlinking is used. Specifically, both + * #G_FILE_COPY_TARGET_DEFAULT_PERMS and #G_FILE_COPY_BACKUP are not + * supported. #G_FILE_COPY_NOFOLLOW_SYMLINKS treated as if it was + * always given - if you want to follow symbolic links, you will need + * to resolve them manually. + * + * Beware - do not use this function if @src may be modified, and it's + * undesirable for the changes to also be reflected in @dest. The + * best use of this function is in the case where @src and @dest are + * read-only, or where @src is a temporary file, and you want to put + * it in the final place. + */ +gboolean +gs_file_linkcopy (GFile *src, + GFile *dest, + GFileCopyFlags flags, + GCancellable *cancellable, + GError **error) +{ + return linkcopy_internal (src, dest, flags, FALSE, cancellable, error); +} + +/** + * gs_file_linkcopy_sync_data: + * @src: Source file + * @dest: Destination file + * @flags: flags + * @cancellable: + * @error: + * + * This function is similar to gs_file_linkcopy(), except it also uses + * gs_file_sync_data() to ensure that @dest is in stable storage + * before it is moved into place. + */ +gboolean +gs_file_linkcopy_sync_data (GFile *src, + GFile *dest, + GFileCopyFlags flags, + GCancellable *cancellable, + GError **error) +{ + return linkcopy_internal (src, dest, flags, TRUE, cancellable, error); +} + +static char * +gs_file_get_target_path (GFile *file) +{ + GFileInfo *info; + const char *target; + char *path; + + info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (info == NULL) + return NULL; + target = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + path = g_filename_from_uri (target, NULL, NULL); + g_object_unref (info); + + return path; +} + +G_LOCK_DEFINE_STATIC (pathname_cache); + +/** + * gs_file_get_path_cached: + * + * Like g_file_get_path(), but returns a constant copy so callers + * don't need to free the result. + */ +const char * +gs_file_get_path_cached (GFile *file) +{ + const char *path; + static GQuark _file_path_quark = 0; + + if (G_UNLIKELY (_file_path_quark) == 0) + _file_path_quark = g_quark_from_static_string ("gsystem-file-path"); + + G_LOCK (pathname_cache); + + path = g_object_get_qdata ((GObject*)file, _file_path_quark); + if (!path) + { + if (g_file_has_uri_scheme (file, "trash") || + g_file_has_uri_scheme (file, "recent")) + path = gs_file_get_target_path (file); + else + path = g_file_get_path (file); + if (path == NULL) + { + G_UNLOCK (pathname_cache); + return NULL; + } + g_object_set_qdata_full ((GObject*)file, _file_path_quark, (char*)path, (GDestroyNotify)g_free); + } + + G_UNLOCK (pathname_cache); + + return path; +} + +/** + * gs_file_get_basename_cached: + * + * Like g_file_get_basename(), but returns a constant copy so callers + * don't need to free the result. + */ +const char * +gs_file_get_basename_cached (GFile *file) +{ + const char *name; + static GQuark _file_name_quark = 0; + + if (G_UNLIKELY (_file_name_quark) == 0) + _file_name_quark = g_quark_from_static_string ("gsystem-file-name"); + + G_LOCK (pathname_cache); + + name = g_object_get_qdata ((GObject*)file, _file_name_quark); + if (!name) + { + name = g_file_get_basename (file); + g_object_set_qdata_full ((GObject*)file, _file_name_quark, (char*)name, (GDestroyNotify)g_free); + } + + G_UNLOCK (pathname_cache); + + return name; +} + +/** + * gs_file_enumerator_iterate: + * @direnum: an open #GFileEnumerator + * @out_info: (out) (transfer none) (allow-none): Output location for the next #GFileInfo + * @out_child: (out) (transfer none) (allow-none): Output location for the next #GFile, or %NULL + * @cancellable: a #GCancellable + * @error: a #GError + * + * This is a version of g_file_enumerator_next_file() that's easier to + * use correctly from C programs. With g_file_enumerator_next_file(), + * the gboolean return value signifies "end of iteration or error", which + * requires allocation of a temporary #GError. + * + * In contrast, with this function, a %FALSE return from + * gs_file_enumerator_iterate() <emphasis>always</emphasis> means + * "error". End of iteration is signaled by @out_info being %NULL. + * + * Another crucial difference is that the references for @out_info and + * @out_child are owned by @direnum (they are cached as hidden + * properties). You must not unref them in your own code. This makes + * memory management significantly easier for C code in combination + * with loops. + * + * Finally, this function optionally allows retrieving a #GFile as + * well. + * + * The code pattern for correctly using gs_file_enumerator_iterate() from C + * is: + * + * |[ + * direnum = g_file_enumerate_children (file, ...); + * while (TRUE) + * { + * GFileInfo *info; + * if (!gs_file_enumerator_iterate (direnum, &info, NULL, cancellable, error)) + * goto out; + * if (!info) + * break; + * ... do stuff with "info"; do not unref it! ... + * } + * + * out: + * g_object_unref (direnum); // Note: frees the last @info + * ]| + */ +gboolean +gs_file_enumerator_iterate (GFileEnumerator *direnum, + GFileInfo **out_info, + GFile **out_child, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + GError *temp_error = NULL; + + static GQuark cached_info_quark; + static GQuark cached_child_quark; + static gsize quarks_initialized; + + g_return_val_if_fail (direnum != NULL, FALSE); + g_return_val_if_fail (out_info != NULL, FALSE); + + if (g_once_init_enter (&quarks_initialized)) + { + cached_info_quark = g_quark_from_static_string ("gsystem-cached-info"); + cached_child_quark = g_quark_from_static_string ("gsystem-cached-child"); + g_once_init_leave (&quarks_initialized, 1); + } + + + *out_info = g_file_enumerator_next_file (direnum, cancellable, &temp_error); + if (out_child) + *out_child = NULL; + if (temp_error != NULL) + { + g_propagate_error (error, temp_error); + goto out; + } + else if (*out_info != NULL) + { + g_object_set_qdata_full ((GObject*)direnum, cached_info_quark, *out_info, (GDestroyNotify)g_object_unref); + if (out_child != NULL) + { + const char *name = g_file_info_get_name (*out_info); + *out_child = g_file_get_child (g_file_enumerator_get_container (direnum), name); + g_object_set_qdata_full ((GObject*)direnum, cached_child_quark, *out_child, (GDestroyNotify)g_object_unref); + } + } + + ret = TRUE; + out: + return ret; +} + +/** + * gs_file_rename: + * @from: Current path + * @to: New path + * @cancellable: a #GCancellable + * @error: a #GError + * + * This function wraps the raw Unix function rename(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +gs_file_rename (GFile *from, + GFile *to, + GCancellable *cancellable, + GError **error) +{ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + if (rename (gs_file_get_path_cached (from), + gs_file_get_path_cached (to)) < 0) + { + _set_error_from_errno (error); + return FALSE; + } + return TRUE; +} + +/** + * gs_file_unlink: + * @path: Path to file + * @cancellable: a #GCancellable + * @error: a #GError + * + * Like g_file_delete(), except this function does not follow Unix + * symbolic links, and will delete a symbolic link even if it's + * pointing to a nonexistent file. In other words, this function + * merely wraps the raw Unix function unlink(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +gs_file_unlink (GFile *path, + GCancellable *cancellable, + GError **error) +{ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + if (unlink (gs_file_get_path_cached (path)) < 0) + { + _set_error_from_errno (error); + return FALSE; + } + return TRUE; +} + +static gboolean +chown_internal (GFile *path, + gboolean dereference_links, + guint32 owner, + guint32 group, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int res; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + do + if (dereference_links) + res = chown (gs_file_get_path_cached (path), owner, group); + else + res = lchown (gs_file_get_path_cached (path), owner, group); + while (G_UNLIKELY (res != 0 && errno == EINTR)); + + if (res < 0) + { + _set_error_from_errno (error); + goto out; + } + + ret = TRUE; + out: + return ret; +} + +/** + * gs_file_chown: + * @path: Path to file + * @owner: UNIX owner + * @group: UNIX group + * @cancellable: a #GCancellable + * @error: a #GError + * + * Merely wraps UNIX chown(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +gs_file_chown (GFile *path, + guint32 owner, + guint32 group, + GCancellable *cancellable, + GError **error) +{ + return chown_internal (path, TRUE, owner, group, cancellable, error); +} + +/** + * gs_file_lchown: + * @path: Path to file + * @owner: UNIX owner + * @group: UNIX group + * @cancellable: a #GCancellable + * @error: a #GError + * + * Merely wraps UNIX lchown(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +gs_file_lchown (GFile *path, + guint32 owner, + guint32 group, + GCancellable *cancellable, + GError **error) +{ + return chown_internal (path, FALSE, owner, group, cancellable, error); +} + +/** + * gs_file_chmod: + * @path: Path to file + * @mode: UNIX mode + * @cancellable: a #GCancellable + * @error: a #GError + * + * Merely wraps UNIX chmod(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +gs_file_chmod (GFile *path, + guint mode, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + int res; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + do + res = chmod (gs_file_get_path_cached (path), mode); + while (G_UNLIKELY (res != 0 && errno == EINTR)); + + if (res < 0) + { + _set_error_from_errno (error); + goto out; + } + + ret = TRUE; + out: + return ret; +} + +/** + * gs_file_ensure_directory: + * @dir: Path to create as directory + * @with_parents: Also create parent directories + * @cancellable: a #GCancellable + * @error: a #GError + * + * Like g_file_make_directory(), except does not throw an error if the + * directory already exists. + */ +gboolean +gs_file_ensure_directory (GFile *dir, + gboolean with_parents, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + GError *temp_error = NULL; + GFile *parent = NULL; + + if (!g_file_make_directory (dir, cancellable, &temp_error)) + { + if (with_parents && + g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + g_clear_error (&temp_error); + + parent = g_file_get_parent (dir); + if (parent) + { + if (!gs_file_ensure_directory (parent, TRUE, cancellable, error)) + goto out; + } + if (!gs_file_ensure_directory (dir, FALSE, cancellable, error)) + goto out; + } + else if (!g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + { + g_propagate_error (error, temp_error); + goto out; + } + else + g_clear_error (&temp_error); + } + + ret = TRUE; + out: + g_clear_object (&parent); + return ret; +} + +/** + * gs_file_ensure_directory_mode: + * @dir: Path to create as directory + * @mode: Create directory with these permissions + * @cancellable: a #GCancellable + * @error: a #GError + * + * Wraps UNIX mkdir() function with support for @cancellable, and + * uses @error instead of errno. + */ +gboolean +gs_file_ensure_directory_mode (GFile *dir, + guint mode, + GCancellable *cancellable, + GError **error) +{ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + if (mkdir (gs_file_get_path_cached (dir), mode) == -1 && errno != EEXIST) + { + _set_error_from_errno (error); + return FALSE; + } + return TRUE; +} + +/** + * gs_file_load_contents_utf8: + * @file: Path to file whose contents must be UTF-8 + * @cancellable: + * @error: + * + * Like g_file_load_contents(), except validates the contents are + * UTF-8. + */ +gchar * +gs_file_load_contents_utf8 (GFile *file, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + gsize len; + char *ret_contents = NULL; + + if (!g_file_load_contents (file, cancellable, &ret_contents, &len, + NULL, error)) + goto out; + if (!g_utf8_validate (ret_contents, len, NULL)) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Invalid UTF-8"); + goto out; + } + + ret = TRUE; + out: + if (!ret) + { + g_free (ret_contents); + return NULL; + } + return ret_contents; +} + +static int +path_common_directory (char *one, + char *two) +{ + int dir_index = 0; + int i = 0; + + while (*one && *two) + { + if (*one != *two) + break; + if (*one == '/') + dir_index = i + 1; + + one++; + two++; + i++; + } + + return dir_index; +} + +/** + * gs_file_get_relpath: + * @one: The first #GFile + * @two: The second #GFile + * + * Like gs_file_get_relative_path(), but does not mandate that + * the two files have any parent in common. This function will + * instead insert "../" where appropriate. + * + * Returns: (transfer full): The relative path between the two. + */ +gchar * +gs_file_get_relpath (GFile *one, + GFile *two) +{ + gchar *simple_path; + gchar *one_path, *one_suffix; + gchar *two_path, *two_suffix; + GString *path; + int i; + + simple_path = g_file_get_relative_path (one, two); + if (simple_path) + return simple_path; + + one_path = g_file_get_path (one); + two_path = g_file_get_path (two); + + i = path_common_directory (one_path, two_path); + one_suffix = one_path + i; + two_suffix = two_path + i; + + path = g_string_new (""); + + /* For every leftover path segment one has, append "../" so + * that we reach the same directory. */ + while (*one_suffix) + { + g_string_append (path, "../"); + one_suffix = strchr (one_suffix, '/'); + if (one_suffix == NULL) + break; + one_suffix++; + } + + /* And now append the leftover stuff on two's side. */ + g_string_append (path, two_suffix); + + g_free (one_path); + g_free (two_path); + + return g_string_free (path, FALSE); +} + +/** + * gs_file_realpath: + * @file: A #GFile + * + * Return a #GFile that contains the same path with symlinks + * followed. That is, it's a #GFile whose path is the result + * of calling realpath() on @file. + * + * Returns: (allow-none) (transfer full): A new #GFile or %NULL if @file is invalid + */ +GFile * +gs_file_realpath (GFile *file) +{ + gchar *path; + gchar path_real[PATH_MAX]; + + path = g_file_get_path (file); + + if (realpath ((const char *) path, path_real) == NULL) + { + g_free (path); + return NULL; + } + + g_free (path); + return g_file_new_for_path (path_real); +} + +#ifdef GSYSTEM_CONFIG_XATTRS +static char * +canonicalize_xattrs (char *xattr_string, + size_t len) +{ + char *p; + GSList *xattrs = NULL; + GSList *iter; + GString *result; + + result = g_string_new (0); + + p = xattr_string; + while (p < xattr_string+len) + { + xattrs = g_slist_prepend (xattrs, p); + p += strlen (p) + 1; + } + + xattrs = g_slist_sort (xattrs, (GCompareFunc) strcmp); + for (iter = xattrs; iter; iter = iter->next) { + g_string_append (result, iter->data); + g_string_append_c (result, '\0'); + } + + g_slist_free (xattrs); + return g_string_free (result, FALSE); +} + +static GVariant * +variant_new_ay_bytes (GBytes *bytes) +{ + gsize size; + gconstpointer data; + data = g_bytes_get_data (bytes, &size); + g_bytes_ref (bytes); + return g_variant_new_from_data (G_VARIANT_TYPE ("ay"), data, size, + TRUE, (GDestroyNotify)g_bytes_unref, bytes); +} + +static gboolean +read_xattr_name_array (const char *path, + const char *xattrs, + size_t len, + GVariantBuilder *builder, + GError **error) +{ + gboolean ret = FALSE; + const char *p; + + p = xattrs; + while (p < xattrs+len) + { + ssize_t bytes_read; + char *buf; + GBytes *bytes = NULL; + + bytes_read = lgetxattr (path, p, NULL, 0); + if (bytes_read < 0) + { + _set_error_from_errno (error); + g_prefix_error (error, "lgetxattr (%s, %s) failed: ", path, p); + goto out; + } + if (bytes_read == 0) + continue; + + buf = g_malloc (bytes_read); + bytes = g_bytes_new_take (buf, bytes_read); + if (lgetxattr (path, p, buf, bytes_read) < 0) + { + g_bytes_unref (bytes); + _set_error_from_errno (error); + g_prefix_error (error, "lgetxattr (%s, %s) failed: ", path, p); + goto out; + } + + g_variant_builder_add (builder, "(@ay@ay)", + g_variant_new_bytestring (p), + variant_new_ay_bytes (bytes)); + + p = p + strlen (p) + 1; + g_bytes_unref (bytes); + } + + ret = TRUE; + out: + return ret; +} +#endif + +static gboolean +get_xattrs_impl (GFile *f, + GVariantBuilder *builder, + GCancellable *cancellable, + GError **error) +{ +#ifdef GSYSTEM_CONFIG_XATTRS + gboolean ret = FALSE; + const char *path; + ssize_t bytes_read; + char *xattr_names = NULL; + char *xattr_names_canonical = NULL; + + path = gs_file_get_path_cached (f); + + bytes_read = llistxattr (path, NULL, 0); + + if (bytes_read < 0) + { + if (errno != ENOTSUP) + { + _set_error_from_errno (error); + g_prefix_error (error, "llistxattr (%s) failed: ", path); + goto out; + } + } + else if (bytes_read > 0) + { + xattr_names = g_malloc (bytes_read); + if (llistxattr (path, xattr_names, bytes_read) < 0) + { + _set_error_from_errno (error); + g_prefix_error (error, "llistxattr (%s) failed: ", path); + goto out; + } + xattr_names_canonical = canonicalize_xattrs (xattr_names, bytes_read); + + if (!read_xattr_name_array (path, xattr_names_canonical, bytes_read, builder, error)) + goto out; + } + + ret = TRUE; + out: + g_clear_pointer (&xattr_names, g_free); + g_clear_pointer (&xattr_names_canonical, g_free); + return ret; +#else + return TRUE; +#endif +} + +/** + * gs_file_get_all_xattrs: + * @f: a #GFile + * @out_xattrs: (out): A new #GVariant containing the extended attributes + * @cancellable: Cancellable + * @error: Error + * + * Read all extended attributes of @f in a canonical sorted order, and + * set @out_xattrs with the result. + * + * If the filesystem does not support extended attributes, @out_xattrs + * will have 0 elements, and this function will return successfully. + */ +gboolean +gs_file_get_all_xattrs (GFile *f, + GVariant **out_xattrs, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + GVariantBuilder builder; + gboolean builder_initialized = FALSE; + GVariant *ret_xattrs = NULL; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ayay)")); + builder_initialized = TRUE; + + if (!get_xattrs_impl (f, &builder, + cancellable, error)) + goto out; + + ret_xattrs = g_variant_builder_end (&builder); + builder_initialized = FALSE; + g_variant_ref_sink (ret_xattrs); + + ret = TRUE; + gs_transfer_out_value (out_xattrs, &ret_xattrs); + out: + g_clear_pointer (&ret_xattrs, g_variant_unref); + if (!builder_initialized) + g_variant_builder_clear (&builder); + return ret; +} + +/** + * gs_fd_set_all_xattrs: + * @fd: File descriptor + * @xattrs: Extended attributes + * @cancellable: Cancellable + * @error: Error + * + * For each attribute in @xattrs, set its value on the file or + * directory referred to by @fd. This function does not remove any + * attributes not in @xattrs. + */ +gboolean +gs_fd_set_all_xattrs (int fd, + GVariant *xattrs, + GCancellable *cancellable, + GError **error) +{ +#ifdef GSYSTEM_CONFIG_XATTRS + gboolean ret = FALSE; + int i, n; + + n = g_variant_n_children (xattrs); + for (i = 0; i < n; i++) + { + const guint8* name; + const guint8* value_data; + GVariant *value = NULL; + gsize value_len; + int res; + + g_variant_get_child (xattrs, i, "(^&ay@ay)", + &name, &value); + value_data = g_variant_get_fixed_array (value, &value_len, 1); + + do + res = fsetxattr (fd, (char*)name, (char*)value_data, value_len, 0); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + g_variant_unref (value); + if (G_UNLIKELY (res == -1)) + { + _set_error_from_errno (error); + goto out; + } + } + + ret = TRUE; + out: + return ret; +#else + return TRUE; +#endif +} + +/** + * gs_file_set_all_xattrs: + * @file: File descriptor + * @xattrs: Extended attributes + * @cancellable: Cancellable + * @error: Error + * + * For each attribute in @xattrs, set its value on the file or + * directory referred to by @file. This function does not remove any + * attributes not in @xattrs. + */ +gboolean +gs_file_set_all_xattrs (GFile *file, + GVariant *xattrs, + GCancellable *cancellable, + GError **error) +{ +#ifdef GSYSTEM_CONFIG_XATTRS + gboolean ret = FALSE; + const char *path; + int i, n; + + path = gs_file_get_path_cached (file); + + n = g_variant_n_children (xattrs); + for (i = 0; i < n; i++) + { + const guint8* name; + GVariant *value; + const guint8* value_data; + gsize value_len; + gboolean loop_err; + + g_variant_get_child (xattrs, i, "(^&ay@ay)", + &name, &value); + value_data = g_variant_get_fixed_array (value, &value_len, 1); + + loop_err = lsetxattr (path, (char*)name, (char*)value_data, value_len, 0) < 0; + g_clear_pointer (&value, (GDestroyNotify) g_variant_unref); + if (loop_err) + { + _set_error_from_errno (error); + g_prefix_error (error, "lsetxattr (%s, %s) failed: ", path, name); + goto out; + } + } + + ret = TRUE; + out: + return ret; +#else + return TRUE; +#endif +} |