Coverage Report

Created: 2026-08-13 07:13

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
static gint
184
fu_path_glob_sort_cb(gconstpointer a, gconstpointer b)
185
0
{
186
0
  return g_strcmp0(*(const gchar **)a, *(const gchar **)b);
187
0
}
188
189
/**
190
 * fu_path_glob:
191
 * @directory: a directory path
192
 * @pattern: a glob pattern, e.g. `*foo*`
193
 * @error: (nullable): optional return location for an error
194
 *
195
 * Returns all the filenames that match a specific glob pattern.
196
 * Any results are sorted. No matching files will set @error.
197
 *
198
 * Returns:  (element-type utf8) (transfer container): matching files, or %NULL
199
 *
200
 * Since: 1.8.2
201
 **/
202
GPtrArray *
203
fu_path_glob(const gchar *directory, const gchar *pattern, GError **error)
204
0
{
205
0
  const gchar *basename;
206
0
  g_autoptr(GDir) dir = NULL;
207
0
  g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);
208
209
0
  g_return_val_if_fail(directory != NULL, NULL);
210
0
  g_return_val_if_fail(pattern != NULL, NULL);
211
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
212
213
0
  dir = g_dir_open(directory, 0, error);
214
0
  if (dir == NULL)
215
0
    return NULL;
216
0
  while ((basename = g_dir_read_name(dir)) != NULL) {
217
0
    if (!g_pattern_match_simple(pattern, basename))
218
0
      continue;
219
0
    g_ptr_array_add(files, g_build_filename(directory, basename, NULL));
220
0
  }
221
0
  if (files->len == 0) {
222
0
    g_set_error_literal(error,
223
0
            FWUPD_ERROR,
224
0
            FWUPD_ERROR_NOT_FOUND,
225
0
            "no files matched pattern");
226
0
    return NULL;
227
0
  }
228
0
  g_ptr_array_sort(files, fu_path_glob_sort_cb);
229
0
  return g_steal_pointer(&files);
230
0
}
231
232
/**
233
 * fu_path_make_absolute:
234
 * @filename: a path to a filename, perhaps symlinked
235
 * @error: (nullable): optional return location for an error
236
 *
237
 * Returns the resolved absolute file name.
238
 *
239
 * Returns: (transfer full): path, or %NULL on error
240
 *
241
 * Since: 2.0.0
242
 **/
243
gchar *
244
fu_path_make_absolute(const gchar *filename, GError **error)
245
0
{
246
0
  char full_tmp[PATH_MAX];
247
248
0
  g_return_val_if_fail(filename != NULL, NULL);
249
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
250
251
0
#ifdef HAVE_REALPATH
252
0
  if (realpath(filename, full_tmp) == NULL) {
253
0
    g_set_error(error,
254
0
          FWUPD_ERROR,
255
0
          FWUPD_ERROR_INVALID_DATA,
256
0
          "cannot resolve path: %s",
257
0
          fwupd_strerror(errno));
258
0
    return NULL;
259
0
  }
260
#else
261
  if (_fullpath(full_tmp, filename, sizeof(full_tmp)) == NULL) {
262
    g_set_error(error,
263
          FWUPD_ERROR,
264
          FWUPD_ERROR_INVALID_DATA,
265
          "cannot resolve path: %s",
266
          fwupd_strerror(errno));
267
    return NULL;
268
  }
269
#endif
270
0
  if (!g_file_test(full_tmp, G_FILE_TEST_EXISTS)) {
271
0
    g_set_error(error,
272
0
          FWUPD_ERROR,
273
0
          FWUPD_ERROR_INVALID_DATA,
274
0
          "cannot find path: %s",
275
0
          full_tmp);
276
0
    return NULL;
277
0
  }
278
0
  return g_strdup(full_tmp);
279
0
}
280
281
/**
282
 * fu_path_get_symlink_target:
283
 * @filename: a path to a symlink
284
 * @error: (nullable): optional return location for an error
285
 *
286
 * Returns the symlink target.
287
 *
288
 * Returns: (transfer full): path, or %NULL on error
289
 *
290
 * Since: 2.0.0
291
 **/
292
gchar *
293
fu_path_get_symlink_target(const gchar *filename, GError **error)
294
0
{
295
0
  const gchar *target;
296
0
  g_autoptr(GFile) file = NULL;
297
0
  g_autoptr(GFileInfo) info = NULL;
298
299
0
  file = g_file_new_for_path(filename);
300
0
  info = g_file_query_info(file,
301
0
         G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
302
0
         G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
303
0
         NULL,
304
0
         error);
305
0
  if (info == NULL) {
306
0
    fwupd_error_convert(error);
307
0
    return NULL;
308
0
  }
309
0
  target =
310
0
      g_file_info_get_attribute_byte_string(info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET);
311
0
  if (target == NULL) {
312
0
    g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no symlink target");
313
0
    return NULL;
314
0
  }
315
316
  /* success */
317
0
  return g_strdup(target);
318
0
}
319
320
/**
321
 * fu_path_verify_safe:
322
 * @filename: an optional local path and basename
323
 * @error: (nullable): optional return location for an error
324
 *
325
 * Verifies the path is safe to use from an archive.
326
 *
327
 * This will reject:
328
 * - an empty string
329
 * - absolute paths, e.g. `/etc/fstab`
330
 * - paths with relative locations, e.g. `../../etc/fstab`)
331
 * - paths with MS-DOS path separators, e.g. `foo\bar`
332
 * - non-ASCII filenames
333
 *
334
 * Returns: %TRUE on success
335
 *
336
 * Since: 2.1.2
337
 **/
338
gboolean
339
fu_path_verify_safe(const gchar *filename, GError **error)
340
26.7k
{
341
26.7k
  g_return_val_if_fail(filename != NULL, FALSE);
342
26.7k
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
343
344
  /* not ASCII */
345
26.7k
  if (!g_str_is_ascii(filename)) {
346
33
    g_set_error_literal(error,
347
33
            FWUPD_ERROR,
348
33
            FWUPD_ERROR_NOT_SUPPORTED,
349
33
            "non-ASCII filenames not allowed");
350
33
    return FALSE;
351
33
  }
352
353
  /* not a basename */
354
26.7k
  if (g_strcmp0(filename, "") == 0) {
355
26
    g_set_error_literal(error,
356
26
            FWUPD_ERROR,
357
26
            FWUPD_ERROR_NOT_SUPPORTED,
358
26
            "empty string not valid");
359
26
    return FALSE;
360
26
  }
361
26.7k
  if (g_strcmp0(filename, ".") == 0 || g_strcmp0(filename, "..") == 0) {
362
5
    g_set_error_literal(error,
363
5
            FWUPD_ERROR,
364
5
            FWUPD_ERROR_NOT_SUPPORTED,
365
5
            "special paths not allowed");
366
5
    return FALSE;
367
5
  }
368
369
  /* absolute */
370
26.7k
  if (g_path_is_absolute(filename)) {
371
2
    g_set_error_literal(error,
372
2
            FWUPD_ERROR,
373
2
            FWUPD_ERROR_NOT_SUPPORTED,
374
2
            "absolute paths not allowed");
375
2
    return FALSE;
376
2
  }
377
378
  /* relative */
379
26.7k
  if (g_str_has_prefix(filename, "../") || g_strstr_len(filename, -1, "/../") != NULL ||
380
26.7k
      g_str_has_suffix(filename, "/..")) {
381
9
    g_set_error_literal(error,
382
9
            FWUPD_ERROR,
383
9
            FWUPD_ERROR_NOT_SUPPORTED,
384
9
            "path traversal detected");
385
9
    return FALSE;
386
9
  }
387
388
  /* MS-DOS */
389
26.7k
  if (g_strstr_len(filename, -1, "\\") != NULL) {
390
3
    g_set_error_literal(error,
391
3
            FWUPD_ERROR,
392
3
            FWUPD_ERROR_NOT_SUPPORTED,
393
3
            "MS-DOS path detected");
394
3
    return FALSE;
395
3
  }
396
397
  /* success */
398
26.7k
  return TRUE;
399
26.7k
}
400
401
/**
402
 * fu_path_sanitize_basename:
403
 * @str: a string to sanitize
404
 *
405
 * Sanitizes a string for use as a path basename by replacing path separators
406
 * and other dangerous characters with underscores.
407
 *
408
 * Returns: (transfer full): a newly allocated sanitized string
409
 *
410
 * Since: 2.1.4
411
 **/
412
gchar *
413
fu_path_sanitize_basename(const gchar *str)
414
0
{
415
0
  g_autoptr(GString) result = g_string_new(str);
416
417
0
  g_return_val_if_fail(str != NULL, NULL);
418
419
  /* replace dangerous characters */
420
0
  g_string_replace(result, "/", "_", 0);
421
0
  g_string_replace(result, "\\", "_", 0);
422
0
  g_string_replace(result, "..", "_", 0);
423
424
  /* detect hidden files */
425
0
  if (g_str_has_prefix(result->str, "."))
426
0
    result->str[0] = '_';
427
428
0
  return g_string_free(g_steal_pointer(&result), FALSE);
429
0
}