1 /* gpathbuf.c: A mutable path builder
2 *
3 * SPDX-FileCopyrightText: 2023 Emmanuele Bassi
4 * SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6
7 #include "config.h"
8
9 #include "gpathbuf.h"
10
11 #include "garray.h"
12 #include "gfileutils.h"
13 #include "ghash.h"
14 #include "gmessages.h"
15 #include "gstrfuncs.h"
16
17 /**
18 * GPathBuf:
19 *
20 * `GPathBuf` is a helper type that allows you to easily build paths from
21 * individual elements, using the platform specific conventions for path
22 * separators.
23 *
24 * ```c
25 * g_auto (GPathBuf) path;
26 *
27 * g_path_buf_init (&path);
28 *
29 * g_path_buf_push (&path, "usr");
30 * g_path_buf_push (&path, "bin");
31 * g_path_buf_push (&path, "echo");
32 *
33 * g_autofree char *echo = g_path_buf_to_path (&path);
34 * g_assert_cmpstr (echo, ==, "/usr/bin/echo");
35 * ```
36 *
37 * You can also load a full path and then operate on its components:
38 *
39 * ```c
40 * g_auto (GPathBuf) path;
41 *
42 * g_path_buf_init_from_path (&path, "/usr/bin/echo");
43 *
44 * g_path_buf_pop (&path);
45 * g_path_buf_push (&path, "sh");
46 *
47 * g_autofree char *sh = g_path_buf_to_path (&path);
48 * g_assert_cmpstr (sh, ==, "/usr/bin/sh");
49 * ```
50 *
51 * Since: 2.76
52 */
53
54 typedef struct {
55 /* (nullable) (owned) (element-type filename) */
56 GPtrArray *path;
57
58 /* (nullable) (owned) */
59 char *extension;
60
61 gpointer padding[6];
62 } RealPathBuf;
63
64 G_STATIC_ASSERT (sizeof (GPathBuf) == sizeof (RealPathBuf));
65
66 #define PATH_BUF(b) ((RealPathBuf *) (b))
67
68 /**
69 * g_path_buf_init:
70 * @buf: a path buffer
71 *
72 * Initializes a `GPathBuf` instance.
73 *
74 * Returns: (transfer none): the initialized path builder
75 *
76 * Since: 2.76
77 */
78 GPathBuf *
79 g_path_buf_init (GPathBuf *buf)
80 {
81 RealPathBuf *rbuf = PATH_BUF (buf);
82
83 rbuf->path = NULL;
84 rbuf->extension = NULL;
85
86 return buf;
87 }
88
89 /**
90 * g_path_buf_init_from_path:
91 * @buf: a path buffer
92 * @path: (type filename) (nullable): a file system path
93 *
94 * Initializes a `GPathBuf` instance with the given path.
95 *
96 * Returns: (transfer none): the initialized path builder
97 *
98 * Since: 2.76
99 */
100 GPathBuf *
101 g_path_buf_init_from_path (GPathBuf *buf,
102 const char *path)
103 {
104 g_return_val_if_fail (buf != NULL, NULL);
105 g_return_val_if_fail (path == NULL || *path != '\0', NULL);
106
107 g_path_buf_init (buf);
108
109 if (path == NULL)
110 return buf;
111 else
112 return g_path_buf_push (buf, path);
113 }
114
115 /**
116 * g_path_buf_clear:
117 * @buf: a path buffer
118 *
119 * Clears the contents of the path buffer.
120 *
121 * This function should be use to free the resources in a stack-allocated
122 * `GPathBuf` initialized using g_path_buf_init() or
123 * g_path_buf_init_from_path().
124 *
125 * Since: 2.76
126 */
127 void
128 g_path_buf_clear (GPathBuf *buf)
129 {
130 RealPathBuf *rbuf = PATH_BUF (buf);
131
132 g_return_if_fail (buf != NULL);
133
134 g_clear_pointer (&rbuf->path, g_ptr_array_unref);
135 g_clear_pointer (&rbuf->extension, g_free);
136 }
137
138 /**
139 * g_path_buf_clear_to_path:
140 * @buf: a path buffer
141 *
142 * Clears the contents of the path buffer and returns the built path.
143 *
144 * This function returns `NULL` if the `GPathBuf` is empty.
145 *
146 * See also: g_path_buf_to_path()
147 *
148 * Returns: (transfer full) (nullable) (type filename): the built path
149 *
150 * Since: 2.76
151 */
152 char *
153 g_path_buf_clear_to_path (GPathBuf *buf)
154 {
155 char *res;
156
157 g_return_val_if_fail (buf != NULL, NULL);
158
159 res = g_path_buf_to_path (buf);
160 g_path_buf_clear (buf);
161
162 return g_steal_pointer (&res);
163 }
164
165 /**
166 * g_path_buf_new:
167 *
168 * Allocates a new `GPathBuf`.
169 *
170 * Returns: (transfer full): the newly allocated path buffer
171 *
172 * Since: 2.76
173 */
174 GPathBuf *
175 g_path_buf_new (void)
176 {
177 return g_path_buf_init (g_new (GPathBuf, 1));
178 }
179
180 /**
181 * g_path_buf_new_from_path:
182 * @path: (type filename) (nullable): the path used to initialize the buffer
183 *
184 * Allocates a new `GPathBuf` with the given @path.
185 *
186 * Returns: (transfer full): the newly allocated path buffer
187 *
188 * Since: 2.76
189 */
190 GPathBuf *
191 g_path_buf_new_from_path (const char *path)
192 {
193 return g_path_buf_init_from_path (g_new (GPathBuf, 1), path);
194 }
195
196 /**
197 * g_path_buf_free:
198 * @buf: (transfer full) (not nullable): a path buffer
199 *
200 * Frees a `GPathBuf` allocated by g_path_buf_new().
201 *
202 * Since: 2.76
203 */
204 void
205 g_path_buf_free (GPathBuf *buf)
206 {
207 g_return_if_fail (buf != NULL);
208
209 g_path_buf_clear (buf);
210 g_free (buf);
211 }
212
213 /**
214 * g_path_buf_free_to_path:
215 * @buf: (transfer full) (not nullable): a path buffer
216 *
217 * Frees a `GPathBuf` allocated by g_path_buf_new(), and
218 * returns the path inside the buffer.
219 *
220 * This function returns `NULL` if the `GPathBuf` is empty.
221 *
222 * See also: g_path_buf_to_path()
223 *
224 * Returns: (transfer full) (nullable) (type filename): the path
225 *
226 * Since: 2.76
227 */
228 char *
229 g_path_buf_free_to_path (GPathBuf *buf)
230 {
231 char *res;
232
233 g_return_val_if_fail (buf != NULL, NULL);
234
235 res = g_path_buf_clear_to_path (buf);
236 g_path_buf_free (buf);
237
238 return g_steal_pointer (&res);
239 }
240
241 /**
242 * g_path_buf_copy:
243 * @buf: (not nullable): a path buffer
244 *
245 * Copies the contents of a path buffer into a new `GPathBuf`.
246 *
247 * Returns: (transfer full): the newly allocated path buffer
248 *
249 * Since: 2.76
250 */
251 GPathBuf *
252 g_path_buf_copy (GPathBuf *buf)
253 {
254 RealPathBuf *rbuf = PATH_BUF (buf);
255 RealPathBuf *rcopy;
256 GPathBuf *copy;
257
258 g_return_val_if_fail (buf != NULL, NULL);
259
260 copy = g_path_buf_new ();
261 rcopy = PATH_BUF (copy);
262
263 if (rbuf->path != NULL)
264 {
265 rcopy->path = g_ptr_array_new_null_terminated (rbuf->path->len, g_free, TRUE);
266 for (guint i = 0; i < rbuf->path->len; i++)
267 {
268 const char *p = g_ptr_array_index (rbuf->path, i);
269
270 if (p != NULL)
271 g_ptr_array_add (rcopy->path, g_strdup (p));
272 }
273 }
274
275 rcopy->extension = g_strdup (rbuf->extension);
276
277 return copy;
278 }
279
280 /**
281 * g_path_buf_push:
282 * @buf: a path buffer
283 * @path: (type filename): a path
284 *
285 * Extends the given path buffer with @path.
286 *
287 * If @path is absolute, it replaces the current path.
288 *
289 * If @path contains a directory separator, the buffer is extended by
290 * as many elements the path provides.
291 *
292 * On Windows, both forward slashes and backslashes are treated as
293 * directory separators. On other platforms, %G_DIR_SEPARATOR_S is the
294 * only directory separator.
295 *
296 * |[<!-- language="C" -->
297 * GPathBuf buf, cmp;
298 *
299 * g_path_buf_init_from_path (&buf, "/tmp");
300 * g_path_buf_push (&buf, ".X11-unix/X0");
301 * g_path_buf_init_from_path (&cmp, "/tmp/.X11-unix/X0");
302 * g_assert_true (g_path_buf_equal (&buf, &cmp));
303 * g_path_buf_clear (&cmp);
304 *
305 * g_path_buf_push (&buf, "/etc/locale.conf");
306 * g_path_buf_init_from_path (&cmp, "/etc/locale.conf");
307 * g_assert_true (g_path_buf_equal (&buf, &cmp));
308 * g_path_buf_clear (&cmp);
309 *
310 * g_path_buf_clear (&buf);
311 * ]|
312 *
313 * Returns: (transfer none): the same pointer to @buf, for convenience
314 *
315 * Since: 2.76
316 */
317 GPathBuf *
318 g_path_buf_push (GPathBuf *buf,
319 const char *path)
320 {
321 RealPathBuf *rbuf = PATH_BUF (buf);
322
323 g_return_val_if_fail (buf != NULL, NULL);
324 g_return_val_if_fail (path != NULL && *path != '\0', buf);
325
326 if (g_path_is_absolute (path))
327 {
328 #ifdef G_OS_WIN32
329 char **elements = g_strsplit_set (path, "\\/", -1);
330 #else
331 char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
332 #endif
333
334 #ifdef G_OS_UNIX
335 /* strsplit() will add an empty element for the leading root,
336 * which will cause the path build to ignore it; to avoid it,
337 * we re-inject the root as the first element.
338 *
339 * The first string is empty, but it's still allocated, so we
340 * need to free it to avoid leaking it.
341 */
342 g_free (elements[0]);
343 elements[0] = g_strdup ("/");
344 #endif
345
346 g_clear_pointer (&rbuf->path, g_ptr_array_unref);
347 rbuf->path = g_ptr_array_new_null_terminated (g_strv_length (elements), g_free, TRUE);
348
349 /* Skip empty elements caused by repeated separators */
350 for (guint i = 0; elements[i] != NULL; i++)
351 {
352 if (*elements[i] != '\0')
353 g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
354 else
355 g_free (elements[i]);
356 }
357
358 g_free (elements);
359 }
360 else
361 {
362 char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
363
364 if (rbuf->path == NULL)
365 rbuf->path = g_ptr_array_new_null_terminated (g_strv_length (elements), g_free, TRUE);
366
367 /* Skip empty elements caused by repeated separators */
368 for (guint i = 0; elements[i] != NULL; i++)
369 {
370 if (*elements[i] != '\0')
371 g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
372 else
373 g_free (elements[i]);
374 }
375
376 g_free (elements);
377 }
378
379 return buf;
380 }
381
382 /**
383 * g_path_buf_pop:
384 * @buf: a path buffer
385 *
386 * Removes the last element of the path buffer.
387 *
388 * If there is only one element in the path buffer (for example, `/` on
389 * Unix-like operating systems or the drive on Windows systems), it will
390 * not be removed and %FALSE will be returned instead.
391 *
392 * |[<!-- language="C" -->
393 * GPathBuf buf, cmp;
394 *
395 * g_path_buf_init_from_path (&buf, "/bin/sh");
396 *
397 * g_path_buf_pop (&buf);
398 * g_path_buf_init_from_path (&cmp, "/bin");
399 * g_assert_true (g_path_buf_equal (&buf, &cmp));
400 * g_path_buf_clear (&cmp);
401 *
402 * g_path_buf_pop (&buf);
403 * g_path_buf_init_from_path (&cmp, "/");
404 * g_assert_true (g_path_buf_equal (&buf, &cmp));
405 * g_path_buf_clear (&cmp);
406 *
407 * g_path_buf_clear (&buf);
408 * ]|
409 *
410 * Returns: `TRUE` if the buffer was modified and `FALSE` otherwise
411 *
412 * Since: 2.76
413 */
414 gboolean
415 g_path_buf_pop (GPathBuf *buf)
416 {
417 RealPathBuf *rbuf = PATH_BUF (buf);
418
419 g_return_val_if_fail (buf != NULL, FALSE);
420 g_return_val_if_fail (rbuf->path != NULL, FALSE);
421
422 /* Keep the first element of the buffer; it's either '/' or the drive */
423 if (rbuf->path->len > 1)
424 {
425 g_ptr_array_remove_index (rbuf->path, rbuf->path->len - 1);
426 return TRUE;
427 }
428
429 return FALSE;
430 }
431
432 /**
433 * g_path_buf_set_filename:
434 * @buf: a path buffer
435 * @file_name: (type filename) (not nullable): the file name in the path
436 *
437 * Sets the file name of the path.
438 *
439 * If the path buffer is empty, the filename is left unset and this
440 * function returns `FALSE`.
441 *
442 * If the path buffer only contains the root element (on Unix-like operating
443 * systems) or the drive (on Windows), this is the equivalent of pushing
444 * the new @file_name.
445 *
446 * If the path buffer contains a path, this is the equivalent of
447 * popping the path buffer and pushing @file_name, creating a
448 * sibling of the original path.
449 *
450 * |[<!-- language="C" -->
451 * GPathBuf buf, cmp;
452 *
453 * g_path_buf_init_from_path (&buf, "/");
454 *
455 * g_path_buf_set_filename (&buf, "bar");
456 * g_path_buf_init_from_path (&cmp, "/bar");
457 * g_assert_true (g_path_buf_equal (&buf, &cmp));
458 * g_path_buf_clear (&cmp);
459 *
460 * g_path_buf_set_filename (&buf, "baz.txt");
461 * g_path_buf_init_from_path (&cmp, "/baz.txt");
462 * g_assert_true (g_path_buf_equal (&buf, &cmp);
463 * g_path_buf_clear (&cmp);
464 *
465 * g_path_buf_clear (&buf);
466 * ]|
467 *
468 * Returns: `TRUE` if the file name was replaced, and `FALSE` otherwise
469 *
470 * Since: 2.76
471 */
472 gboolean
473 g_path_buf_set_filename (GPathBuf *buf,
474 const char *file_name)
475 {
476 g_return_val_if_fail (buf != NULL, FALSE);
477 g_return_val_if_fail (file_name != NULL, FALSE);
478
479 if (PATH_BUF (buf)->path == NULL)
480 return FALSE;
481
482 g_path_buf_pop (buf);
483 g_path_buf_push (buf, file_name);
484
485 return TRUE;
486 }
487
488 /**
489 * g_path_buf_set_extension:
490 * @buf: a path buffer
491 * @extension: (type filename) (nullable): the file extension
492 *
493 * Adds an extension to the file name in the path buffer.
494 *
495 * If @extension is `NULL`, the extension will be unset.
496 *
497 * If the path buffer does not have a file name set, this function returns
498 * `FALSE` and leaves the path buffer unmodified.
499 *
500 * Returns: `TRUE` if the extension was replaced, and `FALSE` otherwise
501 *
502 * Since: 2.76
503 */
504 gboolean
505 g_path_buf_set_extension (GPathBuf *buf,
506 const char *extension)
507 {
508 RealPathBuf *rbuf = PATH_BUF (buf);
509
510 g_return_val_if_fail (buf != NULL, FALSE);
511
512 if (rbuf->path != NULL)
513 return g_set_str (&rbuf->extension, extension);
514 else
515 return FALSE;
516 }
517
518 /**
519 * g_path_buf_to_path:
520 * @buf: a path buffer
521 *
522 * Retrieves the built path from the path buffer.
523 *
524 * On Windows, the result contains backslashes as directory separators,
525 * even if forward slashes were used in input.
526 *
527 * If the path buffer is empty, this function returns `NULL`.
528 *
529 * Returns: (transfer full) (type filename) (nullable): the path
530 *
531 * Since: 2.76
532 */
533 char *
534 g_path_buf_to_path (GPathBuf *buf)
535 {
536 RealPathBuf *rbuf = PATH_BUF (buf);
537 char *path = NULL;
538
539 g_return_val_if_fail (buf != NULL, NULL);
540
541 if (rbuf->path != NULL)
542 path = g_build_filenamev ((char **) rbuf->path->pdata);
543
544 if (path != NULL && rbuf->extension != NULL)
545 {
546 char *tmp = g_strconcat (path, ".", rbuf->extension, NULL);
547
548 g_free (path);
549 path = g_steal_pointer (&tmp);
550 }
551
552 return path;
553 }
554
555 /**
556 * g_path_buf_equal:
557 * @v1: (not nullable): a path buffer to compare
558 * @v2: (not nullable): a path buffer to compare
559 *
560 * Compares two path buffers for equality and returns `TRUE`
561 * if they are equal.
562 *
563 * The path inside the paths buffers are not going to be normalized,
564 * so `X/Y/Z/A/..`, `X/./Y/Z` and `X/Y/Z` are not going to be considered
565 * equal.
566 *
567 * This function can be passed to g_hash_table_new() as the
568 * `key_equal_func` parameter.
569 *
570 * Returns: `TRUE` if the two path buffers are equal,
571 * and `FALSE` otherwise
572 *
573 * Since: 2.76
574 */
575 gboolean
576 g_path_buf_equal (gconstpointer v1,
577 gconstpointer v2)
578 {
579 if (v1 == v2)
580 return TRUE;
581
582 /* We resolve the buffer into a path to normalize its contents;
583 * this won't resolve symbolic links or `.` and `..` components
584 */
585 char *p1 = g_path_buf_to_path ((GPathBuf *) v1);
586 char *p2 = g_path_buf_to_path ((GPathBuf *) v2);
587
588 gboolean res = p1 != NULL && p2 != NULL
589 ? g_str_equal (p1, p2)
590 : FALSE;
591
592 g_free (p1);
593 g_free (p2);
594
595 return res;
596 }