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 (); +}