diff options
Diffstat (limited to 'glib/gpathbuf.c')
-rw-r--r-- | glib/gpathbuf.c | 598 |
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; +} |