/* 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. * * |[ * 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: * * |[ * 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. * * |[ * 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. * * |[ * 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. * * |[ * 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; }