Branch data Line data Source code
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 : 23 : g_path_buf_init (GPathBuf *buf)
80 : : {
81 : 23 : RealPathBuf *rbuf = PATH_BUF (buf);
82 : :
83 : 23 : rbuf->path = NULL;
84 : 23 : rbuf->extension = NULL;
85 : :
86 : 23 : 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 : 18 : g_path_buf_init_from_path (GPathBuf *buf,
102 : : const char *path)
103 : : {
104 : 18 : g_return_val_if_fail (buf != NULL, NULL);
105 : 18 : g_return_val_if_fail (path == NULL || *path != '\0', NULL);
106 : :
107 : 18 : g_path_buf_init (buf);
108 : :
109 : 18 : if (path == NULL)
110 : 1 : return buf;
111 : : else
112 : 17 : 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 : 27 : g_path_buf_clear (GPathBuf *buf)
129 : : {
130 : 27 : RealPathBuf *rbuf = PATH_BUF (buf);
131 : :
132 : 27 : g_return_if_fail (buf != NULL);
133 : :
134 : 27 : g_clear_pointer (&rbuf->path, g_ptr_array_unref);
135 : 27 : 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 : 2 : g_path_buf_clear_to_path (GPathBuf *buf)
154 : : {
155 : : char *res;
156 : :
157 : 2 : g_return_val_if_fail (buf != NULL, NULL);
158 : :
159 : 2 : res = g_path_buf_to_path (buf);
160 : 2 : g_path_buf_clear (buf);
161 : :
162 : 2 : 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 : 2 : g_path_buf_new (void)
176 : : {
177 : 2 : 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 : 2 : g_path_buf_new_from_path (const char *path)
192 : : {
193 : 2 : 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 : 4 : g_path_buf_free (GPathBuf *buf)
206 : : {
207 : 4 : g_return_if_fail (buf != NULL);
208 : :
209 : 4 : g_path_buf_clear (buf);
210 : 4 : 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 : 1 : g_path_buf_free_to_path (GPathBuf *buf)
230 : : {
231 : : char *res;
232 : :
233 : 1 : g_return_val_if_fail (buf != NULL, NULL);
234 : :
235 : 1 : res = g_path_buf_clear_to_path (buf);
236 : 1 : g_path_buf_free (buf);
237 : :
238 : 1 : 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 : 1 : g_path_buf_copy (GPathBuf *buf)
253 : : {
254 : 1 : RealPathBuf *rbuf = PATH_BUF (buf);
255 : : RealPathBuf *rcopy;
256 : : GPathBuf *copy;
257 : :
258 : 1 : g_return_val_if_fail (buf != NULL, NULL);
259 : :
260 : 1 : copy = g_path_buf_new ();
261 : 1 : rcopy = PATH_BUF (copy);
262 : :
263 : 1 : if (rbuf->path != NULL)
264 : : {
265 : 1 : rcopy->path = g_ptr_array_new_null_terminated (rbuf->path->len, g_free, TRUE);
266 : 5 : for (guint i = 0; i < rbuf->path->len; i++)
267 : : {
268 : 4 : const char *p = g_ptr_array_index (rbuf->path, i);
269 : :
270 : 4 : if (p != NULL)
271 : 4 : g_ptr_array_add (rcopy->path, g_strdup (p));
272 : : }
273 : : }
274 : :
275 : 1 : rcopy->extension = g_strdup (rbuf->extension);
276 : :
277 : 1 : 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 : 25 : g_path_buf_push (GPathBuf *buf,
319 : : const char *path)
320 : : {
321 : 25 : RealPathBuf *rbuf = PATH_BUF (buf);
322 : :
323 : 25 : g_return_val_if_fail (buf != NULL, NULL);
324 : 25 : g_return_val_if_fail (path != NULL && *path != '\0', buf);
325 : :
326 : 25 : if (g_path_is_absolute (path))
327 : : {
328 : : #ifdef G_OS_WIN32
329 : : char **elements = g_strsplit_set (path, "\\/", -1);
330 : : #else
331 : 20 : 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 : 20 : g_free (elements[0]);
343 : 20 : elements[0] = g_strdup ("/");
344 : : #endif
345 : :
346 : 20 : g_clear_pointer (&rbuf->path, g_ptr_array_unref);
347 : 20 : 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 : 78 : for (guint i = 0; elements[i] != NULL; i++)
351 : : {
352 : 58 : if (*elements[i] != '\0')
353 : 56 : g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
354 : : else
355 : 2 : g_free (elements[i]);
356 : : }
357 : :
358 : 20 : g_free (elements);
359 : : }
360 : : else
361 : : {
362 : 5 : char **elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
363 : :
364 : 5 : if (rbuf->path == NULL)
365 : 0 : 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 : 11 : for (guint i = 0; elements[i] != NULL; i++)
369 : : {
370 : 6 : if (*elements[i] != '\0')
371 : 6 : g_ptr_array_add (rbuf->path, g_steal_pointer (&elements[i]));
372 : : else
373 : 0 : g_free (elements[i]);
374 : : }
375 : :
376 : 5 : g_free (elements);
377 : : }
378 : :
379 : 25 : 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 : 5 : g_path_buf_pop (GPathBuf *buf)
416 : : {
417 : 5 : RealPathBuf *rbuf = PATH_BUF (buf);
418 : :
419 : 5 : g_return_val_if_fail (buf != NULL, FALSE);
420 : 5 : 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 : 5 : if (rbuf->path->len > 1)
424 : : {
425 : 3 : g_ptr_array_remove_index (rbuf->path, rbuf->path->len - 1);
426 : 3 : return TRUE;
427 : : }
428 : :
429 : 2 : 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 : 3 : g_path_buf_set_filename (GPathBuf *buf,
474 : : const char *file_name)
475 : : {
476 : 3 : g_return_val_if_fail (buf != NULL, FALSE);
477 : 3 : g_return_val_if_fail (file_name != NULL, FALSE);
478 : :
479 : 3 : if (PATH_BUF (buf)->path == NULL)
480 : 1 : return FALSE;
481 : :
482 : 2 : g_path_buf_pop (buf);
483 : 2 : g_path_buf_push (buf, file_name);
484 : :
485 : 2 : 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 : 2 : g_path_buf_set_extension (GPathBuf *buf,
506 : : const char *extension)
507 : : {
508 : 2 : RealPathBuf *rbuf = PATH_BUF (buf);
509 : :
510 : 2 : g_return_val_if_fail (buf != NULL, FALSE);
511 : :
512 : 2 : if (rbuf->path != NULL)
513 : 1 : return g_set_str (&rbuf->extension, extension);
514 : : else
515 : 1 : 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 : 31 : g_path_buf_to_path (GPathBuf *buf)
535 : : {
536 : 31 : RealPathBuf *rbuf = PATH_BUF (buf);
537 : 31 : char *path = NULL;
538 : :
539 : 31 : g_return_val_if_fail (buf != NULL, NULL);
540 : :
541 : 31 : if (rbuf->path != NULL)
542 : 24 : path = g_build_filenamev ((char **) rbuf->path->pdata);
543 : :
544 : 31 : if (path != NULL && rbuf->extension != NULL)
545 : : {
546 : 1 : char *tmp = g_strconcat (path, ".", rbuf->extension, NULL);
547 : :
548 : 1 : g_free (path);
549 : 1 : path = g_steal_pointer (&tmp);
550 : : }
551 : :
552 : 31 : 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 : 11 : g_path_buf_equal (gconstpointer v1,
577 : : gconstpointer v2)
578 : : {
579 : 11 : if (v1 == v2)
580 : 1 : 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 : 10 : char *p1 = g_path_buf_to_path ((GPathBuf *) v1);
586 : 10 : char *p2 = g_path_buf_to_path ((GPathBuf *) v2);
587 : :
588 : 10 : gboolean res = p1 != NULL && p2 != NULL
589 : 10 : ? g_str_equal (p1, p2)
590 : 20 : : FALSE;
591 : :
592 : 10 : g_free (p1);
593 : 10 : g_free (p2);
594 : :
595 : 10 : return res;
596 : : }
|