]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/vpick.c
shared/vpick: add missing condition on ret_result
[thirdparty/systemd.git] / src / shared / vpick.c
CommitLineData
76511c1b
LP
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include <sys/stat.h>
4
5#include "architecture.h"
6#include "chase.h"
7#include "fd-util.h"
8#include "fs-util.h"
9#include "parse-util.h"
10#include "path-util.h"
11#include "recurse-dir.h"
12#include "vpick.h"
13
14void pick_result_done(PickResult *p) {
15 assert(p);
16
17 free(p->path);
18 safe_close(p->fd);
19 free(p->version);
20
21 *p = PICK_RESULT_NULL;
22}
23
24static int format_fname(
25 const PickFilter *filter,
26 PickFlags flags,
27 char **ret) {
28
29 _cleanup_free_ char *fn = NULL;
30 int r;
31
32 assert(filter);
33 assert(ret);
34
35 if (FLAGS_SET(flags, PICK_TRIES) || !filter->version) /* Underspecified? */
36 return -ENOEXEC;
37
38 /* The format for names we match goes like this:
39 *
40 * <basename><suffix>
41 * or:
42 * <basename>_<version><suffix>
43 * or:
44 * <basename>_<version>_<architecture><suffix>
45 * or:
46 * <basename>_<architecture><suffix>
47 *
48 * (Note that basename can be empty, in which case the leading "_" is suppressed)
49 *
50 * Examples: foo.raw, foo_1.3-7.raw, foo_1.3-7_x86-64.raw, foo_x86-64.raw
51 *
52 * Why use "_" as separator here? Primarily because it is not used by Semver 2.0. In RPM it is used
53 * for "unsortable" versions, i.e. doesn't show up in "sortable" versions, which we matter for this
54 * usecase here. In Debian the underscore is not allowed (and it uses it itself for separating
55 * fields).
56 *
57 * This is very close to Debian's way to name packages, but allows arbitrary suffixes, and makes the
58 * architecture field redundant.
59 *
60 * Compare with RPM's "NEVRA" concept. Here we have "BVAS" (basename, version, architecture, suffix).
61 */
62
63 if (filter->basename) {
64 fn = strdup(filter->basename);
65 if (!fn)
66 return -ENOMEM;
67 }
68
69 if (filter->version) {
70 if (isempty(fn)) {
71 r = free_and_strdup(&fn, filter->version);
72 if (r < 0)
73 return r;
74 } else if (!strextend(&fn, "_", filter->version))
75 return -ENOMEM;
76 }
77
78 if (FLAGS_SET(flags, PICK_ARCHITECTURE) && filter->architecture >= 0) {
79 const char *as = ASSERT_PTR(architecture_to_string(filter->architecture));
80 if (isempty(fn)) {
81 r = free_and_strdup(&fn, as);
82 if (r < 0)
83 return r;
84 } else if (!strextend(&fn, "_", as))
85 return -ENOMEM;
86 }
87
88 if (filter->suffix && !strextend(&fn, filter->suffix))
89 return -ENOMEM;
90
91 if (!filename_is_valid(fn))
92 return -EINVAL;
93
94 *ret = TAKE_PTR(fn);
95 return 0;
96}
97
98static int errno_from_mode(uint32_t type_mask, mode_t found) {
99 /* Returns the most appropriate error code if we are lookging for an inode of type of those in the
100 * 'type_mask' but found 'found' instead.
101 *
102 * type_mask is a mask of 1U << DT_REG, 1U << DT_DIR, … flags, while found is a S_IFREG, S_IFDIR, …
103 * mode value. */
104
105 if (type_mask == 0) /* type doesn't matter */
106 return 0;
107
108 if (FLAGS_SET(type_mask, UINT32_C(1) << IFTODT(found)))
109 return 0;
110
111 if (type_mask == (UINT32_C(1) << DT_BLK))
112 return -ENOTBLK;
113 if (type_mask == (UINT32_C(1) << DT_DIR))
114 return -ENOTDIR;
115 if (type_mask == (UINT32_C(1) << DT_SOCK))
116 return -ENOTSOCK;
117
118 if (S_ISLNK(found))
119 return -ELOOP;
120 if (S_ISDIR(found))
121 return -EISDIR;
122
123 return -EBADF;
124}
125
126static int pin_choice(
127 const char *toplevel_path,
128 int toplevel_fd,
129 const char *inode_path,
130 int _inode_fd, /* we always take ownership of the fd, even on failure */
131 unsigned tries_left,
132 unsigned tries_done,
133 const PickFilter *filter,
134 PickFlags flags,
135 PickResult *ret) {
136
137 _cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
138 _cleanup_free_ char *resolved_path = NULL;
139 int r;
140
141 assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
142 assert(inode_path);
143 assert(filter);
6d8690ec 144 assert(ret);
76511c1b 145
76511c1b
LP
146 if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
147 r = chaseat(toplevel_fd,
148 inode_path,
149 CHASE_AT_RESOLVE_IN_ROOT,
150 FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : 0,
151 inode_fd < 0 ? &inode_fd : NULL);
152 if (r < 0)
153 return r;
154
155 if (resolved_path)
156 inode_path = resolved_path;
157 }
158
159 struct stat st;
160 if (fstat(inode_fd, &st) < 0)
d049bffc 161 return log_debug_errno(errno, "Failed to stat discovered inode '%s': %m", prefix_roota(toplevel_path, inode_path));
76511c1b
LP
162
163 if (filter->type_mask != 0 &&
164 !FLAGS_SET(filter->type_mask, UINT32_C(1) << IFTODT(st.st_mode)))
165 return log_debug_errno(
166 SYNTHETIC_ERRNO(errno_from_mode(filter->type_mask, st.st_mode)),
d049bffc
FS
167 "Inode '%s' has wrong type, found '%s'.",
168 prefix_roota(toplevel_path, inode_path),
76511c1b
LP
169 inode_type_to_string(st.st_mode));
170
171 _cleanup_(pick_result_done) PickResult result = {
172 .fd = TAKE_FD(inode_fd),
173 .st = st,
174 .architecture = filter->architecture,
175 .tries_left = tries_left,
176 .tries_done = tries_done,
177 };
178
179 result.path = strdup(inode_path);
180 if (!result.path)
181 return log_oom_debug();
182
183 if (filter->version) {
184 result.version = strdup(filter->version);
185 if (!result.version)
186 return log_oom_debug();
187 }
188
189 *ret = TAKE_PICK_RESULT(result);
190 return 1;
191}
192
193static int parse_tries(const char *s, unsigned *ret_tries_left, unsigned *ret_tries_done) {
194 unsigned left, done;
195 size_t n;
196
197 assert(s);
198 assert(ret_tries_left);
199 assert(ret_tries_done);
200
201 if (s[0] != '+')
202 goto nomatch;
203
204 s++;
205
206 n = strspn(s, DIGITS);
207 if (n == 0)
208 goto nomatch;
209
210 if (s[n] == 0) {
211 if (safe_atou(s, &left) < 0)
212 goto nomatch;
213
214 done = 0;
215 } else if (s[n] == '-') {
216 _cleanup_free_ char *c = NULL;
217
218 c = strndup(s, n);
219 if (!c)
220 return -ENOMEM;
221
222 if (safe_atou(c, &left) < 0)
223 goto nomatch;
224
225 s += n + 1;
226
227 if (!in_charset(s, DIGITS))
228 goto nomatch;
229
230 if (safe_atou(s, &done) < 0)
231 goto nomatch;
232 } else
233 goto nomatch;
234
235 *ret_tries_left = left;
236 *ret_tries_done = done;
237 return 1;
238
239nomatch:
240 *ret_tries_left = *ret_tries_done = UINT_MAX;
241 return 0;
242}
243
244static int make_choice(
245 const char *toplevel_path,
246 int toplevel_fd,
247 const char *inode_path,
248 int _inode_fd, /* we always take ownership of the fd, even on failure */
249 const PickFilter *filter,
250 PickFlags flags,
251 PickResult *ret) {
252
253 static const Architecture local_architectures[] = {
254 /* In order of preference */
255 native_architecture(),
256#ifdef ARCHITECTURE_SECONDARY
257 ARCHITECTURE_SECONDARY,
258#endif
259 _ARCHITECTURE_INVALID, /* accept any arch, as last resort */
260 };
261
262 _cleanup_free_ DirectoryEntries *de = NULL;
263 _cleanup_free_ char *best_version = NULL, *best_filename = NULL, *p = NULL, *j = NULL;
264 _cleanup_close_ int dir_fd = -EBADF, object_fd = -EBADF, inode_fd = TAKE_FD(_inode_fd);
265 const Architecture *architectures;
266 unsigned best_tries_left = UINT_MAX, best_tries_done = UINT_MAX;
267 size_t n_architectures, best_architecture_index = SIZE_MAX;
268 int r;
269
270 assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
271 assert(inode_path);
272 assert(filter);
6d8690ec 273 assert(ret);
76511c1b 274
76511c1b
LP
275 if (inode_fd < 0) {
276 r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd);
277 if (r < 0)
278 return r;
279 }
280
281 /* Maybe the filter is fully specified? Then we can generate the file name directly */
282 r = format_fname(filter, flags, &j);
283 if (r >= 0) {
284 _cleanup_free_ char *object_path = NULL;
285
286 /* Yay! This worked! */
287 p = path_join(inode_path, j);
288 if (!p)
289 return log_oom_debug();
290
291 r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd);
292 if (r < 0) {
293 if (r != -ENOENT)
d049bffc 294 return log_debug_errno(r, "Failed to open '%s': %m", prefix_roota(toplevel_path, p));
76511c1b
LP
295
296 *ret = PICK_RESULT_NULL;
297 return 0;
298 }
299
300 return pin_choice(
301 toplevel_path,
302 toplevel_fd,
303 FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
304 TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
305 /* tries_left= */ UINT_MAX,
306 /* tries_done= */ UINT_MAX,
307 filter,
308 flags & ~PICK_RESOLVE,
309 ret);
310
311 } else if (r != -ENOEXEC)
312 return log_debug_errno(r, "Failed to format file name: %m");
313
314 /* Underspecified, so we do our enumeration dance */
315
316 /* Convert O_PATH to a regular directory fd */
317 dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
318 if (dir_fd < 0)
d049bffc 319 return log_debug_errno(dir_fd, "Failed to reopen '%s' as directory: %m", prefix_roota(toplevel_path, inode_path));
76511c1b
LP
320
321 r = readdir_all(dir_fd, 0, &de);
322 if (r < 0)
d049bffc 323 return log_debug_errno(r, "Failed to read directory '%s': %m", prefix_roota(toplevel_path, inode_path));
76511c1b
LP
324
325 if (filter->architecture < 0) {
326 architectures = local_architectures;
327 n_architectures = ELEMENTSOF(local_architectures);
328 } else {
329 architectures = &filter->architecture;
330 n_architectures = 1;
331 }
332
333 FOREACH_ARRAY(entry, de->entries, de->n_entries) {
334 unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX;
92587847 335 _cleanup_free_ char *dname = NULL;
76511c1b
LP
336 size_t found_architecture_index = SIZE_MAX;
337 const char *e;
338
92587847
FS
339 dname = strdup((*entry)->d_name);
340 if (!dname)
341 return log_oom_debug();
342
76511c1b 343 if (!isempty(filter->basename)) {
92587847 344 e = startswith(dname, filter->basename);
76511c1b
LP
345 if (!e)
346 continue;
347
348 if (e[0] != '_')
349 continue;
350
351 e++;
352 } else
92587847 353 e = dname;
76511c1b
LP
354
355 if (!isempty(filter->suffix)) {
92587847 356 char *sfx = endswith(e, filter->suffix);
76511c1b
LP
357 if (!sfx)
358 continue;
359
92587847 360 *sfx = 0;
76511c1b
LP
361 }
362
363 if (FLAGS_SET(flags, PICK_TRIES)) {
364 char *plus = strrchr(e, '+');
365 if (plus) {
366 r = parse_tries(plus, &found_tries_left, &found_tries_done);
367 if (r < 0)
368 return r;
369 if (r > 0) /* Found and parsed, now chop off */
370 *plus = 0;
371 }
372 }
373
374 if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
375 char *underscore = strrchr(e, '_');
376 Architecture a;
377
378 a = underscore ? architecture_from_string(underscore + 1) : _ARCHITECTURE_INVALID;
379
380 for (size_t i = 0; i < n_architectures; i++)
381 if (architectures[i] == a) {
382 found_architecture_index = i;
383 break;
384 }
385
386 if (found_architecture_index == SIZE_MAX) { /* No matching arch found */
387 log_debug("Found entry with architecture '%s' which is not what we are looking for, ignoring entry.", a < 0 ? "any" : architecture_to_string(a));
388 continue;
389 }
390
391 /* Chop off architecture from string */
392 if (underscore)
393 *underscore = 0;
394 }
395
396 if (!version_is_valid(e)) {
397 log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name);
398 continue;
399 }
400
401 if (filter->version && !streq(filter->version, e)) {
402 log_debug("Found entry with version string '%s', but was looking for '%s', ignoring entry.", e, filter->version);
403 continue;
404 }
405
406 if (best_filename) { /* Already found one matching entry? Then figure out the better one */
407 int d = 0;
408
409 /* First, prefer entries with tries left over those without */
410 if (FLAGS_SET(flags, PICK_TRIES))
411 d = CMP(found_tries_left != 0, best_tries_left != 0);
412
413 /* Second, prefer newer versions */
414 if (d == 0)
415 d = strverscmp_improved(e, best_version);
416
417 /* Third, prefer native architectures over secondary architectures */
418 if (d == 0 &&
419 FLAGS_SET(flags, PICK_ARCHITECTURE) &&
420 found_architecture_index != SIZE_MAX && best_architecture_index != SIZE_MAX)
421 d = -CMP(found_architecture_index, best_architecture_index);
422
423 /* Fourth, prefer entries with more tries left */
424 if (FLAGS_SET(flags, PICK_TRIES)) {
425 if (d == 0)
426 d = CMP(found_tries_left, best_tries_left);
427
428 /* Fifth, prefer entries with fewer attempts done so far */
429 if (d == 0)
430 d = -CMP(found_tries_done, best_tries_done);
431 }
432
433 /* Last, just compare the filenames as strings */
434 if (d == 0)
435 d = strcmp((*entry)->d_name, best_filename);
436
437 if (d < 0) {
438 log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", (*entry)->d_name, best_filename);
439 continue;
440 }
441 }
442
443 r = free_and_strdup_warn(&best_version, e);
444 if (r < 0)
445 return r;
446
447 r = free_and_strdup_warn(&best_filename, (*entry)->d_name);
448 if (r < 0)
449 return r;
450
451 best_architecture_index = found_architecture_index;
452 best_tries_left = found_tries_left;
453 best_tries_done = found_tries_done;
454 }
455
456 if (!best_filename) { /* Everything was good, but we didn't find any suitable entry */
457 *ret = PICK_RESULT_NULL;
458 return 0;
459 }
460
461 p = path_join(inode_path, best_filename);
462 if (!p)
463 return log_oom_debug();
464
465 object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH);
466 if (object_fd < 0)
d049bffc 467 return log_debug_errno(errno, "Failed to open '%s': %m", prefix_roota(toplevel_path, p));
76511c1b
LP
468
469 return pin_choice(
470 toplevel_path,
471 toplevel_fd,
472 p,
473 TAKE_FD(object_fd),
474 best_tries_left,
475 best_tries_done,
476 &(const PickFilter) {
477 .type_mask = filter->type_mask,
478 .basename = filter->basename,
479 .version = empty_to_null(best_version),
480 .architecture = best_architecture_index != SIZE_MAX ? architectures[best_architecture_index] : _ARCHITECTURE_INVALID,
481 .suffix = filter->suffix,
482 },
483 flags,
484 ret);
485}
486
6d8690ec
MY
487int path_pick(
488 const char *toplevel_path,
489 int toplevel_fd,
490 const char *path,
491 const PickFilter *filter,
492 PickFlags flags,
493 PickResult *ret) {
76511c1b
LP
494
495 _cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL;
496 const char *filter_suffix, *enumeration_path;
497 uint32_t filter_type_mask;
498 int r;
499
500 assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
501 assert(path);
6d8690ec
MY
502 assert(filter);
503 assert(ret);
76511c1b 504
76511c1b
LP
505 /* Given a path, resolve .v/ subdir logic (if used!), and returns the choice made. This supports
506 * three ways to be called:
507 *
508 * • with a path referring a directory of any name, and filter→basename *explicitly* specified, in
509 * which case we'll use a pattern "<filter→basename>_*<filter→suffix>" on the directory's files.
510 *
511 * • with no filter→basename explicitly specified and a path referring to a directory named in format
512 * "<somestring><filter→suffix>.v" . In this case the filter basename to search for inside the dir
513 * is derived from the directory name. Example: "/foo/bar/baz.suffix.v" → we'll search for
514 * "/foo/bar/baz.suffix.v/baz_*.suffix".
515 *
516 * • with a path whose penultimate component ends in ".v/". In this case the final component of the
517 * path refers to the pattern. Example: "/foo/bar/baz.v/waldo__.suffix" → we'll search for
518 * "/foo/bar/baz.v/waldo_*.suffix".
519 */
520
521 /* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
522 if (filter->basename)
523 return make_choice(
524 toplevel_path,
525 toplevel_fd,
526 path,
527 /* inode_fd= */ -EBADF,
528 filter,
529 flags,
530 ret);
531
532 r = path_extract_filename(path, &fname);
533 if (r < 0) {
534 if (r != -EADDRNOTAVAIL) /* root dir or "." */
535 return r;
536
537 /* If there's no path element we can derive a pattern off, the don't */
538 goto bypass;
539 }
540
541 /* Remember if the path ends in a dash suffix */
542 bool slash_suffix = r == O_DIRECTORY;
543
544 const char *e = endswith(fname, ".v");
545 if (e) {
546 /* So a path in the form /foo/bar/baz.v is specified. In this case our search basename is
547 * "baz", possibly with a suffix chopped off if there's one specified. */
548 filter_bname = strndup(fname, e - fname);
549 if (!filter_bname)
550 return -ENOMEM;
551
552 if (filter->suffix) {
553 /* Chop off suffix, if specified */
554 char *f = endswith(filter_bname, filter->suffix);
555 if (f)
556 *f = 0;
557 }
558
559 filter_suffix = filter->suffix;
560 filter_type_mask = filter->type_mask;
561
562 enumeration_path = path;
563 } else {
564 /* The path does not end in '.v', hence see if the last element is a pattern. */
565
566 char *wildcard = strrstr(fname, "___");
567 if (!wildcard)
568 goto bypass; /* Not a pattern, then bypass */
569
20a0aeb0
YW
570 /* We found the '___' wildcard, hence everything after it is our filter suffix, and
571 * everything before is our filter basename */
76511c1b
LP
572 *wildcard = 0;
573 filter_suffix = empty_to_null(wildcard + 3);
574
575 filter_bname = TAKE_PTR(fname);
576
577 r = path_extract_directory(path, &dir);
578 if (r < 0) {
579 if (!IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */
580 return r;
581
582 goto bypass; /* No dir extractable, can't check if parent ends in ".v" */
583 }
584
585 r = path_extract_filename(dir, &parent);
586 if (r < 0) {
587 if (r != -EADDRNOTAVAIL) /* root dir or "." */
588 return r;
589
590 goto bypass; /* Cannot extract fname from parent dir, can't check if it ends in ".v" */
591 }
592
593 e = endswith(parent, ".v");
594 if (!e)
595 goto bypass; /* Doesn't end in ".v", shortcut */
596
597 filter_type_mask = filter->type_mask;
598 if (slash_suffix) {
599 /* If the pattern is suffixed by a / then we are looking for directories apparently. */
600 if (filter_type_mask != 0 && !FLAGS_SET(filter_type_mask, UINT32_C(1) << DT_DIR))
601 return log_debug_errno(SYNTHETIC_ERRNO(errno_from_mode(filter_type_mask, S_IFDIR)),
602 "Specified pattern ends in '/', but not looking for directories, refusing.");
603 filter_type_mask = UINT32_C(1) << DT_DIR;
604 }
605
606 enumeration_path = dir;
607 }
608
609 return make_choice(
610 toplevel_path,
611 toplevel_fd,
612 enumeration_path,
613 /* inode_fd= */ -EBADF,
614 &(const PickFilter) {
615 .type_mask = filter_type_mask,
616 .basename = filter_bname,
617 .version = filter->version,
618 .architecture = filter->architecture,
619 .suffix = filter_suffix,
620 },
621 flags,
622 ret);
623
624bypass:
625 /* Don't make any choice, but just use the passed path literally */
626 return pin_choice(
627 toplevel_path,
628 toplevel_fd,
629 path,
630 /* inode_fd= */ -EBADF,
631 /* tries_left= */ UINT_MAX,
632 /* tries_done= */ UINT_MAX,
633 filter,
634 flags,
635 ret);
636}
637
638int path_pick_update_warn(
639 char **path,
640 const PickFilter *filter,
641 PickFlags flags,
642 PickResult *ret_result) {
643
644 _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
645 int r;
646
647 assert(path);
648 assert(*path);
6d8690ec 649 assert(filter);
76511c1b
LP
650
651 /* This updates the first argument if needed! */
652
653 r = path_pick(/* toplevel_path= */ NULL,
654 /* toplevel_fd= */ AT_FDCWD,
655 *path,
656 filter,
657 flags,
658 &result);
659 if (r == -ENOENT) {
660 log_debug("Path '%s' doesn't exist, leaving as is.", *path);
c694419a
MY
661
662 if (ret_result)
663 *ret_result = PICK_RESULT_NULL;
76511c1b
LP
664 return 0;
665 }
666 if (r < 0)
667 return log_error_errno(r, "Failed to pick version on path '%s': %m", *path);
668 if (r == 0)
669 return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entries in versioned directory '%s' found.", *path);
670
671 log_debug("Resolved versioned directory pattern '%s' to file '%s' as version '%s'.", result.path, *path, strna(result.version));
672
673 if (ret_result) {
674 r = free_and_strdup_warn(path, result.path);
675 if (r < 0)
676 return r;
677
678 *ret_result = TAKE_PICK_RESULT(result);
679 } else
680 free_and_replace(*path, result.path);
681
682 return 1;
683}
684
685const PickFilter pick_filter_image_raw = {
686 .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
687 .architecture = _ARCHITECTURE_INVALID,
688 .suffix = ".raw",
689};
690
691const PickFilter pick_filter_image_dir = {
692 .type_mask = UINT32_C(1) << DT_DIR,
693 .architecture = _ARCHITECTURE_INVALID,
694};