summaryrefslogtreecommitdiffstats
path: root/src/shared/vpick.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/vpick.c')
-rw-r--r--src/shared/vpick.c699
1 files changed, 699 insertions, 0 deletions
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
new file mode 100644
index 0000000..4720e74
--- /dev/null
+++ b/src/shared/vpick.c
@@ -0,0 +1,699 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+
+#include "architecture.h"
+#include "chase.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "recurse-dir.h"
+#include "vpick.h"
+
+void pick_result_done(PickResult *p) {
+ assert(p);
+
+ free(p->path);
+ safe_close(p->fd);
+ free(p->version);
+
+ *p = PICK_RESULT_NULL;
+}
+
+static int format_fname(
+ const PickFilter *filter,
+ PickFlags flags,
+ char **ret) {
+
+ _cleanup_free_ char *fn = NULL;
+ int r;
+
+ assert(filter);
+ assert(ret);
+
+ if (FLAGS_SET(flags, PICK_TRIES) || !filter->version) /* Underspecified? */
+ return -ENOEXEC;
+ if (strv_length(filter->suffix) > 1) /* suffix is not deterministic? */
+ return -ENOEXEC;
+
+ /* The format for names we match goes like this:
+ *
+ * <basename><suffix>
+ * or:
+ * <basename>_<version><suffix>
+ * or:
+ * <basename>_<version>_<architecture><suffix>
+ * or:
+ * <basename>_<architecture><suffix>
+ *
+ * (Note that basename can be empty, in which case the leading "_" is suppressed)
+ *
+ * Examples: foo.raw, foo_1.3-7.raw, foo_1.3-7_x86-64.raw, foo_x86-64.raw
+ *
+ * Why use "_" as separator here? Primarily because it is not used by Semver 2.0. In RPM it is used
+ * for "unsortable" versions, i.e. doesn't show up in "sortable" versions, which we matter for this
+ * usecase here. In Debian the underscore is not allowed (and it uses it itself for separating
+ * fields).
+ *
+ * This is very close to Debian's way to name packages, but allows arbitrary suffixes, and makes the
+ * architecture field redundant.
+ *
+ * Compare with RPM's "NEVRA" concept. Here we have "BVAS" (basename, version, architecture, suffix).
+ */
+
+ if (filter->basename) {
+ fn = strdup(filter->basename);
+ if (!fn)
+ return -ENOMEM;
+ }
+
+ if (filter->version) {
+ if (isempty(fn)) {
+ r = free_and_strdup(&fn, filter->version);
+ if (r < 0)
+ return r;
+ } else if (!strextend(&fn, "_", filter->version))
+ return -ENOMEM;
+ }
+
+ if (FLAGS_SET(flags, PICK_ARCHITECTURE) && filter->architecture >= 0) {
+ const char *as = ASSERT_PTR(architecture_to_string(filter->architecture));
+ if (isempty(fn)) {
+ r = free_and_strdup(&fn, as);
+ if (r < 0)
+ return r;
+ } else if (!strextend(&fn, "_", as))
+ return -ENOMEM;
+ }
+
+ if (!strv_isempty(filter->suffix))
+ if (!strextend(&fn, filter->suffix[0]))
+ return -ENOMEM;
+
+ if (!filename_is_valid(fn))
+ return -EINVAL;
+
+ *ret = TAKE_PTR(fn);
+ return 0;
+}
+
+static int errno_from_mode(uint32_t type_mask, mode_t found) {
+ /* Returns the most appropriate error code if we are lookging for an inode of type of those in the
+ * 'type_mask' but found 'found' instead.
+ *
+ * type_mask is a mask of 1U << DT_REG, 1U << DT_DIR, … flags, while found is a S_IFREG, S_IFDIR, …
+ * mode value. */
+
+ if (type_mask == 0) /* type doesn't matter */
+ return 0;
+
+ if (FLAGS_SET(type_mask, UINT32_C(1) << IFTODT(found)))
+ return 0;
+
+ if (type_mask == (UINT32_C(1) << DT_BLK))
+ return -ENOTBLK;
+ if (type_mask == (UINT32_C(1) << DT_DIR))
+ return -ENOTDIR;
+ if (type_mask == (UINT32_C(1) << DT_SOCK))
+ return -ENOTSOCK;
+
+ if (S_ISLNK(found))
+ return -ELOOP;
+ if (S_ISDIR(found))
+ return -EISDIR;
+
+ return -EBADF;
+}
+
+static int pin_choice(
+ const char *toplevel_path,
+ int toplevel_fd,
+ const char *inode_path,
+ int _inode_fd, /* we always take ownership of the fd, even on failure */
+ unsigned tries_left,
+ unsigned tries_done,
+ const PickFilter *filter,
+ PickFlags flags,
+ PickResult *ret) {
+
+ _cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
+ _cleanup_free_ char *resolved_path = NULL;
+ int r;
+
+ assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
+ assert(inode_path);
+ assert(filter);
+ assert(ret);
+
+ if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
+ r = chaseat(toplevel_fd,
+ inode_path,
+ CHASE_AT_RESOLVE_IN_ROOT,
+ FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : 0,
+ inode_fd < 0 ? &inode_fd : NULL);
+ if (r < 0)
+ return r;
+
+ if (resolved_path)
+ inode_path = resolved_path;
+ }
+
+ struct stat st;
+ if (fstat(inode_fd, &st) < 0)
+ return log_debug_errno(errno, "Failed to stat discovered inode '%s': %m", prefix_roota(toplevel_path, inode_path));
+
+ if (filter->type_mask != 0 &&
+ !FLAGS_SET(filter->type_mask, UINT32_C(1) << IFTODT(st.st_mode)))
+ return log_debug_errno(
+ SYNTHETIC_ERRNO(errno_from_mode(filter->type_mask, st.st_mode)),
+ "Inode '%s' has wrong type, found '%s'.",
+ prefix_roota(toplevel_path, inode_path),
+ inode_type_to_string(st.st_mode));
+
+ _cleanup_(pick_result_done) PickResult result = {
+ .fd = TAKE_FD(inode_fd),
+ .st = st,
+ .architecture = filter->architecture,
+ .tries_left = tries_left,
+ .tries_done = tries_done,
+ };
+
+ result.path = strdup(inode_path);
+ if (!result.path)
+ return log_oom_debug();
+
+ r = strdup_to(&result.version, filter->version);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PICK_RESULT(result);
+ return 1;
+}
+
+static int parse_tries(const char *s, unsigned *ret_tries_left, unsigned *ret_tries_done) {
+ unsigned left, done;
+ size_t n;
+
+ assert(s);
+ assert(ret_tries_left);
+ assert(ret_tries_done);
+
+ if (s[0] != '+')
+ goto nomatch;
+
+ s++;
+
+ n = strspn(s, DIGITS);
+ if (n == 0)
+ goto nomatch;
+
+ if (s[n] == 0) {
+ if (safe_atou(s, &left) < 0)
+ goto nomatch;
+
+ done = 0;
+ } else if (s[n] == '-') {
+ _cleanup_free_ char *c = NULL;
+
+ c = strndup(s, n);
+ if (!c)
+ return -ENOMEM;
+
+ if (safe_atou(c, &left) < 0)
+ goto nomatch;
+
+ s += n + 1;
+
+ if (!in_charset(s, DIGITS))
+ goto nomatch;
+
+ if (safe_atou(s, &done) < 0)
+ goto nomatch;
+ } else
+ goto nomatch;
+
+ *ret_tries_left = left;
+ *ret_tries_done = done;
+ return 1;
+
+nomatch:
+ *ret_tries_left = *ret_tries_done = UINT_MAX;
+ return 0;
+}
+
+static int make_choice(
+ const char *toplevel_path,
+ int toplevel_fd,
+ const char *inode_path,
+ int _inode_fd, /* we always take ownership of the fd, even on failure */
+ const PickFilter *filter,
+ PickFlags flags,
+ PickResult *ret) {
+
+ static const Architecture local_architectures[] = {
+ /* In order of preference */
+ native_architecture(),
+#ifdef ARCHITECTURE_SECONDARY
+ ARCHITECTURE_SECONDARY,
+#endif
+ _ARCHITECTURE_INVALID, /* accept any arch, as last resort */
+ };
+
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ _cleanup_free_ char *best_version = NULL, *best_filename = NULL, *p = NULL, *j = NULL;
+ _cleanup_close_ int dir_fd = -EBADF, object_fd = -EBADF, inode_fd = TAKE_FD(_inode_fd);
+ const Architecture *architectures;
+ unsigned best_tries_left = UINT_MAX, best_tries_done = UINT_MAX;
+ size_t n_architectures, best_architecture_index = SIZE_MAX;
+ int r;
+
+ assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
+ assert(inode_path);
+ assert(filter);
+ assert(ret);
+
+ if (inode_fd < 0) {
+ r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd);
+ if (r < 0)
+ return r;
+ }
+
+ /* Maybe the filter is fully specified? Then we can generate the file name directly */
+ r = format_fname(filter, flags, &j);
+ if (r >= 0) {
+ _cleanup_free_ char *object_path = NULL;
+
+ /* Yay! This worked! */
+ p = path_join(inode_path, j);
+ if (!p)
+ return log_oom_debug();
+
+ r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd);
+ if (r == -ENOENT) {
+ *ret = PICK_RESULT_NULL;
+ return 0;
+ }
+ if (r < 0)
+ return log_debug_errno(r, "Failed to open '%s': %m", prefix_roota(toplevel_path, p));
+
+ return pin_choice(
+ toplevel_path,
+ toplevel_fd,
+ FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
+ TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
+ /* tries_left= */ UINT_MAX,
+ /* tries_done= */ UINT_MAX,
+ filter,
+ flags & ~PICK_RESOLVE,
+ ret);
+
+ } else if (r != -ENOEXEC)
+ return log_debug_errno(r, "Failed to format file name: %m");
+
+ /* Underspecified, so we do our enumeration dance */
+
+ /* Convert O_PATH to a regular directory fd */
+ dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (dir_fd < 0)
+ return log_debug_errno(dir_fd, "Failed to reopen '%s' as directory: %m", prefix_roota(toplevel_path, inode_path));
+
+ r = readdir_all(dir_fd, 0, &de);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read directory '%s': %m", prefix_roota(toplevel_path, inode_path));
+
+ if (filter->architecture < 0) {
+ architectures = local_architectures;
+ n_architectures = ELEMENTSOF(local_architectures);
+ } else {
+ architectures = &filter->architecture;
+ n_architectures = 1;
+ }
+
+ FOREACH_ARRAY(entry, de->entries, de->n_entries) {
+ unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX;
+ _cleanup_free_ char *dname = NULL;
+ size_t found_architecture_index = SIZE_MAX;
+ const char *e;
+
+ dname = strdup((*entry)->d_name);
+ if (!dname)
+ return log_oom_debug();
+
+ if (!isempty(filter->basename)) {
+ e = startswith(dname, filter->basename);
+ if (!e)
+ continue;
+
+ if (e[0] != '_')
+ continue;
+
+ e++;
+ } else
+ e = dname;
+
+ if (!strv_isempty(filter->suffix)) {
+ char *sfx = endswith_strv(e, filter->suffix);
+ if (!sfx)
+ continue;
+
+ *sfx = 0;
+ }
+
+ if (FLAGS_SET(flags, PICK_TRIES)) {
+ char *plus = strrchr(e, '+');
+ if (plus) {
+ r = parse_tries(plus, &found_tries_left, &found_tries_done);
+ if (r < 0)
+ return r;
+ if (r > 0) /* Found and parsed, now chop off */
+ *plus = 0;
+ }
+ }
+
+ if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
+ char *underscore = strrchr(e, '_');
+ Architecture a;
+
+ a = underscore ? architecture_from_string(underscore + 1) : _ARCHITECTURE_INVALID;
+
+ for (size_t i = 0; i < n_architectures; i++)
+ if (architectures[i] == a) {
+ found_architecture_index = i;
+ break;
+ }
+
+ if (found_architecture_index == SIZE_MAX) { /* No matching arch found */
+ log_debug("Found entry with architecture '%s' which is not what we are looking for, ignoring entry.", a < 0 ? "any" : architecture_to_string(a));
+ continue;
+ }
+
+ /* Chop off architecture from string */
+ if (underscore)
+ *underscore = 0;
+ }
+
+ if (!version_is_valid(e)) {
+ log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name);
+ continue;
+ }
+
+ if (filter->version && !streq(filter->version, e)) {
+ log_debug("Found entry with version string '%s', but was looking for '%s', ignoring entry.", e, filter->version);
+ continue;
+ }
+
+ if (best_filename) { /* Already found one matching entry? Then figure out the better one */
+ int d = 0;
+
+ /* First, prefer entries with tries left over those without */
+ if (FLAGS_SET(flags, PICK_TRIES))
+ d = CMP(found_tries_left != 0, best_tries_left != 0);
+
+ /* Second, prefer newer versions */
+ if (d == 0)
+ d = strverscmp_improved(e, best_version);
+
+ /* Third, prefer native architectures over secondary architectures */
+ if (d == 0 &&
+ FLAGS_SET(flags, PICK_ARCHITECTURE) &&
+ found_architecture_index != SIZE_MAX && best_architecture_index != SIZE_MAX)
+ d = -CMP(found_architecture_index, best_architecture_index);
+
+ /* Fourth, prefer entries with more tries left */
+ if (FLAGS_SET(flags, PICK_TRIES)) {
+ if (d == 0)
+ d = CMP(found_tries_left, best_tries_left);
+
+ /* Fifth, prefer entries with fewer attempts done so far */
+ if (d == 0)
+ d = -CMP(found_tries_done, best_tries_done);
+ }
+
+ /* Last, just compare the filenames as strings */
+ if (d == 0)
+ d = strcmp((*entry)->d_name, best_filename);
+
+ if (d < 0) {
+ log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", (*entry)->d_name, best_filename);
+ continue;
+ }
+ }
+
+ r = free_and_strdup_warn(&best_version, e);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup_warn(&best_filename, (*entry)->d_name);
+ if (r < 0)
+ return r;
+
+ best_architecture_index = found_architecture_index;
+ best_tries_left = found_tries_left;
+ best_tries_done = found_tries_done;
+ }
+
+ if (!best_filename) { /* Everything was good, but we didn't find any suitable entry */
+ *ret = PICK_RESULT_NULL;
+ return 0;
+ }
+
+ p = path_join(inode_path, best_filename);
+ if (!p)
+ return log_oom_debug();
+
+ object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH);
+ if (object_fd < 0)
+ return log_debug_errno(errno, "Failed to open '%s': %m", prefix_roota(toplevel_path, p));
+
+ return pin_choice(
+ toplevel_path,
+ toplevel_fd,
+ p,
+ TAKE_FD(object_fd),
+ best_tries_left,
+ best_tries_done,
+ &(const PickFilter) {
+ .type_mask = filter->type_mask,
+ .basename = filter->basename,
+ .version = empty_to_null(best_version),
+ .architecture = best_architecture_index != SIZE_MAX ? architectures[best_architecture_index] : _ARCHITECTURE_INVALID,
+ .suffix = filter->suffix,
+ },
+ flags,
+ ret);
+}
+
+int path_pick(
+ const char *toplevel_path,
+ int toplevel_fd,
+ const char *path,
+ const PickFilter *filter,
+ PickFlags flags,
+ PickResult *ret) {
+
+ _cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL;
+ char * const *filter_suffix_strv = NULL;
+ const char *filter_suffix = NULL, *enumeration_path;
+ uint32_t filter_type_mask;
+ int r;
+
+ assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
+ assert(path);
+ assert(filter);
+ assert(ret);
+
+ /* Given a path, resolve .v/ subdir logic (if used!), and returns the choice made. This supports
+ * three ways to be called:
+ *
+ * • with a path referring a directory of any name, and filter→basename *explicitly* specified, in
+ * which case we'll use a pattern "<filter→basename>_*<filter→suffix>" on the directory's files.
+ *
+ * • with no filter→basename explicitly specified and a path referring to a directory named in format
+ * "<somestring><filter→suffix>.v" . In this case the filter basename to search for inside the dir
+ * is derived from the directory name. Example: "/foo/bar/baz.suffix.v" → we'll search for
+ * "/foo/bar/baz.suffix.v/baz_*.suffix".
+ *
+ * • with a path whose penultimate component ends in ".v/". In this case the final component of the
+ * path refers to the pattern. Example: "/foo/bar/baz.v/waldo__.suffix" → we'll search for
+ * "/foo/bar/baz.v/waldo_*.suffix".
+ */
+
+ /* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
+ if (filter->basename)
+ return make_choice(
+ toplevel_path,
+ toplevel_fd,
+ path,
+ /* inode_fd= */ -EBADF,
+ filter,
+ flags,
+ ret);
+
+ r = path_extract_filename(path, &fname);
+ if (r < 0) {
+ if (r != -EADDRNOTAVAIL) /* root dir or "." */
+ return r;
+
+ /* If there's no path element we can derive a pattern off, the don't */
+ goto bypass;
+ }
+
+ /* Remember if the path ends in a dash suffix */
+ bool slash_suffix = r == O_DIRECTORY;
+
+ const char *e = endswith(fname, ".v");
+ if (e) {
+ /* So a path in the form /foo/bar/baz.v is specified. In this case our search basename is
+ * "baz", possibly with a suffix chopped off if there's one specified. */
+ filter_bname = strndup(fname, e - fname);
+ if (!filter_bname)
+ return -ENOMEM;
+
+ /* Chop off suffix, if specified */
+ char *f = endswith_strv(filter_bname, filter->suffix);
+ if (f)
+ *f = 0;
+
+ filter_suffix_strv = filter->suffix;
+ filter_type_mask = filter->type_mask;
+
+ enumeration_path = path;
+ } else {
+ /* The path does not end in '.v', hence see if the last element is a pattern. */
+
+ char *wildcard = strrstr(fname, "___");
+ if (!wildcard)
+ goto bypass; /* Not a pattern, then bypass */
+
+ /* We found the '___' wildcard, hence everything after it is our filter suffix, and
+ * everything before is our filter basename */
+ *wildcard = 0;
+ filter_suffix = empty_to_null(wildcard + 3);
+
+ filter_bname = TAKE_PTR(fname);
+
+ r = path_extract_directory(path, &dir);
+ if (r < 0) {
+ if (!IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */
+ return r;
+
+ goto bypass; /* No dir extractable, can't check if parent ends in ".v" */
+ }
+
+ r = path_extract_filename(dir, &parent);
+ if (r < 0) {
+ if (r != -EADDRNOTAVAIL) /* root dir or "." */
+ return r;
+
+ goto bypass; /* Cannot extract fname from parent dir, can't check if it ends in ".v" */
+ }
+
+ e = endswith(parent, ".v");
+ if (!e)
+ goto bypass; /* Doesn't end in ".v", shortcut */
+
+ filter_type_mask = filter->type_mask;
+ if (slash_suffix) {
+ /* If the pattern is suffixed by a / then we are looking for directories apparently. */
+ if (filter_type_mask != 0 && !FLAGS_SET(filter_type_mask, UINT32_C(1) << DT_DIR))
+ return log_debug_errno(SYNTHETIC_ERRNO(errno_from_mode(filter_type_mask, S_IFDIR)),
+ "Specified pattern ends in '/', but not looking for directories, refusing.");
+ filter_type_mask = UINT32_C(1) << DT_DIR;
+ }
+
+ enumeration_path = dir;
+ }
+
+ return make_choice(
+ toplevel_path,
+ toplevel_fd,
+ enumeration_path,
+ /* inode_fd= */ -EBADF,
+ &(const PickFilter) {
+ .type_mask = filter_type_mask,
+ .basename = filter_bname,
+ .version = filter->version,
+ .architecture = filter->architecture,
+ .suffix = filter_suffix_strv ?: STRV_MAKE(filter_suffix),
+ },
+ flags,
+ ret);
+
+bypass:
+ /* Don't make any choice, but just use the passed path literally */
+ return pin_choice(
+ toplevel_path,
+ toplevel_fd,
+ path,
+ /* inode_fd= */ -EBADF,
+ /* tries_left= */ UINT_MAX,
+ /* tries_done= */ UINT_MAX,
+ filter,
+ flags,
+ ret);
+}
+
+int path_pick_update_warn(
+ char **path,
+ const PickFilter *filter,
+ PickFlags flags,
+ PickResult *ret_result) {
+
+ _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
+ int r;
+
+ assert(path);
+ assert(*path);
+ assert(filter);
+
+ /* This updates the first argument if needed! */
+
+ r = path_pick(/* toplevel_path= */ NULL,
+ /* toplevel_fd= */ AT_FDCWD,
+ *path,
+ filter,
+ flags,
+ &result);
+ if (r == -ENOENT) {
+ log_debug("Path '%s' doesn't exist, leaving as is.", *path);
+
+ if (ret_result)
+ *ret_result = PICK_RESULT_NULL;
+ return 0;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to pick version on path '%s': %m", *path);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entries in versioned directory '%s' found.", *path);
+
+ log_debug("Resolved versioned directory pattern '%s' to file '%s' as version '%s'.", result.path, *path, strna(result.version));
+
+ if (ret_result) {
+ r = free_and_strdup_warn(path, result.path);
+ if (r < 0)
+ return r;
+
+ *ret_result = TAKE_PICK_RESULT(result);
+ } else
+ free_and_replace(*path, result.path);
+
+ return 1;
+}
+
+const PickFilter pick_filter_image_raw = {
+ .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = STRV_MAKE(".raw"),
+};
+
+const PickFilter pick_filter_image_dir = {
+ .type_mask = UINT32_C(1) << DT_DIR,
+ .architecture = _ARCHITECTURE_INVALID,
+};
+
+const PickFilter pick_filter_image_any = {
+ .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) | (UINT32_C(1) << DT_DIR),
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = STRV_MAKE(".raw", ""),
+};