]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared: add new "vpick" concept for ".v/" directories that contain versioned resources
authorLennart Poettering <lennart@poettering.net>
Fri, 3 Mar 2023 17:18:05 +0000 (18:18 +0100)
committerLennart Poettering <lennart@poettering.net>
Wed, 3 Jan 2024 17:38:46 +0000 (18:38 +0100)
This adds a new concept for handling paths. At appropriate places, if a
path such as /foo/bar/baz.v/ is specified, we'll
automatically enumerate all entries in /foo/bar/baz.v/baz* and then
do a version sort and pick the newest file.

A slightly more complex syntax is available, too:

/foo/bar/baz.v/quux___waldo

if that's used, then we'll look for all files matching
/foo/bar/baz.v/quux*waldo, and split out the middle, and version sort
it, and pick the nwest.

The ___ wildcard indicates both a version string, and if needed an
architecture ID, in case per-arch entries shall be supported.

This is a very simple way to maintain versioned resources in a dir, and
make systemd's components automatically pick the newest. Example:

    /srv/myimages.v/foobar_1.32.65_x86-64.raw
    /srv/myimages.v/foobar_1.33.45_x86-64.raw
    /srv/myimages.v/foobar_1.31.5_x86-64.raw
    /srv/myimages.v/foobar_1.31.5_arm64.raw

If now nspawn is invoked like this:

    systemd-nspawn --image=/srv/myimages.v/foobar___.raw

Then it will automatically pick
/srv/myimages.v/foobar_1.33.45_x86-64.raw as the version to boot on
x86-64, and /srv/myimages.v/foobar_1.31.5_arm64.raw on arm64.

This commit only adds the basic implementation for picking files from a
dir, but no hook-up anywhere.

src/shared/meson.build
src/shared/vpick.c [new file with mode: 0644]
src/shared/vpick.h [new file with mode: 0644]
src/test/meson.build
src/test/test-vpick.c [new file with mode: 0644]

index b2cee6fa2c869ac6d9f949e79fe09b5887e7fdd0..1b95430f8843f703696c3a488b520515fc89765d 100644 (file)
@@ -189,6 +189,7 @@ shared_sources = files(
         'verbs.c',
         'vlan-util.c',
         'volatile-util.c',
+        'vpick.c',
         'wall.c',
         'watchdog.c',
         'web-util.c',
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
new file mode 100644 (file)
index 0000000..4a4d1eb
--- /dev/null
@@ -0,0 +1,694 @@
+/* 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;
+
+        /* 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 (filter->suffix && !strextend(&fn, filter->suffix))
+                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);
+
+        toplevel_path = strempty(toplevel_path);
+
+        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/%s': %m", 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/%s' has wrong type, found '%s'.",
+                                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();
+
+        if (filter->version) {
+                result.version = strdup(filter->version);
+                if (!result.version)
+                        return log_oom_debug();
+        }
+
+        *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);
+
+        toplevel_path = strempty(toplevel_path);
+
+        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 < 0) {
+                        if (r != -ENOENT)
+                                return log_debug_errno(r, "Failed to open '%s/%s': %m", toplevel_path, p);
+
+                        *ret = PICK_RESULT_NULL;
+                        return 0;
+                }
+
+                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/%s' as directory: %m", toplevel_path, inode_path);
+
+        r = readdir_all(dir_fd, 0, &de);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to read directory '%s/%s': %m", 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 *chopped = NULL;
+                size_t found_architecture_index = SIZE_MAX;
+                const char *e;
+
+                if (!isempty(filter->basename)) {
+                        e = startswith((*entry)->d_name, filter->basename);
+                        if (!e)
+                                continue;
+
+                        if (e[0] != '_')
+                                continue;
+
+                        e++;
+                } else
+                        e = (*entry)->d_name;
+
+                if (!isempty(filter->suffix)) {
+                        const char *sfx;
+
+                        sfx = endswith(e, filter->suffix);
+                        if (!sfx)
+                                continue;
+
+                        chopped = strndup(e, sfx - e);
+                        if (!chopped)
+                                return log_oom_debug();
+
+                        e = chopped;
+                }
+
+                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/%s': %m", 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;
+        const char *filter_suffix, *enumeration_path;
+        uint32_t filter_type_mask;
+        int r;
+
+        assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
+        assert(path);
+
+        toplevel_path = strempty(toplevel_path);
+
+        /* 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;
+
+                if (filter->suffix) {
+                        /* Chop off suffix, if specified */
+                        char *f = endswith(filter_bname, filter->suffix);
+                        if (f)
+                                *f = 0;
+                }
+
+                filter_suffix = 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 evertyhing after it is our filter suffix, and
+                 * evertyhing 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,
+                        },
+                        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);
+
+        /* 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);
+                *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 = ".raw",
+};
+
+const PickFilter pick_filter_image_dir = {
+        .type_mask = UINT32_C(1) << DT_DIR,
+        .architecture = _ARCHITECTURE_INVALID,
+};
diff --git a/src/shared/vpick.h b/src/shared/vpick.h
new file mode 100644 (file)
index 0000000..0e0d41e
--- /dev/null
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "architecture.h"
+
+typedef enum PickFlags {
+        PICK_ARCHITECTURE = 1 << 0,   /* Look for an architecture suffix */
+        PICK_TRIES        = 1 << 1,   /* Look for tries left/tries done counters */
+        PICK_RESOLVE      = 1 << 2,   /* Return the fully resolved (chased) path, rather than the path to the entry */
+} PickFlags;
+
+typedef struct PickFilter {
+        uint32_t type_mask;           /* A mask of 1U << DT_REG, 1U << DT_DIR, … */
+        const char *basename;         /* Can be overriden by search pattern */
+        const char *version;
+        Architecture architecture;
+        const char *suffix;           /* Can be overriden by search pattern */
+} PickFilter;
+
+typedef struct PickResult {
+        char *path;
+        int fd; /* O_PATH */
+        struct stat st;
+        char *version;
+        Architecture architecture;
+        unsigned tries_left;
+        unsigned tries_done;
+} PickResult;
+
+#define PICK_RESULT_NULL                                \
+        (const PickResult) {                            \
+                .fd = -EBADF,                           \
+                .st.st_mode = MODE_INVALID,             \
+                .architecture = _ARCHITECTURE_INVALID,  \
+                .tries_left = UINT_MAX,                 \
+                .tries_done = UINT_MAX,                 \
+        }
+
+#define TAKE_PICK_RESULT(ptr) TAKE_GENERIC(ptr, PickResult, PICK_RESULT_NULL)
+
+void pick_result_done(PickResult *p);
+
+int path_pick(const char *toplevel_path,
+              int toplevel_fd,
+              const char *path,
+              const PickFilter *filter,
+              PickFlags flags,
+              PickResult *ret);
+
+int path_pick_update_warn(
+                char **path,
+                const PickFilter *filter,
+                PickFlags flags,
+                PickResult *ret);
+
+extern const PickFilter pick_filter_image_raw;
+extern const PickFilter pick_filter_image_dir;
index aec125d483ac9d68fb4170f24c0bd341c1b6c0bb..3439f585ee5b2d727732d7a558e982e7d686863b 100644 (file)
@@ -178,6 +178,7 @@ simple_tests += files(
         'test-user-util.c',
         'test-utf8.c',
         'test-verbs.c',
+        'test-vpick.c',
         'test-web-util.c',
         'test-xattr-util.c',
         'test-xml.c',
diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c
new file mode 100644 (file)
index 0000000..1a288e8
--- /dev/null
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "vpick.h"
+
+TEST(path_pick) {
+        _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+        _cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF;
+
+        dfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &p);
+        assert(dfd >= 0);
+
+        sub_dfd = open_mkdir_at(dfd, "foo.v", O_CLOEXEC, 0777);
+        assert(sub_dfd >= 0);
+
+        assert_se(write_string_file_at(sub_dfd, "foo_5.5.raw", "5.5", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_55.raw", "55", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_5.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_5_ia64.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_7.raw", "7", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_7_x86-64.raw", "7 64bit", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_55_x86-64.raw", "55 64bit", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_55_x86.raw", "55 32bit", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "foo_99_x86.raw", "99 32bit", WRITE_STRING_FILE_CREATE) >= 0);
+
+        /* Let's add an entry for sparc (which is a valid arch, but almost certainly not what we test
+         * on). This entry should hence always be ignored */
+        if (native_architecture() != ARCHITECTURE_SPARC)
+                assert_se(write_string_file_at(sub_dfd, "foo_100_sparc.raw", "100 sparc", WRITE_STRING_FILE_CREATE) >= 0);
+
+        assert_se(write_string_file_at(sub_dfd, "quux_1_s390.raw", "waldo1", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "quux_2_s390+4-6.raw", "waldo2", WRITE_STRING_FILE_CREATE) >= 0);
+        assert_se(write_string_file_at(sub_dfd, "quux_3_s390+0-10.raw", "waldo3", WRITE_STRING_FILE_CREATE) >= 0);
+
+        _cleanup_free_ char *pp = NULL;
+        pp = path_join(p, "foo.v");
+        assert_se(pp);
+
+        _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
+
+        PickFilter filter = {
+                .architecture = _ARCHITECTURE_INVALID,
+                .suffix = ".raw",
+        };
+
+        if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
+                assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+                assert_se(S_ISREG(result.st.st_mode));
+                assert_se(streq_ptr(result.version, "99"));
+                assert_se(result.architecture == ARCHITECTURE_X86);
+                assert_se(endswith(result.path, "/foo_99_x86.raw"));
+
+                pick_result_done(&result);
+        }
+
+        filter.architecture = ARCHITECTURE_X86_64;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(streq_ptr(result.version, "55"));
+        assert_se(result.architecture == ARCHITECTURE_X86_64);
+        assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
+        pick_result_done(&result);
+
+        filter.architecture = ARCHITECTURE_IA64;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(streq_ptr(result.version, "5"));
+        assert_se(result.architecture == ARCHITECTURE_IA64);
+        assert_se(endswith(result.path, "/foo_5_ia64.raw"));
+        pick_result_done(&result);
+
+        filter.architecture = _ARCHITECTURE_INVALID;
+        filter.version = "5";
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(streq_ptr(result.version, "5"));
+        if (native_architecture() != ARCHITECTURE_IA64) {
+                assert_se(result.architecture == _ARCHITECTURE_INVALID);
+                assert_se(endswith(result.path, "/foo_5.raw"));
+        }
+        pick_result_done(&result);
+
+        filter.architecture = ARCHITECTURE_IA64;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(streq_ptr(result.version, "5"));
+        assert_se(result.architecture == ARCHITECTURE_IA64);
+        assert_se(endswith(result.path, "/foo_5_ia64.raw"));
+        pick_result_done(&result);
+
+        filter.architecture = ARCHITECTURE_CRIS;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == 0);
+        assert_se(result.st.st_mode == MODE_INVALID);
+        assert_se(!result.version);
+        assert_se(result.architecture < 0);
+        assert_se(!result.path);
+
+        assert_se(unlinkat(sub_dfd, "foo_99_x86.raw", 0) >= 0);
+
+        filter.architecture = _ARCHITECTURE_INVALID;
+        filter.version = NULL;
+        if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
+                assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+                assert_se(S_ISREG(result.st.st_mode));
+                assert_se(streq_ptr(result.version, "55"));
+
+                if (native_architecture() == ARCHITECTURE_X86_64) {
+                        assert_se(result.architecture == ARCHITECTURE_X86_64);
+                        assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
+                } else {
+                        assert_se(result.architecture == ARCHITECTURE_X86);
+                        assert_se(endswith(result.path, "/foo_55_x86.raw"));
+                }
+                pick_result_done(&result);
+        }
+
+        /* Test explicit patterns in last component of path not being .v */
+        free(pp);
+        pp = path_join(p, "foo.v/foo___.raw");
+        assert_se(pp);
+
+        if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
+                assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+                assert_se(S_ISREG(result.st.st_mode));
+                assert_se(streq_ptr(result.version, "55"));
+                assert_se(result.architecture == native_architecture());
+                assert_se(endswith(result.path, ".raw"));
+                assert_se(strrstr(result.path, "/foo_55_x86"));
+                pick_result_done(&result);
+        }
+
+        /* Specify an explicit path */
+        free(pp);
+        pp = path_join(p, "foo.v/foo_5.raw");
+        assert_se(pp);
+
+        filter.type_mask = UINT32_C(1) << DT_DIR;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == -ENOTDIR);
+
+        filter.type_mask = UINT32_C(1) << DT_REG;
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(!result.version);
+        assert_se(result.architecture == _ARCHITECTURE_INVALID);
+        assert_se(path_equal(result.path, pp));
+        pick_result_done(&result);
+
+        free(pp);
+        pp = path_join(p, "foo.v");
+        assert_se(pp);
+
+        filter.architecture = ARCHITECTURE_S390;
+        filter.basename = "quux";
+
+        assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
+        assert_se(S_ISREG(result.st.st_mode));
+        assert_se(streq_ptr(result.version, "2"));
+        assert_se(result.tries_left == 4);
+        assert_se(result.tries_done == 6);
+        assert_se(endswith(result.path, "quux_2_s390+4-6.raw"));
+        assert_se(result.architecture == ARCHITECTURE_S390);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);