diff --git a/docs/reference/glib/glib-docs.xml b/docs/reference/glib/glib-docs.xml
index c88052c08..a21bfde98 100644
--- a/docs/reference/glib/glib-docs.xml
+++ b/docs/reference/glib/glib-docs.xml
@@ -81,6 +81,7 @@
+
diff --git a/docs/reference/glib/glib-sections.txt.in b/docs/reference/glib/glib-sections.txt.in
index 04f5aa4eb..f617bbd4b 100644
--- a/docs/reference/glib/glib-sections.txt.in
+++ b/docs/reference/glib/glib-sections.txt.in
@@ -3733,3 +3733,24 @@ g_ref_string_acquire
g_ref_string_release
g_ref_string_length
+
+
+gpathbuf
+GPathBuf
+G_PATH_BUF_INIT
+g_path_buf_new
+g_path_buf_new_from_path
+g_path_buf_init
+g_path_buf_init_from_path
+g_path_buf_clear
+g_path_buf_clear_to_path
+g_path_buf_free
+g_path_buf_free_to_path
+g_path_buf_push
+g_path_buf_pop
+g_path_buf_set_filename
+g_path_buf_set_extension
+g_path_buf_to_path
+g_path_buf_copy
+g_path_buf_equal
+
diff --git a/glib/gfileutils.c b/glib/gfileutils.c
index 131484901..110c26f43 100644
--- a/glib/gfileutils.c
+++ b/glib/gfileutils.c
@@ -313,10 +313,13 @@ g_mkdir_with_parents (const gchar *pathname,
*
* You should never use g_file_test() to test whether it is safe
* to perform an operation, because there is always the possibility
- * of the condition changing before you actually perform the operation.
+ * of the condition changing before you actually perform the operation,
+ * see [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use).
+ *
* For example, you might think you could use %G_FILE_TEST_IS_SYMLINK
* to know whether it is safe to write to a file without being
* tricked into writing into a different location. It doesn't work!
+ *
* |[
* // DON'T DO THIS
* if (!g_file_test (filename, G_FILE_TEST_IS_SYMLINK))
@@ -324,6 +327,21 @@ g_mkdir_with_parents (const gchar *pathname,
* fd = g_open (filename, O_WRONLY);
* // write to fd
* }
+ *
+ * // DO THIS INSTEAD
+ * fd = g_open (filename, O_WRONLY);
+ * if (fd == -1)
+ * {
+ * // check error
+ * if (errno == ELOOP)
+ * // file is a symlink and can be ignored
+ * else
+ * // handle errors as before
+ * }
+ * else
+ * {
+ * // write to fd
+ * }
* ]|
*
* Another thing to note is that %G_FILE_TEST_EXISTS and
@@ -1579,8 +1597,8 @@ wrap_g_open (const gchar *filename,
* g_dir_make_tmp() instead.
*
* Returns: (nullable) (type filename): A pointer to @tmpl, which has been
- * modified to hold the directory name. In case of errors, %NULL is
- * returned, and %errno will be set.
+ * modified to hold the directory name. In case of errors, %NULL is
+ * returned, and %errno will be set.
*
* Since: 2.30
*/
@@ -1615,8 +1633,8 @@ g_mkdtemp_full (gchar *tmpl,
* g_dir_make_tmp() instead.
*
* Returns: (nullable) (type filename): A pointer to @tmpl, which has been
- * modified to hold the directory name. In case of errors, %NULL is
- * returned and %errno will be set.
+ * modified to hold the directory name. In case of errors, %NULL is
+ * returned and %errno will be set.
*
* Since: 2.30
*/
@@ -1630,7 +1648,7 @@ g_mkdtemp (gchar *tmpl)
* g_mkstemp_full: (skip)
* @tmpl: (type filename): template filename
* @flags: flags to pass to an open() call in addition to O_EXCL
- * and O_CREAT, which are passed automatically
+ * and O_CREAT, which are passed automatically
* @mode: permissions to create the temporary file with
*
* Opens a temporary file. See the mkstemp() documentation
@@ -1646,9 +1664,9 @@ g_mkdtemp (gchar *tmpl)
* on Windows it should be in UTF-8.
*
* Returns: A file handle (as from open()) to the file
- * opened for reading and writing. The file handle should be
- * closed with close(). In case of errors, -1 is returned
- * and %errno will be set.
+ * opened for reading and writing. The file handle should be
+ * closed with close(). In case of errors, -1 is returned
+ * and %errno will be set.
*
* Since: 2.22
*/
@@ -1678,10 +1696,10 @@ g_mkstemp_full (gchar *tmpl,
* Most importantly, on Windows it should be in UTF-8.
*
* Returns: A file handle (as from open()) to the file
- * opened for reading and writing. The file is opened in binary
- * mode on platforms where there is a difference. The file handle
- * should be closed with close(). In case of errors, -1 is
- * returned and %errno will be set.
+ * opened for reading and writing. The file is opened in binary
+ * mode on platforms where there is a difference. The file handle
+ * should be closed with close(). In case of errors, -1 is
+ * returned and %errno will be set.
*/
gint
g_mkstemp (gchar *tmpl)
@@ -1769,9 +1787,9 @@ g_get_tmp_name (const gchar *tmpl,
/**
* g_file_open_tmp:
* @tmpl: (type filename) (nullable): Template for file name, as in
- * g_mkstemp(), basename only, or %NULL for a default template
+ * g_mkstemp(), basename only, or %NULL for a default template
* @name_used: (out) (type filename): location to store actual name used,
- * or %NULL
+ * or %NULL
* @error: return location for a #GError
*
* Opens a file for writing in the preferred directory for temporary
@@ -1792,9 +1810,9 @@ g_get_tmp_name (const gchar *tmpl,
* name encoding.
*
* Returns: A file handle (as from open()) to the file opened for
- * reading and writing. The file is opened in binary mode on platforms
- * where there is a difference. The file handle should be closed with
- * close(). In case of errors, -1 is returned and @error will be set.
+ * reading and writing. The file is opened in binary mode on platforms
+ * where there is a difference. The file handle should be closed with
+ * close(). In case of errors, -1 is returned and @error will be set.
*/
gint
g_file_open_tmp (const gchar *tmpl,
@@ -1825,7 +1843,7 @@ g_file_open_tmp (const gchar *tmpl,
/**
* g_dir_make_tmp:
* @tmpl: (type filename) (nullable): Template for directory name,
- * as in g_mkdtemp(), basename only, or %NULL for a default template
+ * as in g_mkdtemp(), basename only, or %NULL for a default template
* @error: return location for a #GError
*
* Creates a subdirectory in the preferred directory for temporary
@@ -1841,9 +1859,9 @@ g_file_open_tmp (const gchar *tmpl,
* modified, and might thus be a read-only literal string.
*
* Returns: (type filename) (transfer full): The actual name used. This string
- * should be freed with g_free() when not needed any longer and is
- * is in the GLib file name encoding. In case of errors, %NULL is
- * returned and @error will be set.
+ * should be freed with g_free() when not needed any longer and is
+ * is in the GLib file name encoding. In case of errors, %NULL is
+ * returned and @error will be set.
*
* Since: 2.30
*/
@@ -1968,11 +1986,12 @@ g_build_path_va (const gchar *separator,
* g_build_pathv:
* @separator: a string used to separator the elements of the path.
* @args: (array zero-terminated=1) (element-type filename): %NULL-terminated
- * array of strings containing the path elements.
+ * array of strings containing the path elements.
*
- * Behaves exactly like g_build_path(), but takes the path elements
- * as a string array, instead of varargs. This function is mainly
- * meant for language bindings.
+ * Behaves exactly like g_build_path(), but takes the path elements
+ * as a string array, instead of variadic arguments.
+ *
+ * This function is mainly meant for language bindings.
*
* Returns: (type filename) (transfer full): a newly-allocated string that
* must be freed with g_free().
@@ -1997,10 +2016,12 @@ g_build_pathv (const gchar *separator,
* @...: remaining elements in path, terminated by %NULL
*
* Creates a path from a series of elements using @separator as the
- * separator between elements. At the boundary between two elements,
- * any trailing occurrences of separator in the first element, or
- * leading occurrences of separator in the second element are removed
- * and exactly one copy of the separator is inserted.
+ * separator between elements.
+ *
+ * At the boundary between two elements, any trailing occurrences of
+ * separator in the first element, or leading occurrences of separator
+ * in the second element are removed and exactly one copy of the
+ * separator is inserted.
*
* Empty elements are ignored.
*
@@ -2023,8 +2044,7 @@ g_build_pathv (const gchar *separator,
* copies of the separator, elements consisting only of copies
* of the separator are ignored.
*
- * Returns: (type filename) (transfer full): a newly-allocated string that
- * must be freed with g_free().
+ * Returns: (type filename) (transfer full): the newly allocated path
**/
gchar *
g_build_path (const gchar *separator,
@@ -2180,11 +2200,16 @@ g_build_filename_va (const gchar *first_argument,
* @first_element: (type filename): the first element in the path
* @args: va_list of remaining elements in path
*
- * Behaves exactly like g_build_filename(), but takes the path elements
- * as a va_list. This function is mainly meant for language bindings.
+ * Creates a filename from a list of elements using the correct
+ * separator for the current platform.
*
- * Returns: (type filename) (transfer full): a newly-allocated string that
- * must be freed with g_free().
+ * Behaves exactly like g_build_filename(), but takes the path elements
+ * as a va_list.
+ *
+ * This function is mainly meant for implementing other variadic arguments
+ * functions.
+ *
+ * Returns: (type filename) (transfer full): the newly allocated path
*
* Since: 2.56
*/
@@ -2200,14 +2225,19 @@ g_build_filename_valist (const gchar *first_element,
/**
* g_build_filenamev:
* @args: (array zero-terminated=1) (element-type filename): %NULL-terminated
- * array of strings containing the path elements.
+ * array of strings containing the path elements.
*
- * Behaves exactly like g_build_filename(), but takes the path elements
- * as a string array, instead of varargs. This function is mainly
+ * Creates a filename from a vector of elements using the correct
+ * separator for the current platform.
+ *
+ * This function behaves exactly like g_build_filename(), but takes the path
+ * elements as a string array, instead of varargs. This function is mainly
* meant for language bindings.
*
- * Returns: (type filename) (transfer full): a newly-allocated string that
- * must be freed with g_free().
+ * If you are building a path programmatically you may want to use
+ * #GPathBuf instead.
+ *
+ * Returns: (type filename) (transfer full): the newly allocated path
*
* Since: 2.8
*/
@@ -2223,7 +2253,7 @@ g_build_filenamev (gchar **args)
* @...: remaining elements in path, terminated by %NULL
*
* Creates a filename from a series of elements using the correct
- * separator for filenames.
+ * separator for the current platform.
*
* On Unix, this function behaves identically to `g_build_path
* (G_DIR_SEPARATOR_S, first_element, ....)`.
@@ -2238,9 +2268,11 @@ g_build_filenamev (gchar **args)
* path. If the first element is a relative path, the result will
* be a relative path.
*
- * Returns: (type filename) (transfer full): a newly-allocated string that
- * must be freed with g_free().
- **/
+ * If you are building a path programmatically you may want to use
+ * #GPathBuf instead.
+ *
+ * Returns: (type filename) (transfer full): the newly allocated path
+ */
gchar *
g_build_filename (const gchar *first_element,
...)
@@ -2261,14 +2293,15 @@ g_build_filename (const gchar *first_element,
* @error: return location for a #GError
*
* Reads the contents of the symbolic link @filename like the POSIX
- * readlink() function.
+ * `readlink()` function.
*
- * The returned string is in the encoding used
- * for filenames. Use g_filename_to_utf8() to convert it to UTF-8.
+ * The returned string is in the encoding used for filenames. Use
+ * g_filename_to_utf8() to convert it to UTF-8.
*
- * The returned string may also be a relative path. Use g_build_filename() to
- * convert it to an absolute path:
- * |[
+ * The returned string may also be a relative path. Use g_build_filename()
+ * to convert it to an absolute path:
+ *
+ * |[
* g_autoptr(GError) local_error = NULL;
* g_autofree gchar *link_target = g_file_read_link ("/etc/localtime", &local_error);
*
@@ -2284,7 +2317,7 @@ g_build_filename (const gchar *first_element,
* ]|
*
* Returns: (type filename) (transfer full): A newly-allocated string with
- * the contents of the symbolic link, or %NULL if an error occurred.
+ * the contents of the symbolic link, or %NULL if an error occurred.
*
* Since: 2.4
*/
@@ -2491,12 +2524,12 @@ g_path_skip_root (const gchar *file_name)
* string.
*
* Returns: (type filename): the name of the file without any leading
- * directory components
+ * directory components
*
* Deprecated:2.2: Use g_path_get_basename() instead, but notice
- * that g_path_get_basename() allocates new memory for the
- * returned string, unlike this function which returns a pointer
- * into the argument.
+ * that g_path_get_basename() allocates new memory for the
+ * returned string, unlike this function which returns a pointer
+ * into the argument.
*/
const gchar *
g_basename (const gchar *file_name)
@@ -2539,7 +2572,7 @@ g_basename (const gchar *file_name)
* separator is returned. If @file_name is empty, it gets ".".
*
* Returns: (type filename) (transfer full): a newly allocated string
- * containing the last component of the filename
+ * containing the last component of the filename
*/
gchar *
g_path_get_basename (const gchar *file_name)
@@ -2733,7 +2766,8 @@ g_path_get_dirname (const gchar *file_name)
* No file system I/O is done.
*
* Returns: (type filename) (transfer full): a newly allocated string with the
- * canonical file path
+ * canonical file path
+ *
* Since: 2.58
*/
gchar *
diff --git a/glib/glib-autocleanups.h b/glib/glib-autocleanups.h
index e2e0075e5..6adf23282 100644
--- a/glib/glib-autocleanups.h
+++ b/glib/glib-autocleanups.h
@@ -101,5 +101,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariantType, g_variant_type_free)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(GStrv, g_strfreev, NULL)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRefString, g_ref_string_release)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUri, g_uri_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GPathBuf, g_path_buf_free)
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (GPathBuf, g_path_buf_clear)
G_GNUC_END_IGNORE_DEPRECATIONS
diff --git a/glib/glib.h b/glib/glib.h
index cfd28ce0d..40e501997 100644
--- a/glib/glib.h
+++ b/glib/glib.h
@@ -64,6 +64,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/glib/gpathbuf.c b/glib/gpathbuf.c
new file mode 100644
index 000000000..ac359c33d
--- /dev/null
+++ b/glib/gpathbuf.c
@@ -0,0 +1,587 @@
+/* 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 `G_DIR_SEPARATOR_S`, the buffer is extended by
+ * as many elements the path provides.
+ *
+ * |[
+ * 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))
+ {
+ char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
+
+#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.
+ *
+ * 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;
+}
diff --git a/glib/gpathbuf.h b/glib/gpathbuf.h
new file mode 100644
index 000000000..b42341998
--- /dev/null
+++ b/glib/gpathbuf.h
@@ -0,0 +1,90 @@
+/* gpathbuf.h: A mutable path builder
+ *
+ * SPDX-FileCopyrightText: 2023 Emmanuele Bassi
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined (__GLIB_H_INSIDE__) && !defined (GLIB_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+
+G_BEGIN_DECLS
+
+typedef struct _GPathBuf GPathBuf;
+
+/**
+ * GPathBuf: (copy-func g_path_buf_copy) (free-func g_path_buf_free)
+ *
+ * A mutable path builder.
+ *
+ * Since: 2.76
+ */
+struct _GPathBuf
+{
+ /*< private >*/
+ gpointer dummy[8];
+};
+
+/**
+ * G_PATH_BUF_INIT:
+ *
+ * Initializes a #GPathBuf on the stack.
+ *
+ * A stack-allocated `GPathBuf` must be initialized if it is used
+ * together with g_auto() to avoid warnings and crashes if the
+ * function returns before calling g_path_buf_init().
+ *
+ * |[
+ * g_auto (GPathBuf) buf = G_PATH_BUF_INIT;
+ * ]|
+ *
+ * Since: 2.76
+ */
+#define G_PATH_BUF_INIT { { NULL, } } \
+ GLIB_AVAILABLE_MACRO_IN_2_76
+
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_new (void);
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_new_from_path (const char *path);
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_init (GPathBuf *buf);
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_init_from_path (GPathBuf *buf,
+ const char *path);
+GLIB_AVAILABLE_IN_2_76
+void g_path_buf_clear (GPathBuf *buf);
+GLIB_AVAILABLE_IN_2_76
+char * g_path_buf_clear_to_path (GPathBuf *buf) G_GNUC_WARN_UNUSED_RESULT;
+GLIB_AVAILABLE_IN_2_76
+void g_path_buf_free (GPathBuf *buf);
+GLIB_AVAILABLE_IN_2_76
+char * g_path_buf_free_to_path (GPathBuf *buf) G_GNUC_WARN_UNUSED_RESULT;
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_copy (GPathBuf *buf);
+
+GLIB_AVAILABLE_IN_2_76
+GPathBuf * g_path_buf_push (GPathBuf *buf,
+ const char *path);
+GLIB_AVAILABLE_IN_2_76
+gboolean g_path_buf_pop (GPathBuf *buf);
+
+GLIB_AVAILABLE_IN_2_76
+gboolean g_path_buf_set_filename (GPathBuf *buf,
+ const char *file_name);
+GLIB_AVAILABLE_IN_2_76
+gboolean g_path_buf_set_extension (GPathBuf *buf,
+ const char *extension);
+
+GLIB_AVAILABLE_IN_2_76
+char * g_path_buf_to_path (GPathBuf *buf) G_GNUC_WARN_UNUSED_RESULT;
+
+GLIB_AVAILABLE_IN_2_76
+gboolean g_path_buf_equal (gconstpointer v1,
+ gconstpointer v2);
+
+G_END_DECLS
diff --git a/glib/meson.build b/glib/meson.build
index b623983e3..75b3b4018 100644
--- a/glib/meson.build
+++ b/glib/meson.build
@@ -205,6 +205,7 @@ glib_sub_headers = files(
'gmessages.h',
'gnode.h',
'goption.h',
+ 'gpathbuf.h',
'gpattern.h',
'gpoll.h',
'gprimes.h',
@@ -293,6 +294,7 @@ glib_sources += files(
'gmessages.c',
'gnode.c',
'goption.c',
+ 'gpathbuf.c',
'gpattern.c',
'gpoll.c',
'gprimes.c',
diff --git a/glib/tests/autoptr.c b/glib/tests/autoptr.c
index 06f1a0bff..e10c95c9d 100644
--- a/glib/tests/autoptr.c
+++ b/glib/tests/autoptr.c
@@ -618,6 +618,30 @@ test_refstring (void)
g_assert_nonnull (str);
}
+static void
+test_pathbuf (void)
+{
+#if defined(G_OS_UNIX)
+ g_autoptr(GPathBuf) buf1 = g_path_buf_new_from_path ("/bin/sh");
+ g_auto(GPathBuf) buf2 = G_PATH_BUF_INIT;
+
+ g_path_buf_push (&buf2, "/bin/sh");
+#elif defined(G_OS_WIN32)
+ g_autoptr(GPathBuf) buf1 = g_path_buf_new_from_path ("C:\\windows\\system32.dll");
+ g_auto(GPathBuf) buf2 = G_PATH_BUF_INIT;
+
+ g_path_buf_push (&buf2, "C:\\windows\\system32.dll");
+#else
+ g_test_skip ("Unsupported platform");
+ return;
+#endif
+
+ g_autofree char *path1 = g_path_buf_to_path (buf1);
+ g_autofree char *path2 = g_path_buf_to_path (&buf2);
+
+ g_assert_cmpstr (path1, ==, path2);
+}
+
static void
mark_freed (gpointer ptr)
{
@@ -772,6 +796,7 @@ main (int argc, gchar *argv[])
g_test_add_func ("/autoptr/g_variant_type", test_g_variant_type);
g_test_add_func ("/autoptr/strv", test_strv);
g_test_add_func ("/autoptr/refstring", test_refstring);
+ g_test_add_func ("/autoptr/pathbuf", test_pathbuf);
g_test_add_func ("/autoptr/autolist", test_autolist);
g_test_add_func ("/autoptr/autoslist", test_autoslist);
g_test_add_func ("/autoptr/autoqueue", test_autoqueue);
diff --git a/glib/tests/meson.build b/glib/tests/meson.build
index 03e81f3dd..a1626aa52 100644
--- a/glib/tests/meson.build
+++ b/glib/tests/meson.build
@@ -77,6 +77,7 @@ glib_tests = {
'source' : 'overflow.c',
'c_args' : ['-D_GLIB_TEST_OVERFLOW_FALLBACK'],
},
+ 'pathbuf' : {},
'pattern' : {},
'private' : {},
'protocol' : {},
diff --git a/glib/tests/pathbuf.c b/glib/tests/pathbuf.c
new file mode 100644
index 000000000..e9104f15d
--- /dev/null
+++ b/glib/tests/pathbuf.c
@@ -0,0 +1,255 @@
+/* Unit tests for GPathBuf
+ *
+ * SPDX-FileCopyrightText: 2023 Emmanuele Bassi
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+#include
+#include
+
+#include
+
+#ifndef g_assert_path_buf_equal
+#define g_assert_path_buf_equal(p1,p2) \
+ G_STMT_START { \
+ if (g_path_buf_equal ((p1), (p2))) ; else { \
+ char *__p1 = g_path_buf_to_path ((p1)); \
+ char *__p2 = g_path_buf_to_path ((p2)); \
+ g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+ #p1 " == " #p2, __p1, "==", __p2); \
+ g_free (__p1); \
+ g_free (__p2); \
+ } \
+ } G_STMT_END
+#endif
+
+static void
+test_pathbuf_init (void)
+{
+#ifdef G_OS_UNIX
+ GPathBuf buf, cmp;
+ char *path;
+
+ g_test_message ("Initializing empty path buf");
+ g_path_buf_init (&buf);
+ g_assert_null (g_path_buf_to_path (&buf));
+ g_path_buf_clear (&buf);
+
+ g_test_message ("Initializing with empty path");
+ g_path_buf_init_from_path (&buf, NULL);
+ g_assert_null (g_path_buf_to_path (&buf));
+ g_path_buf_clear (&buf);
+
+ g_test_message ("Initializing with full path");
+ g_path_buf_init_from_path (&buf, "/usr/bin/echo");
+ path = g_path_buf_clear_to_path (&buf);
+ g_assert_nonnull (path);
+ g_assert_cmpstr (path, ==, "/usr/bin/echo");
+ g_free (path);
+
+ g_test_message ("Initializing with no path");
+ g_path_buf_init (&buf);
+ g_assert_null (g_path_buf_to_path (&buf));
+ g_path_buf_clear (&buf);
+
+ g_test_message ("Allocating GPathBuf on the heap");
+ GPathBuf *allocated = g_path_buf_new ();
+ g_assert_null (g_path_buf_to_path (allocated));
+ g_path_buf_clear (allocated);
+
+ g_path_buf_init_from_path (allocated, "/bin/sh");
+ path = g_path_buf_to_path (allocated);
+ g_assert_cmpstr (path, ==, "/bin/sh");
+ g_free (path);
+
+ g_path_buf_clear (allocated);
+ g_assert_null (g_path_buf_to_path (allocated));
+ g_assert_null (g_path_buf_free_to_path (allocated));
+
+ allocated = g_path_buf_new_from_path ("/bin/sh");
+ g_path_buf_init_from_path (&cmp, "/bin/sh");
+ g_assert_path_buf_equal (allocated, &cmp);
+ g_path_buf_clear (&cmp);
+ g_path_buf_free (allocated);
+
+ g_path_buf_init_from_path (&buf, "/usr/bin/bash");
+ allocated = g_path_buf_copy (&buf);
+ g_assert_path_buf_equal (allocated, allocated);
+ g_assert_path_buf_equal (allocated, &buf);
+ g_path_buf_clear (&buf);
+
+ g_path_buf_init_from_path (&cmp, "/usr/bin/bash");
+ g_assert_path_buf_equal (allocated, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_path_buf_free (allocated);
+#elif defined(G_OS_WIN32)
+ GPathBuf buf;
+ char *path;
+
+ g_path_buf_init_from_path (&buf, "C:\\windows\\system32.dll");
+ path = g_path_buf_clear_to_path (&buf);
+ g_assert_nonnull (path);
+ g_assert_cmpstr (path, ==, "C:\\windows\\system32.dll");
+ g_free (path);
+
+ g_path_buf_init (&buf);
+ g_assert_null (g_path_buf_to_path (&buf));
+ g_path_buf_clear (&buf);
+
+ g_test_message ("Allocating GPathBuf on the heap");
+ GPathBuf *allocated = g_path_buf_new ();
+ g_assert_null (g_path_buf_to_path (allocated));
+ g_path_buf_clear (allocated);
+
+ g_path_buf_init_from_path (allocated, "C:\\does-not-exist.txt");
+ path = g_path_buf_to_path (allocated);
+ g_assert_cmpstr (path, ==, "C:\\does-not-exist.txt");
+ g_free (path);
+
+ g_path_buf_clear (allocated);
+ g_assert_null (g_path_buf_to_path (allocated));
+ g_assert_null (g_path_buf_free_to_path (allocated));
+#else
+ g_test_skip ("Unsupported platform"):
+#endif
+}
+
+static void
+test_pathbuf_push_pop (void)
+{
+#ifdef G_OS_UNIX
+ GPathBuf buf, cmp;
+
+ g_test_message ("Pushing relative path component");
+ 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_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_test_message ("Pushing absolute path component");
+ g_path_buf_push (&buf, "/etc/locale.conf");
+ g_path_buf_init_from_path (&cmp, "/etc/locale.conf");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+ g_path_buf_clear (&buf);
+
+ g_test_message ("Popping a path component");
+ g_path_buf_init_from_path (&buf, "/bin/sh");
+
+ g_assert_true (g_path_buf_pop (&buf));
+ g_path_buf_init_from_path (&cmp, "/bin");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_assert_true (g_path_buf_pop (&buf));
+ g_path_buf_init_from_path (&cmp, "/");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_test_message ("Can't pop the last element of a path buffer");
+ g_assert_false (g_path_buf_pop (&buf));
+
+ g_path_buf_clear (&buf);
+ g_path_buf_clear (&cmp);
+#elif defined(G_OS_WIN32)
+ GPathBuf buf, cmp;
+
+ g_test_message ("Pushing relative path component");
+ g_path_buf_init_from_path (&buf, "C:\\");
+ g_path_buf_push (&buf, "windows");
+ g_path_buf_push (&buf, "system32.dll");
+
+ g_test_message ("Popping a path component");
+ g_path_buf_init_from_path (&cmp, "C:\\windows\\system32.dll");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_assert_true (g_path_buf_pop (&buf));
+ g_path_buf_init_from_path (&cmp, "C:\\windows");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_assert_true (g_path_buf_pop (&buf));
+ g_path_buf_init_from_path (&cmp, "C:");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_test_message ("Can't pop the last element of a path buffer");
+ g_assert_false (g_path_buf_pop (&buf));
+
+ g_path_buf_clear (&buf);
+ g_path_buf_clear (&cmp);
+#else
+ g_test_skip ("Unsupported platform"):
+#endif
+}
+
+static void
+test_pathbuf_filename_extension (void)
+{
+#ifdef G_OS_UNIX
+ GPathBuf buf, cmp;
+
+ g_path_buf_init (&buf);
+ g_assert_false (g_path_buf_set_filename (&buf, "foo"));
+ g_assert_false (g_path_buf_set_extension (&buf, "txt"));
+ g_assert_null (g_path_buf_to_path (&buf));
+ g_path_buf_clear (&buf);
+
+ g_path_buf_init_from_path (&buf, "/");
+ g_path_buf_set_filename (&buf, "bar");
+
+ g_path_buf_init_from_path (&cmp, "/bar");
+ g_assert_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_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_path_buf_push (&buf, "/usr");
+ g_path_buf_push (&buf, "lib64");
+ g_path_buf_push (&buf, "libc");
+ g_assert_true (g_path_buf_set_extension (&buf, "so.6"));
+
+ g_path_buf_init_from_path (&cmp, "/usr/lib64/libc.so.6");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_path_buf_clear (&buf);
+#elif defined(G_OS_WIN32)
+ GPathBuf buf, cmp;
+
+ g_path_buf_init_from_path (&buf, "C:\\");
+ g_path_buf_push (&buf, "windows");
+ g_path_buf_push (&buf, "system32");
+ g_assert_true (g_path_buf_set_extension (&buf, "dll"));
+
+ g_path_buf_init_from_path (&cmp, "C:\\windows\\system32.dll");
+ g_assert_path_buf_equal (&buf, &cmp);
+ g_path_buf_clear (&cmp);
+
+ g_path_buf_clear (&buf);
+#else
+ g_test_skip ("Unsupported platform"):
+#endif
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_setenv ("LC_ALL", "C", TRUE);
+ g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
+
+ g_test_add_func ("/pathbuf/init", test_pathbuf_init);
+ g_test_add_func ("/pathbuf/push-pop", test_pathbuf_push_pop);
+ g_test_add_func ("/pathbuf/filename-extension", test_pathbuf_filename_extension);
+
+ return g_test_run ();
+}