git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file src/resolve.c

#define _POSIX_C_SOURCE 200809L
#include "resolve.h"
#include "deps.h"
#include "package.h"
#include "provides.h"
#include "scan.h"
#include "solve.h"
#include "stage.h"
#include "verify.h"
#include "../backends/pacman.h"
#include "../backends/deb-version.h"
#include "version.h"
#include "../backends/apk-version.h"
#include "../backends/xbps-version.h"
#include "../backends/rpm-version.h"

#include <archive.h>
#include <archive_entry.h>
#include <elf.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct elf_edge {
    size_t requirement;
    const char *path, *kind, *target;
    char *owned_target;
    const struct holy_scanned_file *file;
    const struct holy_elf_symbol *symbol;
};

struct package_edge {
    size_t requirement;
    char *kind, *name, *arch, *libc, *relation, *version;
};

struct package_claim { char *capability, *version; };

struct payload_path {
    char *path, *link;
    unsigned int mode;
};

struct version_adapter {
    const char *family;
    int (*compare)(const char *, const char *, int *);
};

static const struct version_adapter version_adapters[] = {
    {"pacman", holy_pacman_version_compare},
    {"deb", holy_deb_version_compare},
    {"holy", holy_version_compare},
    {"apk", holy_apk_version_compare},
    {"xbps", holy_xbps_version_compare},
    {"rpm", holy_rpm_version_compare}
};

struct local_item {
    struct holy_package_identity identity;
    char *unsupported_id;
    struct holy_solver_requirement *requirements;
    char **requirement_ids;
    char **original_requirements;
    size_t requirement_count;
    char *capability;
    struct holy_scan_result scan;
    struct elf_edge *edges;
    size_t edge_count;
    struct package_edge *package_edges;
    size_t package_edge_count;
    struct package_claim *claims;
    size_t claim_count;
    struct payload_path *file_paths;
    size_t file_count;
};

static int literal_path(const char *path);

static char *named_capability(const char *kind, const char *name)
{
    size_t prefix = strlen(kind), n = strlen(name);
    char *capability;
    if (prefix > 65536 || n > 65536 - prefix - 2) return NULL;
    capability = malloc(prefix + n + 2);
    if (capability) {
        memcpy(capability, kind, prefix);
        capability[prefix] = ':';
        memcpy(capability + prefix + 1, name, n + 1);
    }
    return capability;
}

static char *package_capability(const char *name)
{
    return named_capability("package", name);
}

static int add_requirement(struct local_item *item, const char *id, const char *cap)
{
    struct holy_solver_requirement *next;
    char **ids;
    char *capability;
    char *identifier;
    char *original;
    size_t i;
    if (item->requirement_count >= 65536) return 0;
    for (i = 0; i < item->requirement_count; ++i)
        if (!strcmp(item->requirement_ids[i], id)) return 0;
    capability = strdup(cap);
    if (!capability) return 0;
    identifier = strdup(id);
    if (!identifier) { free(capability); return 0; }
    original = strdup(cap);
    if (!original) { free(capability); free(identifier); return 0; }
    next = realloc(item->requirements,
                   (item->requirement_count + 1) * sizeof *next);
    if (!next) { free(identifier); free(capability); free(original); return 0; }
    item->requirements = next;
    ids = realloc(item->requirement_ids,
                  (item->requirement_count + 1) * sizeof *ids);
    if (!ids) { free(identifier); free(capability); free(original); return 0; }
    item->requirement_ids = ids;
    ids = realloc(item->original_requirements, (item->requirement_count + 1) * sizeof *ids);
    if (!ids) { free(identifier); free(capability); free(original); return 0; }
    item->original_requirements = ids;
    ids[item->requirement_count] = original;
    item->requirements[item->requirement_count].first = capability;
    item->requirements[item->requirement_count].alternative = NULL;
    item->requirement_ids[item->requirement_count] = identifier;
    ++item->requirement_count;
    return 1;
}

static int add_package_edge(struct local_item *item, size_t requirement,
                            const char *kind, const char *name,
                            const char *arch, const char *libc,
                            const char *relation, const char *version)
{
    struct package_edge *grown, *edge;
    if (item->package_edge_count >= 65536) return 0;
    grown = realloc(item->package_edges,
                    (item->package_edge_count + 1) * sizeof *grown);
    if (!grown) return 0;
    item->package_edges = grown;
    edge = &grown[item->package_edge_count++];
    memset(edge, 0, sizeof *edge);
    edge->requirement = requirement;
    edge->kind = strdup(kind); edge->name = strdup(name);
    edge->arch = strdup(arch); edge->libc = strdup(libc);
    edge->relation = strdup(relation); edge->version = strdup(version);
    return edge->kind && edge->name && edge->arch && edge->libc &&
           edge->relation && edge->version;
}

struct or_context { struct local_item *item; size_t requirement; };

static int add_or_branch(void *opaque, const char *name,
                         const char *relation, const char *version)
{
    struct or_context *context = opaque;
    return add_package_edge(context->item, context->requirement,
                            "package", name, "any", "any", relation, version);
}

static int exact_requirement(void *opaque, const char *id,
    const char *consumer, const char *kind, const char *name,
    const char *arch, const char *libc, const char *relation,
    const char *version, const char *original, const char *evidence)
{
    struct local_item *item = opaque;
    char *capability;
    int ok;
    (void)original; (void)evidence;
    if (strcmp(consumer, item->identity.name)) return 0;
    if (!strcmp(kind, "package-or")) {
        struct or_context context;
        if (!item->identity.version_family ||
            strcmp(item->identity.version_family, "deb") ||
            strcmp(relation, "any") || strcmp(arch, "any") ||
            strcmp(libc, "any")) return 0;
        capability = named_capability(kind, name);
        if (!capability) return 0;
        ok = add_requirement(item, id, capability);
        free(capability);
        if (!ok) return 0;
        context.item = item;
        context.requirement = item->requirement_count - 1;
        return holy_package_or_each(name, add_or_branch, &context);
    }
    if (strcmp(kind, "package") && strcmp(kind, "file") &&
        strcmp(kind, "command") && strcmp(kind, "soname")) {
        if (!item->unsupported_id) item->unsupported_id = strdup(id);
        return item->unsupported_id != NULL;
    }
    if (!strcmp(kind, "soname") &&
        (!name[0] || strchr(name, '/') || strcmp(relation, "any"))) {
        if (!item->unsupported_id) item->unsupported_id = strdup(id);
        return item->unsupported_id != NULL;
    }
    if (
        (!strcmp(kind, "file") && (!literal_path(name) || strcmp(relation, "any"))) ||
        (!strcmp(kind, "command") && (!name[0] || strchr(name, '/') ||
                                     !strcmp(name, ".") || !strcmp(name, "..") ||
                                     strcmp(relation, "any")))) return 0;
    capability = named_capability(kind, name);
    if (!capability) return 0;
    ok = add_requirement(item, id, capability);
    free(capability);
    if (ok && (strcmp(kind, "package") || strcmp(arch, "any") ||
               strcmp(libc, "any") || strcmp(relation, "any"))) {
        ok = add_package_edge(item, item->requirement_count - 1,
                              kind, name, arch, libc, relation, version);
    }
    return ok;
}

static int package_claim(void *opaque, const char *kind, const char *name,
                          const char *arch, const char *libc, const char *version,
                          const char *evidence)
{
    struct local_item *item = opaque;
    struct package_claim *grown, *claim;
    (void)evidence;
    if (strcmp(kind, "package")) return 1;
    if ((strcmp(arch, "any") && strcmp(arch, item->identity.arch)) ||
        (strcmp(libc, "any") && strcmp(libc, item->identity.libc))) {
        fprintf(stderr, "holypkg: package capability scope disagrees with artifact: %s\n", name);
        return 0;
    }
    if (item->claim_count == 65536) return 0;
    grown = realloc(item->claims, (item->claim_count + 1) * sizeof *grown);
    if (!grown) return 0;
    item->claims = grown;
    claim = &grown[item->claim_count++];
    claim->capability = package_capability(name);
    claim->version = strdup(version);
    return claim->capability && claim->version;
}

static const struct version_adapter *version_adapter(const char *family)
{
    size_t i;
    if (!family) return NULL;
    for (i = 0; i < sizeof version_adapters / sizeof *version_adapters; ++i)
        if (!strcmp(family, version_adapters[i].family)) return &version_adapters[i];
    return NULL;
}

static int version_matches(const char *candidate, const struct package_edge *edge,
                           const struct version_adapter *adapter)
{
    int order;
    if (!strcmp(edge->relation, "any")) return 1;
    if (!strcmp(candidate, "-")) return 0;
    if (!adapter || !adapter->compare(candidate, edge->version, &order)) return -1;
    return !strcmp(edge->relation, "eq") ? order == 0 :
           !strcmp(edge->relation, "ge") ? order >= 0 :
           !strcmp(edge->relation, "gt") ? order > 0 :
           !strcmp(edge->relation, "le") ? order <= 0 : order < 0;
}

static int has_file(const struct local_item *item, const char *absolute);
static int has_command(const struct local_item *item, const char *name);
static int has_soname(const struct local_item *item, const char *name);

static int package_edge_matches(const struct local_item *consumer,
                                const struct package_edge *edge,
                                const struct local_item *candidate)
{
    const char *family = consumer->identity.version_family;
    const struct version_adapter *adapter = version_adapter(family);
    char *base;
    int matches = 0, constrained = strcmp(edge->relation, "any") != 0;
    size_t claim;
    if ((strcmp(edge->arch, "any") && strcmp(edge->arch, candidate->identity.arch)) ||
        (strcmp(edge->libc, "any") && strcmp(edge->libc, candidate->identity.libc)) ||
        (constrained && (!candidate->identity.version_family ||
                         strcmp(candidate->identity.version_family, family)))) return 0;
    if (!strcmp(edge->kind, "file")) return has_file(candidate, edge->name);
    if (!strcmp(edge->kind, "command")) return has_command(candidate, edge->name);
    if (!strcmp(edge->kind, "soname")) return has_soname(candidate, edge->name);
    base = named_capability(edge->kind, edge->name);
    if (!base) return -1;
    if (!strcmp(base, candidate->capability)) {
        const char *candidate_version = candidate->identity.version;
        char *with_revision = NULL;
        if (constrained && (!strcmp(family, "xbps") || !strcmp(family, "rpm")) &&
            candidate->identity.release) {
            size_t length = strlen(candidate_version) + strlen(candidate->identity.release) + 2;
            with_revision = malloc(length);
            if (!with_revision) { free(base); return -1; }
            snprintf(with_revision, length, "%s%c%s", candidate_version,
                     !strcmp(family, "rpm") ? '-' : '_', candidate->identity.release);
            candidate_version = with_revision;
        }
        matches = version_matches(candidate_version, edge, adapter);
        free(with_revision);
    }
    for (claim = 0; !matches && claim < candidate->claim_count; ++claim)
        if (!strcmp(base, candidate->claims[claim].capability))
            matches = version_matches(candidate->claims[claim].version, edge, adapter);
    free(base);
    return matches;
}

static int add_provide(struct holy_solver_item *item, const char *capability)
{
    const char **next;
    char *copy;
    size_t i;
    for (i = 0; i < item->provides_count; ++i)
        if (!strcmp(item->provides[i], capability)) return 1;
    if (item->provides_count >= 65536) return 0;
    copy = strdup(capability);
    if (!copy) return 0;
    next = realloc((void *)item->provides, (item->provides_count + 1) * sizeof *next);
    if (!next) { free(copy); return 0; }
    item->provides = next;
    next[item->provides_count++] = copy;
    return 1;
}

static int file_path_order(const void *left, const void *right)
{
    const struct payload_path *a = left, *b = right;
    return strcmp(a->path, b->path);
}

static int collect_file_path(void *opaque, const struct holy_manifest_entry *entry)
{
    struct local_item *item = opaque;
    struct payload_path *paths, *path;
    if (entry->directory) return 1;
    if (item->file_count == SIZE_MAX / sizeof *paths) return 0;
    paths = realloc(item->file_paths, (item->file_count + 1) * sizeof *paths);
    if (!paths) return 0;
    item->file_paths = paths;
    path = &paths[item->file_count];
    path->path = strdup(entry->path);
    path->link = entry->link ? strdup(entry->link) : NULL;
    path->mode = entry->mode;
    if (!path->path || (entry->link && !path->link)) {
        free(path->path); free(path->link);
        return 0;
    }
    ++item->file_count;
    return 1;
}

static const struct payload_path *find_path(const struct local_item *item, const char *path)
{
    struct payload_path key = {(char *)path, NULL, 0};
    return item->file_count ? bsearch(&key, item->file_paths, item->file_count,
                                      sizeof *item->file_paths, file_path_order) : NULL;
}

static int has_file(const struct local_item *item, const char *absolute)
{
    return find_path(item, absolute + 1) != NULL;
}

static int command_target(const struct local_item *item, const char *path)
{
    char *current = strdup(path);
    size_t hop;
    int result = 0;
    if (!current) return 0;
    for (hop = 0; hop < 16; ++hop) {
        const struct payload_path *entry = find_path(item, current);
        char *next;
        if (!entry) break;
        if (!entry->link) { result = (entry->mode & 0111) != 0; break; }
        next = holy_relative_link_path(current, strlen(current), entry->link, "");
        if (!next) break;
        free(current);
        current = next;
    }
    free(current);
    return result;
}

static int has_command(const struct local_item *item, const char *name)
{
    static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"};
    size_t i, length = strlen(name);
    for (i = 0; i < sizeof dirs / sizeof *dirs; ++i) {
        size_t prefix = strlen(dirs[i]);
        char *path = malloc(prefix + length + 1);
        int found;
        if (!path) return 0;
        memcpy(path, dirs[i], prefix);
        memcpy(path + prefix, name, length + 1);
        found = command_target(item, path);
        free(path);
        if (found) return 1;
    }
    return 0;
}

static int has_soname(const struct local_item *item, const char *name)
{
    size_t i;
    for (i = 0; i < item->scan.count; ++i) {
        const struct holy_scanned_file *file = &item->scan.files[i];
        if (file->elf.type == ET_DYN && !(file->elf.flags1 & DF_1_PIE) &&
            file->elf.soname && !strcmp(file->elf.soname, name) &&
            !strcmp(file->runtime, item->identity.libc)) return 1;
    }
    return 0;
}

static int package_requirements(struct local_item *local, struct holy_solver_item *items, size_t count)
{
    size_t i, j, k;
    for (i = 0; i < count; ++i) for (j = 0; j < local[i].package_edge_count; ++j) {
        const struct package_edge *edge = &local[i].package_edges[j];
        size_t index = edge->requirement;
        const char *family = local[i].identity.version_family;
        const char *relation = edge->relation;
        const struct version_adapter *adapter = version_adapter(family);
        int constrained = strcmp(relation, "any") != 0;
        char *capability;
        size_t length = strlen(local[i].requirement_ids[index]) + 80;
        if (constrained && !adapter) {
            fprintf(stderr, "holypkg: unsupported-version-family consumer=%s requirement=%s\n",
                local[i].identity.digest, local[i].requirement_ids[index]);
            return 0;
        }
        capability = malloc(length);
        if (!capability) return 0;
        snprintf(capability, length, "package-edge:%s:%s", local[i].identity.digest,
                 local[i].requirement_ids[index]);
        if (j && local[i].package_edges[j - 1].requirement == index) free(capability);
        else {
            free((char *)local[i].requirements[index].first);
            local[i].requirements[index].first = capability;
        }
        capability = (char *)local[i].requirements[index].first;
        for (k = 0; k < count; ++k) {
            int matches = package_edge_matches(&local[i], edge, &local[k]);
            if (matches < 0 || (matches && !add_provide(&items[k], capability))) {
                return 0;
            }
        }
    }
    return 1;
}

static int provides(const struct holy_solver_item *item, const char *capability)
{
    size_t i;
    for (i = 0; i < item->provides_count; ++i)
        if (!strcmp(item->provides[i], capability)) return 1;
    return 0;
}

static int compatible(const struct holy_scanned_file *a, const struct holy_scanned_file *b)
{
    return a->elf.elf_class == b->elf.elf_class && a->elf.machine == b->elf.machine &&
           !strcmp(a->runtime, b->runtime);
}

static int literal_path(const char *path)
{
    const char *part, *end;
    if (path[0] != '/' || !path[1] || strchr(path, '$')) return 0;
    for (part = path + 1; ; part = end + 1) {
        size_t length;
        end = strchr(part, '/');
        length = end ? (size_t)(end - part) : strlen(part);
        if (!length || (length == 1 && part[0] == '.') ||
            (length == 2 && !memcmp(part, "..", 2))) return 0;
        if (!end) return 1;
    }
}

static int needed_matches(const struct holy_scanned_file *consumer,
                           const struct holy_scanned_file *provider, const char *needed)
{
    size_t i, j;
    if (!compatible(consumer, provider) || provider->elf.type != ET_DYN ||
        (provider->elf.flags1 & DF_1_PIE)) return 0;
    if (needed[0] == '/') {
        if (!literal_path(needed) || strcmp(provider->path, needed + 1)) return 0;
    } else if (!provider->elf.soname || strcmp(provider->elf.soname, needed)) return 0;
    for (i = 0; i < consumer->elf.version_count; ++i) {
        const struct holy_elf_version *v = &consumer->elf.versions[i];
        if (v->weak || strcmp(v->provider, needed)) continue;
        for (j = 0; j < provider->elf.defined_version_count; ++j)
            if (!strcmp(v->name, provider->elf.defined_versions[j].name)) break;
        if (j == provider->elf.defined_version_count) return 0;
    }
    for (i = 0; i < consumer->elf.symbol_count; ++i) {
        const struct holy_elf_symbol *s = &consumer->elf.symbols[i];
        if (s->section || s->binding == STB_WEAK || !s->provider || strcmp(s->provider, needed)) continue;
        if (!holy_elf_exports_symbol(&provider->elf, s)) return 0;
    }
    return 1;
}

static int direct_provider(const struct holy_scanned_file *consumer,
                            const struct holy_scanned_file *provider)
{
    size_t i;
    if (consumer == provider) return 1;
    if (consumer->elf.interpreter && consumer->elf.interpreter[0] == '/' &&
        !strcmp(consumer->elf.interpreter + 1, provider->path)) return 1;
    for (i = 0; i < consumer->elf.needed_count; ++i) {
        const char *needed = consumer->elf.needed[i];
        if ((needed[0] == '/' && literal_path(needed) && !strcmp(needed + 1, provider->path)) ||
            (provider->elf.soname && !strcmp(needed, provider->elf.soname))) return 1;
    }
    return 0;
}

static int elf_requirement(struct local_item *local, struct holy_solver_item *items,
                            size_t count, size_t consumer_index,
                            const struct holy_scanned_file *file, const char *kind,
                            const char *target, const struct holy_elf_symbol *symbol)
{
    struct local_item *consumer = &local[consumer_index];
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    const char *parts[] = {consumer->identity.name, consumer->identity.arch,
                          consumer->identity.libc, file->path, kind, target};
    unsigned char hash[32];
    unsigned int length;
    char id[69] = "elf-", capability[140];
    struct elf_edge *edges;
    size_t i, j;
    int ok = 0;
    if (!ctx || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
    for (i = 0; i < sizeof parts / sizeof *parts; ++i)
        if (EVP_DigestUpdate(ctx, parts[i], strlen(parts[i]) + 1) != 1) goto done;
    if (EVP_DigestFinal_ex(ctx, hash, &length) != 1 || length != sizeof hash) goto done;
    for (i = 0; i < sizeof hash; ++i) snprintf(id + 4 + i * 2, 3, "%02x", hash[i]);
    snprintf(capability, sizeof capability, "elf:%s:%s", consumer->identity.digest, id);
    edges = realloc(consumer->edges, (consumer->edge_count + 1) * sizeof *edges);
    if (!edges) goto done;
    consumer->edges = edges;
    edges[consumer->edge_count].requirement = consumer->requirement_count;
    edges[consumer->edge_count].path = file->path;
    edges[consumer->edge_count].kind = kind;
    edges[consumer->edge_count].target = target;
    edges[consumer->edge_count].owned_target = NULL;
    edges[consumer->edge_count].file = file;
    edges[consumer->edge_count].symbol = symbol;
    if (!add_requirement(consumer, id, capability)) goto done;
    ++consumer->edge_count;
    for (i = 0; i < count; ++i) {
        for (j = 0; j < local[i].scan.count; ++j) {
            const struct holy_scanned_file *candidate = &local[i].scan.files[j];
            int matches;
            if (!strcmp(kind, "soname") || !strcmp(kind, "needed-path"))
                matches = needed_matches(file, candidate, target);
            else if (!strcmp(kind, "interpreter"))
                matches = target[0] == '/' && !strcmp(target + 1, candidate->path) &&
                          (candidate->mode & 0111) && compatible(file, candidate);
            else matches = compatible(file, candidate) && direct_provider(file, candidate) &&
                           holy_elf_exports_symbol(&candidate->elf, symbol);
            if (matches && !add_provide(&items[i], capability)) goto done;
        }
    }
    ok = 1;
done:
    EVP_MD_CTX_free(ctx);
    return ok;
}

static int script_path_edge(struct local_item *local, struct holy_solver_item *items,
                            size_t count, size_t consumer_index,
                            const struct holy_scanned_script *script,
                            const char *kind, const char *path,
                            const char *link_target)
{
    struct local_item *consumer = &local[consumer_index];
    const char *parts[] = {consumer->identity.name, consumer->identity.arch,
                          consumer->identity.libc, script->path, kind, path};
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    struct elf_edge *edges;
    unsigned char hash[32];
    unsigned int length;
    char id[72] = "script-", capability[160];
    size_t i, j;
    int ok = 0;
    if (!ctx || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
    for (i = 0; i < sizeof parts / sizeof *parts; ++i)
        if (EVP_DigestUpdate(ctx, parts[i], strlen(parts[i]) + 1) != 1) goto done;
    if (EVP_DigestFinal_ex(ctx, hash, &length) != 1 || length != sizeof hash) goto done;
    for (i = 0; i < sizeof hash; ++i) snprintf(id + 7 + i * 2, 3, "%02x", hash[i]);
    snprintf(capability, sizeof capability, "script:%s:%s", consumer->identity.digest, id);
    edges = realloc(consumer->edges, (consumer->edge_count + 1) * sizeof *edges);
    if (!edges) goto done;
    consumer->edges = edges;
    edges[consumer->edge_count].requirement = consumer->requirement_count;
    edges[consumer->edge_count].path = script->path;
    edges[consumer->edge_count].kind = kind;
    edges[consumer->edge_count].owned_target = malloc(strlen(path) + 2);
    if (!edges[consumer->edge_count].owned_target) goto done;
    sprintf(edges[consumer->edge_count].owned_target, "/%s", path);
    edges[consumer->edge_count].target = edges[consumer->edge_count].owned_target;
    edges[consumer->edge_count].file = NULL;
    edges[consumer->edge_count].symbol = NULL;
    if (!add_requirement(consumer, id, capability)) {
        free(edges[consumer->edge_count].owned_target);
        goto done;
    }
    ++consumer->edge_count;
    for (i = 0; i < count; ++i) {
        if (link_target) {
            for (j = 0; j < local[i].scan.symlink_count; ++j) {
                const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j];
                if (!strcmp(candidate->path, path) && !strcmp(candidate->target, link_target) &&
                    !add_provide(&items[i], capability)) goto done;
            }
        } else for (j = 0; j < local[i].scan.count; ++j) {
            const struct holy_scanned_file *candidate = &local[i].scan.files[j];
            if (!strcmp(candidate->path, path) && (candidate->mode & 0111) &&
                (candidate->elf.type == ET_EXEC ||
                 (candidate->elf.type == ET_DYN &&
                  ((candidate->elf.flags1 & DF_1_PIE) || candidate->elf.interpreter))) &&
                !add_provide(&items[i], capability)) goto done;
        }
    }
    ok = 1;
done:
    EVP_MD_CTX_free(ctx);
    return ok;
}

static int script_requirement(struct local_item *local, struct holy_solver_item *items,
                              size_t count, size_t consumer_index,
                              const struct holy_scanned_script *script)
{
    char *path = strdup(script->interpreter + 1);
    char *visited[16] = {0};
    size_t hop, i, j;
    int result = 0;
    if (!path) return 0;
    for (hop = 0; hop < 16; ++hop) {
        const char *target = NULL;
        size_t prefix = 0;
        char *next;
        for (i = 0; i < hop; ++i) if (!strcmp(path, visited[i])) {
            result = 3; goto done;
        }
        visited[hop] = strdup(path);
        if (!visited[hop]) goto done;
        for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.symlink_count; ++j) {
            const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j];
            size_t n = strlen(candidate->path);
            if (n <= prefix || strncmp(path, candidate->path, n) ||
                (path[n] && path[n] != '/')) continue;
            prefix = n;
            target = candidate->target;
        }
        if (!target) {
            result = script_path_edge(local, items, count, consumer_index, script,
                                      "shebang", path, NULL);
            break;
        }
        for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.symlink_count; ++j) {
            const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j];
            if (strlen(candidate->path) == prefix && !strncmp(path, candidate->path, prefix) &&
                strcmp(candidate->target, target)) {
                fprintf(stderr, "holypkg: ambiguous script alias %.*s\n", (int)prefix, path);
                result = 3; goto done;
            }
        }
        next = holy_relative_link_path(path, prefix, target, path + prefix);
        if (!next) { result = 3; goto done; }
        path[prefix] = 0;
        if (!script_path_edge(local, items, count, consumer_index, script,
                              "path-alias", path, target)) { free(next); goto done; }
        free(path);
        path = next;
    }
    if (hop == 16) result = 3;
done:
    for (i = 0; i < 16; ++i) free(visited[i]);
    free(path);
    return result;
}

static int elf_requirements(struct local_item *local, struct holy_solver_item *items, size_t count)
{
    size_t i, j, k;
    for (i = 0; i < count; ++i)
        for (j = 0; j < local[i].scan.script_count; ++j)
            {
                int status = script_requirement(local, items, count, i, &local[i].scan.scripts[j]);
                if (status != 1) return status;
            }
    for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.count; ++j) {
        const struct holy_scanned_file *f = &local[i].scan.files[j];
        if (f->elf.interpreter && !elf_requirement(local, items, count, i, f, "interpreter", f->elf.interpreter, NULL)) return 0;
        for (k = 0; k < f->elf.needed_count; ++k) {
            if (strchr(f->elf.needed[k], '/') && !literal_path(f->elf.needed[k])) {
                fputs("holypkg: DT_NEEDED paths require a launch context\n", stderr);
                return 0;
            }
            if (!elf_requirement(local, items, count, i, f,
                                 f->elf.needed[k][0] == '/' ? "needed-path" : "soname",
                                 f->elf.needed[k], NULL)) return 0;
        }
        for (k = 0; k < f->elf.symbol_count; ++k) {
            const struct holy_elf_symbol *s = &f->elf.symbols[k];
            if (s->section || !s->name[0] || s->binding == STB_WEAK || s->binding == STB_LOCAL) continue;
            if (s->provider) {
                size_t n;
                for (n = 0; n < f->elf.needed_count; ++n)
                    if (!strcmp(f->elf.needed[n], s->provider)) break;
                if (n == f->elf.needed_count) return 0;
            } else if (!elf_requirement(local, items, count, i, f, "symbol", s->name, s)) return 0;
        }
    }
    return 1;
}

static void json_string(const char *s)
{
    const unsigned char *p = (const unsigned char *)s;
    putchar('"');
    for (; *p; ++p) {
        if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); }
        else if (*p < 32 || *p >= 127) printf("\\u%04x", *p);
        else putchar(*p);
    }
    putchar('"');
}

static int symbol_context(const struct local_item *consumer, const struct elf_edge *edge,
                            const struct local_item *provider, const struct holy_solver_item *item)
{
    size_t i, j;
    for (i = 0; i < provider->scan.count; ++i) {
        const struct holy_scanned_file *f = &provider->scan.files[i];
        if (!compatible(edge->file, f) || !holy_elf_exports_symbol(&f->elf, edge->symbol)) continue;
        if (f == edge->file) return 1;
        for (j = 0; j < consumer->edge_count; ++j) {
            const struct elf_edge *dependency = &consumer->edges[j];
            if (dependency->file != edge->file) continue;
            if ((!strcmp(dependency->kind, "soname") && f->elf.soname &&
                 !strcmp(dependency->target, f->elf.soname)) ||
                ((!strcmp(dependency->kind, "interpreter") || !strcmp(dependency->kind, "needed-path")) &&
                 dependency->target[0] == '/' && !strcmp(dependency->target + 1, f->path)))
                if (provides(item, consumer->requirements[dependency->requirement].first)) return 1;
        }
    }
    return 0;
}

static void report_edges(const struct local_item *local, const struct holy_solver_item *items,
                          size_t count, const int *selected, int json)
{
    size_t i, j, k;
    for (i = 0; i < count; ++i) {
        if (selected ? !selected[i] : i != 0) continue;
        for (j = 0; j < local[i].edge_count; ++j) {
            const struct elf_edge *edge = &local[i].edges[j];
            const char *id = local[i].requirement_ids[edge->requirement];
            const char *cap = local[i].requirements[edge->requirement].first;
            int first = 1;
            if (json) {
                printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"%s\",\"id\":\"%s\",\"consumer\":\"%s\",\"path\":",
                       !strcmp(edge->kind, "shebang") ? "script-edge" : "elf-edge",
                       id, local[i].identity.digest);
                json_string(edge->path);
                printf(",\"kind\":\"%s\",\"target\":", edge->kind);
                json_string(edge->target);
                fputs(",\"providers\":[", stdout);
            } else {
                printf("%s %s consumer=%s path=",
                       !strcmp(edge->kind, "shebang") ? "script-edge" : "elf-edge",
                       id, local[i].identity.digest);
                json_string(edge->path);
                printf(" kind=%s target=", edge->kind);
                json_string(edge->target);
                fputs(" providers=", stdout);
            }
            for (k = 0; k < count; ++k) {
                if ((selected && !selected[k]) || !provides(&items[k], cap)) continue;
                if (!first) putchar(',');
                if (json) json_string(local[k].identity.digest);
                else fputs(local[k].identity.digest, stdout);
                first = 0;
            }
            puts(json ? "]}" : "");
        }
    }
}

static int supported_metadata(const char *snapshot)
{
    struct archive *a = archive_read_new();
    struct archive_entry *entry;
    int status, hooks = 0, transform = 0, ok = 0;
    if (!a || archive_read_support_filter_lz4(a) != ARCHIVE_OK ||
        archive_read_support_format_tar(a) != ARCHIVE_OK ||
        archive_read_open_filename(a, snapshot, 8192) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
        const char *name = archive_entry_pathname(entry);
        if (!strcmp(name, "HOLY/hooks")) {
            hooks = 1;
            if (archive_entry_filetype(entry) != AE_IFREG ||
                archive_entry_size(entry) < 0 || archive_entry_size(entry) > 1024 * 1024)
                goto done;
        }
        if (!strcmp(name, "HOLY/transform")) {
            transform = 1;
            if (archive_entry_filetype(entry) != AE_IFREG) goto done;
        }
        if (archive_read_data_skip(a) != ARCHIVE_OK) goto done;
    }
    ok = status == ARCHIVE_EOF && hooks && transform;
done:
    if (a) archive_read_free(a);
    return ok;
}

static const char *missing_requirement(const struct local_item *local,
                                        const struct holy_solver_item *items, size_t count,
                                        const char *const *skip_ids, size_t skip_count,
                                        size_t *consumer_index, size_t *requirement_index)
{
    unsigned char *seen = calloc(count, 1);
    size_t *queue = malloc(count * sizeof *queue);
    size_t head = 0, tail = 1, j;
    const char *missing = NULL;
    if (!seen || !queue) goto done;
    seen[0] = 1;
    queue[0] = 0;
    while (head < tail) {
        size_t current = queue[head++];
        const struct local_item *consumer = &local[current];
        for (j = 0; j < consumer->requirement_count; ++j) {
            size_t i, matches = 0, provider = 0;
            for (i = 0; i < count; ++i)
                if (provides(&items[i], consumer->requirements[j].first)) {
                    ++matches;
                    provider = i;
                }
            if (!matches) {
                size_t skip;
                for (skip = 0; skip < skip_count; ++skip)
                    if (!strncmp(skip_ids[skip], consumer->identity.digest, 64) &&
                        skip_ids[skip][64] == ':' &&
                        !strcmp(skip_ids[skip] + 65, consumer->requirement_ids[j])) break;
                if (skip < skip_count) continue;
                missing = consumer->requirement_ids[j];
                *consumer_index = current;
                *requirement_index = j;
                goto done;
            }
            if (matches == 1 && !seen[provider]) {
                seen[provider] = 1;
                queue[tail++] = provider;
            }
        }
    }
done:
    free(seen);
    free(queue);
    return missing;
}

static int capture_missing(const struct local_item *consumer, size_t requirement,
                           struct holy_missing_requirement *missing)
{
    size_t k;
    const char *id = consumer->requirement_ids[requirement];
    const char *kind = NULL, *name = NULL, *path = NULL, *original;
    for (k = 0; k < consumer->edge_count; ++k) {
        const struct elf_edge *edge = &consumer->edges[k];
        if (edge->requirement != requirement) continue;
        kind = !strcmp(edge->kind, "soname") ? "soname" :
               !strcmp(edge->kind, "symbol") ? NULL : "file";
        name = edge->target;
        if (!strcmp(edge->kind, "soname")) path = edge->path;
        break;
    }
    if (!name) {
        original = consumer->original_requirements[requirement];
        if (!strncmp(original, "package-or:", 11)) {
            kind = "package-or";
            name = original + 11;
        }
    }
    if (!name) {
        for (k = 0; k < consumer->package_edge_count; ++k)
            if (consumer->package_edges[k].requirement == requirement) {
                kind = consumer->package_edges[k].kind;
                name = consumer->package_edges[k].name;
                break;
            }
    }
    if (!name) {
        original = consumer->original_requirements[requirement];
        if (!strncmp(original, "package:", 8)) { kind = "package"; name = original + 8; }
        else if (!strncmp(original, "file:", 5)) { kind = "file"; name = original + 5; }
        else if (!strncmp(original, "command:", 8)) { kind = "command"; name = original + 8; }
        else if (!strncmp(original, "soname:", 7)) { kind = "soname"; name = original + 7; }
    }
    if (!kind || !name || !*name) return 0;
    missing->consumer = strdup(consumer->identity.digest);
    missing->id = strdup(id);
    missing->kind = strdup(kind);
    missing->name = strdup(name);
    if (path) missing->path = strdup(path);
    if (missing->consumer && missing->id && missing->kind && missing->name &&
        (!path || missing->path))
        return 1;
    holy_missing_requirement_free(missing);
    return 0;
}

static char *choice_requirement(const char *choice, const char **digest)
{
    const char *equal = strchr(choice, '=');
    char *id;
    size_t i, length;
    if (!equal || equal == choice || strlen(equal + 1) != 64) return NULL;
    length = (size_t)(equal - choice);
    if (length > 65536) return NULL;
    for (i = 0; i < length; ++i)
        if (!((choice[i] >= 'a' && choice[i] <= 'z') ||
              (choice[i] >= 'A' && choice[i] <= 'Z') ||
              (choice[i] >= '0' && choice[i] <= '9') ||
              choice[i] == '-' || choice[i] == '_' || choice[i] == '.')) return NULL;
    for (i = 0; i < 64; ++i)
        if (!((equal[i + 1] >= '0' && equal[i + 1] <= '9') ||
              (equal[i + 1] >= 'a' && equal[i + 1] <= 'f'))) return NULL;
    id = strndup(choice, length);
    if (id) *digest = equal + 1;
    return id;
}

static int artifact_order(const void *a, const void *b)
{
    return strcmp(*(const char *const *)a, *(const char *const *)b);
}

static int edge_order(const void *a, const void *b)
{
    const struct holy_resolved_edge *x = a, *y = b;
    int c = strcmp(x->consumer, y->consumer);
    return c ? c : strcmp(x->id, y->id);
}

static int collect_result(const struct local_item *local, const struct holy_solver_item *items,
                            size_t count, const int *selected, struct holy_resolution *out)
{
    size_t i, j, k;
    memcpy(out->root, local[0].identity.digest, sizeof out->root);
    out->artifacts = calloc(count, sizeof *out->artifacts);
    if (!out->artifacts) return 0;
    for (i = 0; i < count; ++i) if (selected[i]) {
        out->artifacts[out->artifact_count] = strdup(local[i].identity.digest);
        if (!out->artifacts[out->artifact_count]) return 0;
        ++out->artifact_count;
        for (j = 0; j < local[i].requirement_count; ++j) {
            struct holy_resolved_edge *next, *edge;
            const char *path = "-", *kind = "package", *target = NULL;
            size_t provider = count;
            for (k = 0; k < count; ++k)
                if (selected[k] && provides(&items[k], local[i].requirements[j].first)) {
                    provider = k;
                    break;
                }
            if (provider == count) return 0;
            for (k = 0; k < local[i].edge_count; ++k) {
                const struct elf_edge *e = &local[i].edges[k];
                if (e->requirement == j) { path = e->path; kind = e->kind; target = e->target; break; }
            }
            if (!target) {
                const char *original = local[i].original_requirements[j];
                if (!strncmp(original, "package-or:", 11)) {
                    for (k = 0; k < local[i].package_edge_count; ++k)
                        if (local[i].package_edges[k].requirement == j &&
                            package_edge_matches(&local[i], &local[i].package_edges[k],
                                                 &local[provider]) == 1) {
                            target = local[i].package_edges[k].name;
                            break;
                        }
                    if (!target) return 0;
                }
                else if (!strncmp(original, "package:", 8)) target = original + 8;
                else if (!strncmp(original, "file:", 5)) {
                    kind = "file";
                    target = original + 5;
                } else if (!strncmp(original, "command:", 8)) {
                    kind = "command";
                    target = original + 8;
                } else if (!strncmp(original, "soname:", 7)) {
                    kind = "soname";
                    target = original + 7;
                } else return 0;
            }
            if (out->edge_count >= 65536) return 0;
            next = realloc(out->edges, (out->edge_count + 1) * sizeof *next);
            if (!next) return 0;
            out->edges = next;
            edge = &next[out->edge_count++];
            memset(edge, 0, sizeof *edge);
            edge->consumer = strdup(local[i].identity.digest);
            edge->id = strdup(local[i].requirement_ids[j]);
            edge->path = strdup(path);
            edge->kind = strdup(kind);
            edge->target = strdup(target);
            edge->provider = strdup(local[provider].identity.digest);
            if (!edge->consumer || !edge->id || !edge->path || !edge->kind ||
                !edge->target || !edge->provider) return 0;
        }
    }
    qsort(out->artifacts, out->artifact_count, sizeof *out->artifacts, artifact_order);
    if (out->edge_count) qsort(out->edges, out->edge_count, sizeof *out->edges, edge_order);
    return 1;
}

static int resolve(const char *const *paths, size_t count, int json,
                    const char *generation, const char *choice,
                    struct holy_resolution *output, int all,
                    struct holy_missing_requirement *missing_output,
                    const char *const *skip_ids, size_t skip_count)
{
    struct local_item *local = NULL;
    struct holy_solver_item *items = NULL;
    int *selected = NULL, result = 6;
    size_t i, j, prepared = 0;
    int solved;
    const char *unresolved = NULL;
    size_t missing_consumer = 0, missing_index = 0;
    const char *unknown_context = NULL;
    const char *chosen_digest = NULL;
    char *chosen_id = NULL, *choice_capability = NULL;
    if (choice) {
        chosen_id = choice_requirement(choice, &chosen_digest);
        if (!chosen_id) { result = 2; goto done; }
    }
    if (!paths || !count || count > 10000) goto done;
    local = calloc(count, sizeof *local);
    items = calloc(count, sizeof *items);
    selected = calloc(count, sizeof *selected);
    if (!local || !items || !selected) goto done;
    for (i = 0; i < count; ++i) {
        char *snapshot;
        if (!paths[i]) goto done;
        snapshot = holy_stage_local(paths[i], "holy-resolve");
        if (!snapshot) goto done;
        prepared = i + 1;
        if (!holy_verify_visit(snapshot, collect_file_path, &local[i]) ||
            !holy_scan_collect(snapshot, &local[i].scan) ||
            !holy_package_identity(snapshot, &local[i].identity) ||
            strcmp(local[i].identity.os, "linux") ||
            !supported_metadata(snapshot)) {
            unlink(snapshot); free(snapshot); goto done;
        }
        if (!holy_provides_visit(snapshot, package_claim, &local[i])) {
            unlink(snapshot); free(snapshot); goto done;
        }
        local[i].capability = package_capability(local[i].identity.name);
        if (!local[i].capability ||
            !holy_deps_visit(snapshot, exact_requirement, &local[i])) {
            unlink(snapshot); free(snapshot); goto done;
        }
        for (j = 0; j < i; ++j)
            if (!strcmp(local[j].identity.digest, local[i].identity.digest)) {
                unlink(snapshot); free(snapshot); goto done;
            }
        items[i].id = local[i].identity.digest;
        if (!add_provide(&items[i], local[i].capability)) {
            unlink(snapshot); free(snapshot); goto done;
        }
        for (j = 0; j < local[i].claim_count; ++j)
            if (!add_provide(&items[i], local[i].claims[j].capability)) {
                unlink(snapshot); free(snapshot); goto done;
            }
        if (local[i].file_count)
            qsort(local[i].file_paths, local[i].file_count,
                  sizeof *local[i].file_paths, file_path_order);
        unlink(snapshot);
        free(snapshot);
    }
    if (!package_requirements(local, items, count)) goto done;
    for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.script_count; ++j) {
        const struct holy_scanned_script *script = &local[i].scan.scripts[j];
        if (script->kind != 1 || !literal_path(script->interpreter)) {
            fprintf(stderr, "holypkg: script-interpreter-decision consumer=%s interpreter=%s\n",
                    script->path, script->interpreter);
            result = 3;
            goto done;
        }
    }
    {
        int status = elf_requirements(local, items, count);
        if (status != 1) { if (status == 3) result = 3; goto done; }
    }
    for (i = 0; i < count; ++i) {
        items[i].requires = local[i].requirements;
        items[i].requires_count = local[i].requirement_count;
    }
    if (chosen_id) {
        size_t requirement = local[0].requirement_count, provider = count;
        char *replacement;
        for (j = 0; j < local[0].requirement_count; ++j)
            if (!strcmp(local[0].requirement_ids[j], chosen_id)) {
                requirement = j;
                break;
            }
        for (i = 0; i < count; ++i)
            if (!strcmp(local[i].identity.digest, chosen_digest)) {
                provider = i;
                break;
            }
        if (requirement == local[0].requirement_count || provider == count ||
            !provides(&items[provider], local[0].requirements[requirement].first)) {
            result = 3;
            goto done;
        }
        choice_capability = malloc(strlen(chosen_id) + 8);
        if (!choice_capability) goto done;
        sprintf(choice_capability, "choice:%s", chosen_id);
        replacement = strdup(choice_capability);
        if (!replacement) goto done;
        if (!add_provide(&items[provider], choice_capability)) { free(replacement); goto done; }
        free((char *)local[0].requirements[requirement].first);
        local[0].requirements[requirement].first = replacement;
    }
    solved = all ? holy_solve_exact_set(items, count, selected) :
                   holy_solve_exact_unique(items, count, items[0].id, selected);
    if (solved == 1) for (i = 0; i < count; ++i) if (selected[i]) {
        if (local[i].unsupported_id) {
            unresolved = local[i].unsupported_id;
            unknown_context = "unsupported-requirement";
            result = 3;
            goto done;
        }
        for (j = 0; j < local[i].requirement_count; ++j) {
            size_t k, matches = 0;
            for (k = 0; k < count; ++k)
                if (selected[k] && provides(&items[k], local[i].requirements[j].first)) ++matches;
            if (matches > 1) { result = 3; goto done; }
        }
        for (j = 0; j < local[i].edge_count; ++j) {
            const struct elf_edge *edge = &local[i].edges[j];
            size_t k;
            if (!edge->symbol) continue;
            for (k = 0; k < count; ++k)
                if (selected[k] && provides(&items[k], local[i].requirements[edge->requirement].first) &&
                    !symbol_context(&local[i], edge, &local[k], &items[k])) {
                    result = 3; goto done;
                }
        }
    }
    if (solved == 1) {
        size_t selected_count = 0;
        if (output && !collect_result(local, items, count, selected, output)) { result = 1; goto done; }
        if (json >= 0) report_edges(local, items, count, selected, json);
        for (i = 0; i < count; ++i) if (selected[i]) {
            if (json > 0)
                printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"selected\",\"sha256\":\"%s\"}\n",
                       local[i].identity.digest);
            else if (!json) printf("selected %s\n", local[i].identity.digest);
            ++selected_count;
        }
        if (json > 0) {
            printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"summary\",\"count\":%zu", selected_count);
            if (generation) printf(",\"generation\":\"%s\"", generation);
            puts("}");
        } else if (!json && generation) printf("generation %s\n", generation);
        result = 0;
    } else if (solved == 3) result = 3;
    else if (solved == 2) {
        result = 4;
        unresolved = missing_requirement(local, items, count, skip_ids, skip_count,
                                         &missing_consumer, &missing_index);
        if (missing_output && unresolved &&
            !capture_missing(&local[missing_consumer], missing_index,
                             missing_output)) result = 3;
        if (unresolved) for (j = 0; j < local[missing_consumer].edge_count; ++j) {
                const struct elf_edge *edge = &local[missing_consumer].edges[j];
                if (edge->requirement != missing_index) continue;
                if (edge->symbol) unknown_context = "unknown-symbol-scope";
                else if (!strcmp(edge->kind, "interpreter")) unknown_context = "unknown-interpreter-context";
                if (unknown_context) result = 3;
        }
    }
done:
    if (result && output) holy_resolution_free(output);
    if (json >= 0 && (result == 3 || result == 4) && prepared == count && local && items)
        report_edges(local, items, count, NULL, json);
    if (result && !missing_output) fprintf(stderr, "holypkg: local resolution %s\n",
                        result == 2 ? "has an invalid choice" :
                        result == 3 ? "needs provider choice" :
                        result == 4 ? "has a dependency conflict" :
                        "requires unsupported data or failed");
    if (unresolved && !missing_output)
        fprintf(stderr, "holypkg: unresolved requirement %s\n", unresolved);
    if (result && json > 0) {
        if (unresolved)
            printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\",\"requirement\":\"%s\"}\n",
                   unknown_context ? unknown_context : "dependency-conflict", unresolved);
        else
            printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\"}\n",
                   result == 2 ? "invalid-query" :
                   result == 3 ? "decision-required" :
                   result == 4 ? "dependency-conflict" : "unsupported-input");
    }
    for (i = 0; i < prepared; ++i) {
        for (j = 0; j < local[i].requirement_count; ++j)
            free((char *)local[i].requirements[j].first);
        for (j = 0; j < local[i].requirement_count; ++j)
            free(local[i].requirement_ids[j]);
        for (j = 0; j < local[i].requirement_count; ++j)
            free(local[i].original_requirements[j]);
        free(local[i].requirements);
        free(local[i].requirement_ids);
        free(local[i].original_requirements);
        free(local[i].capability);
        free(local[i].unsupported_id);
        for (j = 0; j < local[i].edge_count; ++j)
            free(local[i].edges[j].owned_target);
        free(local[i].edges);
        for (j = 0; j < local[i].package_edge_count; ++j) {
            free(local[i].package_edges[j].kind);
            free(local[i].package_edges[j].name);
            free(local[i].package_edges[j].arch); free(local[i].package_edges[j].libc);
            free(local[i].package_edges[j].relation); free(local[i].package_edges[j].version);
        }
        free(local[i].package_edges);
        for (j = 0; j < local[i].claim_count; ++j) {
            free(local[i].claims[j].capability); free(local[i].claims[j].version);
        }
        free(local[i].claims);
        for (j = 0; j < local[i].file_count; ++j) {
            free(local[i].file_paths[j].path);
            free(local[i].file_paths[j].link);
        }
        free(local[i].file_paths);
        holy_scan_free(&local[i].scan);
        holy_package_identity_free(&local[i].identity);
    }
    if (items) for (i = 0; i < count; ++i) {
        for (j = 0; j < items[i].provides_count; ++j) free((void *)items[i].provides[j]);
        free((void *)items[i].provides);
    }
    free(items); free(local); free(selected);
    free(chosen_id); free(choice_capability);
    return result;
}

int holy_resolve_local(const char *const *paths, size_t count, int json,
                       const char *generation, const char *choice)
{
    return resolve(paths, count, json, generation, choice, NULL, 0, NULL, NULL, 0);
}

int holy_resolve_collect(const char *const *paths, size_t count,
                          const char *choice, struct holy_resolution *result)
{
    memset(result, 0, sizeof *result);
    return resolve(paths, count, -1, NULL, choice, result, 0, NULL, NULL, 0);
}

int holy_resolve_collect_set(const char *const *paths, size_t count,
                              struct holy_resolution *result)
{
    memset(result, 0, sizeof *result);
    return resolve(paths, count, -1, NULL, NULL, result, 1, NULL, NULL, 0);
}

void holy_missing_requirement_free(struct holy_missing_requirement *missing)
{
    free(missing->consumer);
    free(missing->id);
    free(missing->kind);
    free(missing->name);
    free(missing->path);
    memset(missing, 0, sizeof *missing);
}

int holy_resolve_missing(const char *const *paths, size_t count,
                         const char *const *skip_ids, size_t skip_count,
                         struct holy_missing_requirement *missing)
{
    memset(missing, 0, sizeof *missing);
    return resolve(paths, count, -1, NULL, NULL, NULL, 0, missing,
                   skip_ids, skip_count);
}

void holy_resolution_free(struct holy_resolution *result)
{
    size_t i;
    for (i = 0; i < result->artifact_count; ++i) free(result->artifacts[i]);
    for (i = 0; i < result->edge_count; ++i) {
        struct holy_resolved_edge *e = &result->edges[i];
        free(e->consumer); free(e->id); free(e->provider);
        free(e->path); free(e->kind); free(e->target);
    }
    free(result->artifacts); free(result->edges);
    memset(result, 0, sizeof *result);
}

static int record_token(FILE *stream, const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    size_t encoded = 2;
    long offset = ftell(stream);
    if (!value || offset < 0 || offset > 16 * 1024 * 1024 - 2) return 0;
    for (; *p; ++p) {
        encoded += (*p < 32 || *p >= 127) ? 4 : (*p == '"' || *p == '\\') ? 2 : 1;
        if (encoded > 16 * 1024 * 1024 - (size_t)offset) return 0;
    }
    if (fputc('"', stream) == EOF) return 0;
    for (p = (const unsigned char *)value; *p; ++p) {
        if (*p == '"' || *p == '\\') {
            if (fputc('\\', stream) == EOF || fputc(*p, stream) == EOF) return 0;
        } else if (*p < 32 || *p >= 127) {
            if (fprintf(stream, "\\x%02x", *p) < 0) return 0;
        } else if (fputc(*p, stream) == EOF) return 0;
    }
    return fputc('"', stream) != EOF;
}

int holy_resolution_record(const struct holy_resolution *result, char **record, size_t *length)
{
    FILE *stream;
    size_t i, j;
    int ok = 0;
    *record = NULL; *length = 0;
    if (!result->artifact_count || strlen(result->root) != 64) return 0;
    stream = open_memstream(record, length);
    if (!stream) return 0;
    if (fprintf(stream, "format holy-resolution-1\nscope artifact-candidates\nroot %s\n", result->root) < 0) goto done;
    for (i = 0; i < result->artifact_count; ++i)
        if (fprintf(stream, "artifact %s\n", result->artifacts[i]) < 0) goto done;
    for (i = 0; i < result->edge_count; ++i) {
        const struct holy_resolved_edge *e = &result->edges[i];
        const char *fields[] = {e->consumer, e->id, e->provider, e->path, e->kind, e->target};
        if (fputs("edge", stream) == EOF) goto done;
        for (j = 0; j < sizeof fields / sizeof *fields; ++j)
            if (fputc(' ', stream) == EOF || !record_token(stream, fields[j])) goto done;
        if (fputc('\n', stream) == EOF) goto done;
    }
    ok = 1;
done:
    if (fclose(stream)) ok = 0;
    if (*length > 16 * 1024 * 1024) ok = 0;
    if (!ok) { free(*record); *record = NULL; *length = 0; }
    return ok;
}