/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <sys/stat.h>

#include "alloc-util.h"
#include "architecture.h"
#include "bitfield.h"
#include "chase.h"
#include "fd-util.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
#include "recurse-dir.h"
#include "stat-util.h"
#include "string-util.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;
}

int pick_result_compare(const PickResult *a, const PickResult *b, PickFlags flags) {
        int d;

        assert(a);
        assert(b);

        /* Returns > 0 if 'a' is the better pick, < 0 if 'b' is the better pick, 0 if they are equal. */

        /* Prefer entries with tries left over those without */
        if (FLAGS_SET(flags, PICK_TRIES))
                d = CMP(a->tries_left != 0, b->tries_left != 0);
        else
                d = 0;

        /* Prefer newer versions */
        if (d == 0)
                d = strverscmp_improved(a->version, b->version);

        if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
                /* Prefer native architectures over non-native architectures */
                if (d == 0)
                        d = CMP(a->architecture == native_architecture(), b->architecture == native_architecture());

                /* Prefer secondary architectures over other architectures */
#ifdef ARCHITECTURE_SECONDARY
                if (d == 0)
                        d = CMP(a->architecture == ARCHITECTURE_SECONDARY, b->architecture == ARCHITECTURE_SECONDARY);
#endif
        }

        /* Prefer entries with more tries left */
        if (FLAGS_SET(flags, PICK_TRIES)) {
                if (d == 0)
                        d = CMP(a->tries_left, b->tries_left);

                /* Prefer entries with fewer attempts done so far */
                if (d == 0)
                        d = -CMP(a->tries_done, b->tries_done);
        }

        /* Finally, just compare the filenames as strings */
        if (d == 0)
                d = path_compare_filename(a->path, b->path);

        return d;
}

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 (!isempty(filter->suffix))
                if (!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 (BIT_SET(type_mask, 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(wildcard_fd_is_valid(toplevel_fd));
        assert(inode_path);
        assert(filter);
        assert(ret);

        if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
                r = chaseat(toplevel_fd,
                            toplevel_fd,
                            inode_path,
                            /* flags= */ 0,
                            FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : NULL,
                            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",
                                       empty_to_root(toplevel_path), skip_leading_slash(inode_path));

        if (filter->type_mask != 0 && !BIT_SET(filter->type_mask, IFTODT(st.st_mode))) {
                log_debug("Inode '%s/%s' has wrong type, found '%s'.",
                          empty_to_root(toplevel_path), skip_leading_slash(inode_path),
                          inode_type_to_string(st.st_mode));
                *ret = PICK_RESULT_NULL;
                return 0;
        }

        _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 bool architecture_matches(const PickFilter *filter, Architecture a) {
        assert(filter);

        if (filter->architecture >= 0)
                return a == filter->architecture;

        if (a == native_architecture())
                return true;

#ifdef ARCHITECTURE_SECONDARY
        if (a == ARCHITECTURE_SECONDARY)
                return true;
#endif

        return a == _ARCHITECTURE_INVALID;
}

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) {

        _cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
        int r;

        assert(wildcard_fd_is_valid(toplevel_fd));
        assert(inode_path);
        assert(filter);
        assert(ret);

        if (inode_fd < 0) {
                r = chaseat(toplevel_fd, toplevel_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
                if (r < 0)
                        return r;
        }

        /* Maybe the filter is fully specified? Then we can generate the file name directly */
        _cleanup_free_ char *j = NULL;
        r = format_fname(filter, flags, &j);
        if (r >= 0) {
                _cleanup_free_ char *object_path = NULL;

                /* Yay! This worked! */
                _cleanup_free_ char *p = path_join(inode_path, j);
                if (!p)
                        return log_oom_debug();

                _cleanup_close_ int object_fd = -EBADF;
                r = chaseat(toplevel_fd, toplevel_fd, p, /* flags= */ 0, &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/%s': %m",
                                               empty_to_root(toplevel_path), skip_leading_slash(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 */
        _cleanup_close_ int 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",
                                       empty_to_root(toplevel_path), skip_leading_slash(inode_path));

        _cleanup_free_ DirectoryEntries *de = NULL;
        r = readdir_all(dir_fd, 0, &de);
        if (r < 0)
                return log_debug_errno(r, "Failed to read directory '%s/%s': %m",
                                       empty_to_root(toplevel_path), skip_leading_slash(inode_path));

        _cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;

        FOREACH_ARRAY(entry, de->entries, de->n_entries) {
                unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX;
                _cleanup_free_ char *dname = NULL;
                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 (!isempty(filter->suffix)) {
                        char *sfx = endswith(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;
                        }
                }

                Architecture a = _ARCHITECTURE_INVALID;
                if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
                        char *underscore = strrchr(e, '_');

                        if (underscore)
                                a = architecture_from_string(underscore + 1);

                        if (!architecture_matches(filter, a)) {
                                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;
                }

                _cleanup_free_ char *p = path_join(inode_path, (*entry)->d_name);
                if (!p)
                        return log_oom_debug();

                _cleanup_(pick_result_done) PickResult found = PICK_RESULT_NULL;
                r = pin_choice(toplevel_path,
                               toplevel_fd,
                               p,
                               /* _inode_fd= */ -EBADF,
                               found_tries_left,
                               found_tries_done,
                               &(const PickFilter) {
                                       .type_mask = filter->type_mask,
                                       .basename = filter->basename,
                                       .version = empty_to_null(e),
                                       .architecture = a,
                                       .suffix = filter->suffix,
                               },
                               flags,
                               &found);
                if (r == 0)
                        continue;
                if (r < 0)
                        return r;

                if (!best.path) {
                        best = TAKE_PICK_RESULT(found);
                        continue;
                }

                if (pick_result_compare(&found, &best, flags) <= 0) {
                        log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", found.path, best.path);
                        continue;
                }

                pick_result_done(&best);
                best = TAKE_PICK_RESULT(found);
        }

        if (!best.path) { /* Everything was good, but we didn't find any suitable entry */
                *ret = PICK_RESULT_NULL;
                return 0;
        }

        *ret = TAKE_PICK_RESULT(best);
        return 1;
}

static int path_pick_one(
                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 = NULL, *enumeration_path;
        uint32_t filter_type_mask;
        int r;

        assert(wildcard_fd_is_valid(toplevel_fd));
        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 */
                if (!isempty(filter->suffix)) {
                        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 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 && !BIT_SET(filter_type_mask, 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(const char *toplevel_path,
              int toplevel_fd,
              const char *path,
              const PickFilter filters[],
              size_t n_filters,
              PickFlags flags,
              PickResult *ret) {

        _cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;
        int r;

        assert(wildcard_fd_is_valid(toplevel_fd));
        assert(path);
        assert(filters || n_filters == 0);
        assert(ret);

        /* Iterate through all filters and pick the best result */
        for (size_t i = 0; i < n_filters; i++) {
                _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;

                r = path_pick_one(toplevel_path, toplevel_fd, path, &filters[i], flags, &result);
                if (r < 0)
                        return r;
                if (r == 0)
                        continue;

                if (!best.path) {
                        best = TAKE_PICK_RESULT(result);
                        continue;
                }

                if (pick_result_compare(&result, &best, flags) <= 0) {
                        log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.",
                                  result.path, best.path);
                        continue;
                }

                pick_result_done(&best);
                best = TAKE_PICK_RESULT(result);
        }

        if (!best.path) {
                *ret = PICK_RESULT_NULL;
                return 0;
        }

        *ret = TAKE_PICK_RESULT(best);
        return 1;
}

int path_pick_update_warn(
                char **path,
                const PickFilter filters[],
                size_t n_filters,
                PickFlags flags,
                PickResult *ret_result) {

        _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
        int r;

        assert(path);
        assert(*path);
        assert(filters || n_filters == 0);

        /* This updates the first argument if needed! */

        r = path_pick(/* toplevel_path= */ NULL,
                      /* toplevel_fd= */ AT_FDCWD,
                      *path,
                      filters,
                      n_filters,
                      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;
}

int path_uses_vpick(const char *path) {
        _cleanup_free_ char *dir = NULL, *parent = NULL, *fname = NULL;
        int r;

        assert(path);

        r = path_extract_filename(path, &fname);
        if (r == -EADDRNOTAVAIL)
                return 0;
        if (r < 0)
                return r;

        /* ...PATH/NAME.SUFFIX.v */
        if (endswith(fname, ".v"))
                return 1;

        /* ...PATH.v/NAME___.SUFFIX */
        if (!strrstr(fname, "___"))
                return 0;

        r = path_extract_directory(path, &dir);
        if (IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */
                return 0;
        if (r < 0)
                return r;

        r = path_extract_filename(dir, &parent);
        if (r == -EADDRNOTAVAIL)
                return 0;
        if (r < 0)
                return r;

        return !!endswith(parent, ".v");
}

const PickFilter pick_filter_image_raw[1] = {
        {
                .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
                .architecture = _ARCHITECTURE_INVALID,
                .suffix = ".raw",
        },
};

const PickFilter pick_filter_image_dir[1] = {
        {
                .type_mask = UINT32_C(1) << DT_DIR,
                .architecture = _ARCHITECTURE_INVALID,
        },
};

const PickFilter pick_filter_image_mstack[1] = {
        {
                .type_mask = UINT32_C(1) << DT_DIR,
                .architecture = _ARCHITECTURE_INVALID,
                .suffix = ".mstack",
        },
};
