Coverage Report

Created: 2026-05-30 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fwupd/libfwupdplugin/fu-path.c
Line
Count
Source
1
/*
2
 * Copyright 2017 Richard Hughes <richard@hughsie.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-or-later
5
 */
6
7
0
#define G_LOG_DOMAIN "FuCommon"
8
9
#include "config.h"
10
11
#include <errno.h>
12
#include <glib/gstdio.h>
13
14
#ifdef _WIN32
15
#include <stdlib.h>
16
#endif
17
18
#include "fwupd-error.h"
19
20
#include "fu-common.h"
21
#include "fu-path.h"
22
23
static gboolean
24
fu_path_delete(const gchar *path, GError **error)
25
0
{
26
0
  g_autoptr(GFile) file = g_file_new_for_path(path);
27
0
  if (!g_file_delete(file, NULL, error)) {
28
0
    fwupd_error_convert(error);
29
0
    return FALSE;
30
0
  }
31
0
  return TRUE;
32
0
}
33
34
/**
35
 * fu_path_rmtree:
36
 * @directory: a directory name
37
 * @error: (nullable): optional return location for an error
38
 *
39
 * Recursively removes a directory.
40
 *
41
 * Returns: %TRUE for success, %FALSE otherwise
42
 *
43
 * Since: 1.8.2
44
 **/
45
gboolean
46
fu_path_rmtree(const gchar *directory, GError **error)
47
0
{
48
0
  const gchar *filename;
49
0
  g_autoptr(GDir) dir = NULL;
50
51
0
  g_return_val_if_fail(directory != NULL, FALSE);
52
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
53
54
  /* try to open */
55
0
  g_debug("removing %s", directory);
56
0
  dir = g_dir_open(directory, 0, error);
57
0
  if (dir == NULL)
58
0
    return FALSE;
59
60
  /* find each */
61
0
  while ((filename = g_dir_read_name(dir))) {
62
0
    g_autofree gchar *src = NULL;
63
0
    src = g_build_filename(directory, filename, NULL);
64
0
    if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
65
0
      if (!fu_path_rmtree(src, error))
66
0
        return FALSE;
67
0
    } else {
68
0
      if (!fu_path_delete(src, error))
69
0
        return FALSE;
70
0
    }
71
0
  }
72
0
  return fu_path_delete(directory, error);
73
0
}
74
75
static gboolean
76
fu_path_get_file_list_internal(GPtrArray *files, const gchar *directory, GError **error)
77
0
{
78
0
  const gchar *filename;
79
0
  g_autoptr(GDir) dir = NULL;
80
81
  /* try to open */
82
0
  dir = g_dir_open(directory, 0, error);
83
0
  if (dir == NULL) {
84
0
    fwupd_error_convert(error);
85
0
    return FALSE;
86
0
  }
87
88
  /* find each */
89
0
  while ((filename = g_dir_read_name(dir))) {
90
0
    g_autofree gchar *src = g_build_filename(directory, filename, NULL);
91
0
    if (g_file_test(src, G_FILE_TEST_IS_SYMLINK))
92
0
      continue;
93
0
    if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
94
0
      if (!fu_path_get_file_list_internal(files, src, error))
95
0
        return FALSE;
96
0
    } else {
97
0
      g_ptr_array_add(files, g_steal_pointer(&src));
98
0
    }
99
0
  }
100
0
  return TRUE;
101
0
}
102
103
/**
104
 * fu_path_get_files:
105
 * @path: a directory name
106
 * @error: (nullable): optional return location for an error
107
 *
108
 * Returns every file found under @directory, and any subdirectory.
109
 * If any path under @directory cannot be accessed due to permissions an error
110
 * will be returned.
111
 *
112
 * Returns: (transfer container) (element-type utf8): array of files, or %NULL for error
113
 *
114
 * Since: 1.8.2
115
 **/
116
GPtrArray *
117
fu_path_get_files(const gchar *path, GError **error)
118
0
{
119
0
  g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);
120
121
0
  g_return_val_if_fail(path != NULL, NULL);
122
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
123
124
0
  if (!fu_path_get_file_list_internal(files, path, error))
125
0
    return NULL;
126
0
  return g_steal_pointer(&files);
127
0
}
128
129
/**
130
 * fu_path_mkdir:
131
 * @dirname: a directory name
132
 * @error: (nullable): optional return location for an error
133
 *
134
 * Creates any required directories, including any parent directories.
135
 *
136
 * Returns: %TRUE for success
137
 *
138
 * Since: 1.8.2
139
 **/
140
gboolean
141
fu_path_mkdir(const gchar *dirname, GError **error)
142
0
{
143
0
  g_return_val_if_fail(dirname != NULL, FALSE);
144
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
145
146
0
  if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
147
0
    g_debug("creating path %s", dirname);
148
0
  if (g_mkdir_with_parents(dirname, 0755) == -1) {
149
0
    g_set_error(error,
150
0
          FWUPD_ERROR,
151
0
          FWUPD_ERROR_INTERNAL,
152
0
          "Failed to create '%s': %s",
153
0
          dirname,
154
0
          fwupd_strerror(errno));
155
0
    return FALSE;
156
0
  }
157
0
  return TRUE;
158
0
}
159
160
/**
161
 * fu_path_mkdir_parent:
162
 * @filename: a full pathname
163
 * @error: (nullable): optional return location for an error
164
 *
165
 * Creates any required directories, including any parent directories.
166
 *
167
 * Returns: %TRUE for success
168
 *
169
 * Since: 1.8.2
170
 **/
171
gboolean
172
fu_path_mkdir_parent(const gchar *filename, GError **error)
173
0
{
174
0
  g_autofree gchar *parent = NULL;
175
176
0
  g_return_val_if_fail(filename != NULL, FALSE);
177
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
178
179
0
  parent = g_path_get_dirname(filename);
180
0
  return fu_path_mkdir(parent, error);
181
0
}
182
183
/**
184
 * fu_path_find_program:
185
 * @basename: the program to search
186
 * @error: (nullable): optional return location for an error
187
 *
188
 * Looks for a program in the PATH variable
189
 *
190
 * Returns: a new #gchar, or %NULL for error
191
 *
192
 * Since: 1.8.2
193
 **/
194
gchar *
195
fu_path_find_program(const gchar *basename, GError **error)
196
0
{
197
0
  gchar *fn = g_find_program_in_path(basename);
198
0
  if (fn == NULL) {
199
0
    g_set_error(error,
200
0
          FWUPD_ERROR,
201
0
          FWUPD_ERROR_NOT_SUPPORTED,
202
0
          "missing executable %s in PATH",
203
0
          basename);
204
0
    return NULL;
205
0
  }
206
0
  return fn;
207
0
}
208
209
static gint
210
fu_path_glob_sort_cb(gconstpointer a, gconstpointer b)
211
0
{
212
0
  return g_strcmp0(*(const gchar **)a, *(const gchar **)b);
213
0
}
214
215
/**
216
 * fu_path_glob:
217
 * @directory: a directory path
218
 * @pattern: a glob pattern, e.g. `*foo*`
219
 * @error: (nullable): optional return location for an error
220
 *
221
 * Returns all the filenames that match a specific glob pattern.
222
 * Any results are sorted. No matching files will set @error.
223
 *
224
 * Returns:  (element-type utf8) (transfer container): matching files, or %NULL
225
 *
226
 * Since: 1.8.2
227
 **/
228
GPtrArray *
229
fu_path_glob(const gchar *directory, const gchar *pattern, GError **error)
230
0
{
231
0
  const gchar *basename;
232
0
  g_autoptr(GDir) dir = NULL;
233
0
  g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);
234
235
0
  g_return_val_if_fail(directory != NULL, NULL);
236
0
  g_return_val_if_fail(pattern != NULL, NULL);
237
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
238
239
0
  dir = g_dir_open(directory, 0, error);
240
0
  if (dir == NULL)
241
0
    return NULL;
242
0
  while ((basename = g_dir_read_name(dir)) != NULL) {
243
0
    if (!g_pattern_match_simple(pattern, basename))
244
0
      continue;
245
0
    g_ptr_array_add(files, g_build_filename(directory, basename, NULL));
246
0
  }
247
0
  if (files->len == 0) {
248
0
    g_set_error_literal(error,
249
0
            FWUPD_ERROR,
250
0
            FWUPD_ERROR_NOT_FOUND,
251
0
            "no files matched pattern");
252
0
    return NULL;
253
0
  }
254
0
  g_ptr_array_sort(files, fu_path_glob_sort_cb);
255
0
  return g_steal_pointer(&files);
256
0
}
257
258
/**
259
 * fu_path_make_absolute:
260
 * @filename: a path to a filename, perhaps symlinked
261
 * @error: (nullable): optional return location for an error
262
 *
263
 * Returns the resolved absolute file name.
264
 *
265
 * Returns: (transfer full): path, or %NULL on error
266
 *
267
 * Since: 2.0.0
268
 **/
269
gchar *
270
fu_path_make_absolute(const gchar *filename, GError **error)
271
0
{
272
0
  char full_tmp[PATH_MAX];
273
274
0
  g_return_val_if_fail(filename != NULL, NULL);
275
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
276
277
0
#ifdef HAVE_REALPATH
278
0
  if (realpath(filename, full_tmp) == NULL) {
279
0
    g_set_error(error,
280
0
          FWUPD_ERROR,
281
0
          FWUPD_ERROR_INVALID_DATA,
282
0
          "cannot resolve path: %s",
283
0
          fwupd_strerror(errno));
284
0
    return NULL;
285
0
  }
286
#else
287
  if (_fullpath(full_tmp, filename, sizeof(full_tmp)) == NULL) {
288
    g_set_error(error,
289
          FWUPD_ERROR,
290
          FWUPD_ERROR_INVALID_DATA,
291
          "cannot resolve path: %s",
292
          fwupd_strerror(errno));
293
    return NULL;
294
  }
295
#endif
296
0
  if (!g_file_test(full_tmp, G_FILE_TEST_EXISTS)) {
297
0
    g_set_error(error,
298
0
          FWUPD_ERROR,
299
0
          FWUPD_ERROR_INVALID_DATA,
300
0
          "cannot find path: %s",
301
0
          full_tmp);
302
0
    return NULL;
303
0
  }
304
0
  return g_strdup(full_tmp);
305
0
}
306
307
/**
308
 * fu_path_get_symlink_target:
309
 * @filename: a path to a symlink
310
 * @error: (nullable): optional return location for an error
311
 *
312
 * Returns the symlink target.
313
 *
314
 * Returns: (transfer full): path, or %NULL on error
315
 *
316
 * Since: 2.0.0
317
 **/
318
gchar *
319
fu_path_get_symlink_target(const gchar *filename, GError **error)
320
0
{
321
0
  const gchar *target;
322
0
  g_autoptr(GFile) file = NULL;
323
0
  g_autoptr(GFileInfo) info = NULL;
324
325
0
  file = g_file_new_for_path(filename);
326
0
  info = g_file_query_info(file,
327
0
         G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
328
0
         G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
329
0
         NULL,
330
0
         error);
331
0
  if (info == NULL) {
332
0
    fwupd_error_convert(error);
333
0
    return NULL;
334
0
  }
335
0
  target =
336
0
      g_file_info_get_attribute_byte_string(info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET);
337
0
  if (target == NULL) {
338
0
    g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no symlink target");
339
0
    return NULL;
340
0
  }
341
342
  /* success */
343
0
  return g_strdup(target);
344
0
}
345
346
/**
347
 * fu_path_verify_safe:
348
 * @filename: an optional local path and basename
349
 * @error: (nullable): optional return location for an error
350
 *
351
 * Verifies the path is safe to use from an archive.
352
 *
353
 * This will reject:
354
 * - an empty string
355
 * - absolute paths, e.g. `/etc/fstab`
356
 * - paths with relative locations, e.g. `../../etc/fstab`)
357
 * - paths with MS-DOS path separators, e.g. `foo\bar`
358
 * - non-ASCII filenames
359
 *
360
 * Returns: %TRUE on success
361
 *
362
 * Since: 2.1.2
363
 **/
364
gboolean
365
fu_path_verify_safe(const gchar *filename, GError **error)
366
24.2k
{
367
24.2k
  g_return_val_if_fail(filename != NULL, FALSE);
368
24.2k
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
369
370
  /* not ASCII */
371
24.2k
  if (!g_str_is_ascii(filename)) {
372
30
    g_set_error_literal(error,
373
30
            FWUPD_ERROR,
374
30
            FWUPD_ERROR_NOT_SUPPORTED,
375
30
            "non-ASCII filenames not allowed");
376
30
    return FALSE;
377
30
  }
378
379
  /* not a basename */
380
24.2k
  if (g_strcmp0(filename, "") == 0) {
381
25
    g_set_error_literal(error,
382
25
            FWUPD_ERROR,
383
25
            FWUPD_ERROR_NOT_SUPPORTED,
384
25
            "empty string not valid");
385
25
    return FALSE;
386
25
  }
387
24.2k
  if (g_strcmp0(filename, ".") == 0 || g_strcmp0(filename, "..") == 0) {
388
5
    g_set_error_literal(error,
389
5
            FWUPD_ERROR,
390
5
            FWUPD_ERROR_NOT_SUPPORTED,
391
5
            "special paths not allowed");
392
5
    return FALSE;
393
5
  }
394
395
  /* absolute */
396
24.2k
  if (g_path_is_absolute(filename)) {
397
5
    g_set_error_literal(error,
398
5
            FWUPD_ERROR,
399
5
            FWUPD_ERROR_NOT_SUPPORTED,
400
5
            "absolute paths not allowed");
401
5
    return FALSE;
402
5
  }
403
404
  /* relative */
405
24.2k
  if (g_str_has_prefix(filename, "../") || g_strstr_len(filename, -1, "/../") != NULL ||
406
24.2k
      g_str_has_suffix(filename, "/..")) {
407
9
    g_set_error_literal(error,
408
9
            FWUPD_ERROR,
409
9
            FWUPD_ERROR_NOT_SUPPORTED,
410
9
            "path traversal detected");
411
9
    return FALSE;
412
9
  }
413
414
  /* MS-DOS */
415
24.2k
  if (g_strstr_len(filename, -1, "\\") != NULL) {
416
2
    g_set_error_literal(error,
417
2
            FWUPD_ERROR,
418
2
            FWUPD_ERROR_NOT_SUPPORTED,
419
2
            "MS-DOS path detected");
420
2
    return FALSE;
421
2
  }
422
423
  /* success */
424
24.2k
  return TRUE;
425
24.2k
}
426
427
/**
428
 * fu_path_sanitize_basename:
429
 * @str: a string to sanitize
430
 *
431
 * Sanitizes a string for use as a path basename by replacing path separators
432
 * and other dangerous characters with underscores.
433
 *
434
 * Returns: (transfer full): a newly allocated sanitized string
435
 *
436
 * Since: 2.1.4
437
 **/
438
gchar *
439
fu_path_sanitize_basename(const gchar *str)
440
0
{
441
0
  g_autoptr(GString) result = g_string_new(str);
442
443
0
  g_return_val_if_fail(str != NULL, NULL);
444
445
  /* replace dangerous characters */
446
0
  g_string_replace(result, "/", "_", 0);
447
0
  g_string_replace(result, "\\", "_", 0);
448
0
  g_string_replace(result, "..", "_", 0);
449
450
  /* detect hidden files */
451
0
  if (g_str_has_prefix(result->str, "."))
452
0
    result->str[0] = '_';
453
454
0
  return g_string_free(g_steal_pointer(&result), FALSE);
455
0
}