summaryrefslogtreecommitdiff
path: root/glib/gpathbuf.c
diff options
context:
space:
mode:
Diffstat (limited to 'glib/gpathbuf.c')
-rw-r--r--glib/gpathbuf.c598
1 files changed, 598 insertions, 0 deletions
diff --git a/glib/gpathbuf.c b/glib/gpathbuf.c
new file mode 100644
index 000000000..c7cf04849
--- /dev/null
+++ b/glib/gpathbuf.c
@@ -0,0 +1,598 @@
+/* gpathbuf.c: A mutable path builder
+ *
+ * SPDX-FileCopyrightText: 2023 Emmanuele Bassi
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gpathbuf.h"
+
+#include "garray.h"
+#include "gfileutils.h"
+#include "ghash.h"
+#include "gmessages.h"
+#include "gstrfuncs.h"
+
+/**
+ * SECTION:gpathbuf
+ * @Title: GPathBuf
+ * @Short_description: A mutable path builder
+ *
+ * `GPathBuf` is a helper type that allows you to easily build paths from
+ * individual elements, using the platform specific conventions for path
+ * separators.
+ *
+ * |[<!-- language="C" -->
+ * g_auto (GPathBuf) path;
+ *
+ * g_path_buf_init (&path);
+ *
+ * g_path_buf_push (&path, "usr");
+ * g_path_buf_push (&path, "bin");
+ * g_path_buf_push (&path, "echo");
+ *
+ * g_autofree char *echo = g_path_buf_to_path (&path);
+ * g_assert_cmpstr (echo, ==, "/usr/bin/echo");
+ * ]|
+ *
+ * You can also load a full path and then operate on its components:
+ *
+ * |[<!-- language="C" -->
+ * g_auto (GPathBuf) path;
+ *
+ * g_path_buf_init_from_path (&path, "/usr/bin/echo");
+ *
+ * g_path_buf_pop (&path);
+ * g_path_buf_push (&path, "sh");
+ *
+ * g_autofree char *sh = g_path_buf_to_path (&path);
+ * g_assert_cmpstr (sh, ==, "/usr/bin/sh");
+ * ]|
+ *
+ * `GPathBuf` is available since GLib 2.76.
+ */
+
+typedef struct {
+ /* (nullable) (owned) (element-type filename) */
+ GPtrArray *path;
+
+ /* (nullable) (owned) */
+ char *extension;
+
+ gpointer padding[6];
+} RealPathBuf;
+
+G_STATIC_ASSERT (sizeof (GPathBuf) == sizeof (RealPathBuf));
+
+#define PATH_BUF(b) ((RealPathBuf *) (b))
+
+/**
+ * g_path_buf_init:
+ * @buf: a path buffer
+ *
+ * Initializes a `GPathBuf` instance.
+ *
+ * Returns: (transfer none): the initialized path builder
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_init (GPathBuf *buf)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+
+ rbuf->path = NULL;
+ rbuf->extension = NULL;
+
+ return buf;
+}
+
+/**
+ * g_path_buf_init_from_path:
+ * @buf: a path buffer
+ * @path: (type filename) (nullable): a file system path
+ *
+ * Initializes a `GPathBuf` instance with the given path.
+ *
+ * Returns: (transfer none): the initialized path builder
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_init_from_path (GPathBuf *buf,
+ const char *path)
+{
+ g_return_val_if_fail (buf != NULL, NULL);
+ g_return_val_if_fail (path == NULL || *path != '\0', NULL);
+
+ g_path_buf_init (buf);
+
+ if (path == NULL)
+ return buf;
+ else
+ return g_path_buf_push (buf, path);
+}
+
+/**
+ * g_path_buf_clear:
+ * @buf: a path buffer
+ *
+ * Clears the contents of the path buffer.
+ *
+ * This function should be use to free the resources in a stack-allocated
+ * `GPathBuf` initialized using g_path_buf_init() or
+ * g_path_buf_init_from_path().
+ *
+ * Since: 2.76
+ */
+void
+g_path_buf_clear (GPathBuf *buf)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+
+ g_return_if_fail (buf != NULL);
+
+ g_clear_pointer (&rbuf->path, g_ptr_array_unref);
+ g_clear_pointer (&rbuf->extension, g_free);
+}
+
+/**
+ * g_path_buf_clear_to_path:
+ * @buf: a path buffer
+ *
+ * Clears the contents of the path buffer and returns the built path.
+ *
+ * This function returns `NULL` if the `GPathBuf` is empty.
+ *
+ * See also: g_path_buf_to_path()
+ *
+ * Returns: (transfer full) (nullable) (type filename): the built path
+ *
+ * Since: 2.76
+ */
+char *
+g_path_buf_clear_to_path (GPathBuf *buf)
+{
+ char *res;
+
+ g_return_val_if_fail (buf != NULL, NULL);
+
+ res = g_path_buf_to_path (buf);
+ g_path_buf_clear (buf);
+
+ return g_steal_pointer (&res);
+}
+
+/**
+ * g_path_buf_new:
+ *
+ * Allocates a new `GPathBuf`.
+ *
+ * Returns: (transfer full): the newly allocated path buffer
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_new (void)
+{
+ return g_path_buf_init (g_new (GPathBuf, 1));
+}
+
+/**
+ * g_path_buf_new_from_path:
+ * @path: (type filename) (nullable): the path used to initialize the buffer
+ *
+ * Allocates a new `GPathBuf` with the given @path.
+ *
+ * Returns: (transfer full): the newly allocated path buffer
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_new_from_path (const char *path)
+{
+ return g_path_buf_init_from_path (g_new (GPathBuf, 1), path);
+}
+
+/**
+ * g_path_buf_free:
+ * @buf: (transfer full) (not nullable): a path buffer
+ *
+ * Frees a `GPathBuf` allocated by g_path_buf_new().
+ *
+ * Since: 2.76
+ */
+void
+g_path_buf_free (GPathBuf *buf)
+{
+ g_return_if_fail (buf != NULL);
+
+ g_path_buf_clear (buf);
+ g_free (buf);
+}
+
+/**
+ * g_path_buf_free_to_path:
+ * @buf: (transfer full) (not nullable): a path buffer
+ *
+ * Frees a `GPathBuf` allocated by g_path_buf_new(), and
+ * returns the path inside the buffer.
+ *
+ * This function returns `NULL` if the `GPathBuf` is empty.
+ *
+ * See also: g_path_buf_to_path()
+ *
+ * Returns: (transfer full) (nullable) (type filename): the path
+ *
+ * Since: 2.76
+ */
+char *
+g_path_buf_free_to_path (GPathBuf *buf)
+{
+ char *res;
+
+ g_return_val_if_fail (buf != NULL, NULL);
+
+ res = g_path_buf_clear_to_path (buf);
+ g_path_buf_free (buf);
+
+ return g_steal_pointer (&res);
+}
+
+/**
+ * g_path_buf_copy:
+ * @buf: (not nullable): a path buffer
+ *
+ * Copies the contents of a path buffer into a new `GPathBuf`.
+ *
+ * Returns: (transfer full): the newly allocated path buffer
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_copy (GPathBuf *buf)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+ RealPathBuf *rcopy;
+ GPathBuf *copy;
+
+ g_return_val_if_fail (buf != NULL, NULL);
+
+ copy = g_path_buf_new ();
+ rcopy = PATH_BUF (copy);
+
+ if (rbuf->path != NULL)
+ {
+ rcopy->path = g_ptr_array_new_null_terminated (rbuf->path->len, g_free, TRUE);
+ for (guint i = 0; i < rbuf->path->len; i++)
+ {
+ const char *p = g_ptr_array_index (rbuf->path, i);
+
+ if (p != NULL)
+ g_ptr_array_add (rcopy->path, g_strdup (p));
+ }
+ }
+
+ rcopy->extension = g_strdup (rbuf->extension);
+
+ return copy;
+}
+
+/**
+ * g_path_buf_push:
+ * @buf: a path buffer
+ * @path: (type filename): a path
+ *
+ * Extends the given path buffer with @path.
+ *
+ * If @path is absolute, it replaces the current path.
+ *
+ * If @path contains a directory separator, the buffer is extended by
+ * as many elements the path provides.
+ *
+ * On Windows, both forward slashes and backslashes are treated as
+ * directory separators. On other platforms, %G_DIR_SEPARATOR_S is the
+ * only directory separator.
+ *
+ * |[<!-- language="C" -->
+ * GPathBuf buf, cmp;
+ *
+ * g_path_buf_init_from_path (&buf, "/tmp");
+ * g_path_buf_push (&buf, ".X11-unix/X0");
+ * g_path_buf_init_from_path (&cmp, "/tmp/.X11-unix/X0");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp));
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_push (&buf, "/etc/locale.conf");
+ * g_path_buf_init_from_path (&cmp, "/etc/locale.conf");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp));
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_clear (&buf);
+ * ]|
+ *
+ * Returns: (transfer none): the same pointer to @buf, for convenience
+ *
+ * Since: 2.76
+ */
+GPathBuf *
+g_path_buf_push (GPathBuf *buf,
+ const char *path)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+
+ g_return_val_if_fail (buf != NULL, NULL);
+ g_return_val_if_fail (path != NULL && *path != '\0', buf);
+
+ if (g_path_is_absolute (path))
+ {
+#ifdef G_OS_WIN32
+ char **elements = g_strsplit_set (path, "\\/", -1);
+#else
+ char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
+#endif
+
+#ifdef G_OS_UNIX
+ /* strsplit() will add an empty element for the leading root,
+ * which will cause the path build to ignore it; to avoid it,
+ * we re-inject the root as the first element.
+ *
+ * The first string is empty, but it's still allocated, so we
+ * need to free it to avoid leaking it.
+ */
+ g_free (elements[0]);
+ elements[0] = g_strdup ("/");
+#endif
+
+ g_clear_pointer (&rbuf->path, g_ptr_array_unref);
+ rbuf->path = g_ptr_array_new_null_terminated (g_strv_length (elements), g_free, TRUE);
+
+ /* Skip empty elements caused by repeated separators */
+ for (guint i = 0; elements[i] != NULL; i++)
+ {
+ if (*elements[i] != '\0')
+ g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
+ else
+ g_free (elements[i]);
+ }
+
+ g_free (elements);
+ }
+ else
+ {
+ char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
+
+ if (rbuf->path == NULL)
+ rbuf->path = g_ptr_array_new_null_terminated (g_strv_length (elements), g_free, TRUE);
+
+ /* Skip empty elements caused by repeated separators */
+ for (guint i = 0; elements[i] != NULL; i++)
+ {
+ if (*elements[i] != '\0')
+ g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
+ else
+ g_free (elements[i]);
+ }
+
+ g_free (elements);
+ }
+
+ return buf;
+}
+
+/**
+ * g_path_buf_pop:
+ * @buf: a path buffer
+ *
+ * Removes the last element of the path buffer.
+ *
+ * If there is only one element in the path buffer (for example, `/` on
+ * Unix-like operating systems or the drive on Windows systems), it will
+ * not be removed and %FALSE will be returned instead.
+ *
+ * |[<!-- language="C" -->
+ * GPathBuf buf, cmp;
+ *
+ * g_path_buf_init_from_path (&buf, "/bin/sh");
+ *
+ * g_path_buf_pop (&buf);
+ * g_path_buf_init_from_path (&cmp, "/bin");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp));
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_pop (&buf);
+ * g_path_buf_init_from_path (&cmp, "/");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp));
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_clear (&buf);
+ * ]|
+ *
+ * Returns: `TRUE` if the buffer was modified and `FALSE` otherwise
+ *
+ * Since: 2.76
+ */
+gboolean
+g_path_buf_pop (GPathBuf *buf)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+
+ g_return_val_if_fail (buf != NULL, FALSE);
+ g_return_val_if_fail (rbuf->path != NULL, FALSE);
+
+ /* Keep the first element of the buffer; it's either '/' or the drive */
+ if (rbuf->path->len > 1)
+ {
+ g_ptr_array_remove_index (rbuf->path, rbuf->path->len - 1);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * g_path_buf_set_filename:
+ * @buf: a path buffer
+ * @file_name: (type filename) (not nullable): the file name in the path
+ *
+ * Sets the file name of the path.
+ *
+ * If the path buffer is empty, the filename is left unset and this
+ * function returns `FALSE`.
+ *
+ * If the path buffer only contains the root element (on Unix-like operating
+ * systems) or the drive (on Windows), this is the equivalent of pushing
+ * the new @file_name.
+ *
+ * If the path buffer contains a path, this is the equivalent of
+ * popping the path buffer and pushing @file_name, creating a
+ * sibling of the original path.
+ *
+ * |[<!-- language="C" -->
+ * GPathBuf buf, cmp;
+ *
+ * g_path_buf_init_from_path (&buf, "/");
+ *
+ * g_path_buf_set_filename (&buf, "bar");
+ * g_path_buf_init_from_path (&cmp, "/bar");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp));
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_set_filename (&buf, "baz.txt");
+ * g_path_buf_init_from_path (&cmp, "/baz.txt");
+ * g_assert_true (g_path_buf_equal (&buf, &cmp);
+ * g_path_buf_clear (&cmp);
+ *
+ * g_path_buf_clear (&buf);
+ * ]|
+ *
+ * Returns: `TRUE` if the file name was replaced, and `FALSE` otherwise
+ *
+ * Since: 2.76
+ */
+gboolean
+g_path_buf_set_filename (GPathBuf *buf,
+ const char *file_name)
+{
+ g_return_val_if_fail (buf != NULL, FALSE);
+ g_return_val_if_fail (file_name != NULL, FALSE);
+
+ if (PATH_BUF (buf)->path == NULL)
+ return FALSE;
+
+ g_path_buf_pop (buf);
+ g_path_buf_push (buf, file_name);
+
+ return TRUE;
+}
+
+/**
+ * g_path_buf_set_extension:
+ * @buf: a path buffer
+ * @extension: (type filename) (nullable): the file extension
+ *
+ * Adds an extension to the file name in the path buffer.
+ *
+ * If @extension is `NULL`, the extension will be unset.
+ *
+ * If the path buffer does not have a file name set, this function returns
+ * `FALSE` and leaves the path buffer unmodified.
+ *
+ * Returns: `TRUE` if the extension was replaced, and `FALSE` otherwise
+ *
+ * Since: 2.76
+ */
+gboolean
+g_path_buf_set_extension (GPathBuf *buf,
+ const char *extension)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+
+ g_return_val_if_fail (buf != NULL, FALSE);
+
+ if (rbuf->path != NULL)
+ return g_set_str (&rbuf->extension, extension);
+ else
+ return FALSE;
+}
+
+/**
+ * g_path_buf_to_path:
+ * @buf: a path buffer
+ *
+ * Retrieves the built path from the path buffer.
+ *
+ * On Windows, the result contains backslashes as directory separators,
+ * even if forward slashes were used in input.
+ *
+ * If the path buffer is empty, this function returns `NULL`.
+ *
+ * Returns: (transfer full) (type filename) (nullable): the path
+ *
+ * Since: 2.76
+ */
+char *
+g_path_buf_to_path (GPathBuf *buf)
+{
+ RealPathBuf *rbuf = PATH_BUF (buf);
+ char *path = NULL;
+
+ g_return_val_if_fail (buf != NULL, NULL);
+
+ if (rbuf->path != NULL)
+ path = g_build_filenamev ((char **) rbuf->path->pdata);
+
+ if (path != NULL && rbuf->extension != NULL)
+ {
+ char *tmp = g_strconcat (path, ".", rbuf->extension, NULL);
+
+ g_free (path);
+ path = g_steal_pointer (&tmp);
+ }
+
+ return path;
+}
+
+/**
+ * g_path_buf_equal:
+ * @v1: (not nullable): a path buffer to compare
+ * @v2: (not nullable): a path buffer to compare
+ *
+ * Compares two path buffers for equality and returns `TRUE`
+ * if they are equal.
+ *
+ * The path inside the paths buffers are not going to be normalized,
+ * so `X/Y/Z/A/..`, `X/./Y/Z` and `X/Y/Z` are not going to be considered
+ * equal.
+ *
+ * This function can be passed to g_hash_table_new() as the
+ * `key_equal_func` parameter.
+ *
+ * Returns: `TRUE` if the two path buffers are equal,
+ * and `FALSE` otherwise
+ *
+ * Since: 2.76
+ */
+gboolean
+g_path_buf_equal (gconstpointer v1,
+ gconstpointer v2)
+{
+ if (v1 == v2)
+ return TRUE;
+
+ /* We resolve the buffer into a path to normalize its contents;
+ * this won't resolve symbolic links or `.` and `..` components
+ */
+ char *p1 = g_path_buf_to_path ((GPathBuf *) v1);
+ char *p2 = g_path_buf_to_path ((GPathBuf *) v2);
+
+ gboolean res = p1 != NULL && p2 != NULL
+ ? g_str_equal (p1, p2)
+ : FALSE;
+
+ g_free (p1);
+ g_free (p2);
+
+ return res;
+}