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

file src/repo.c

#define _POSIX_C_SOURCE 200809L
#include "repo.h"
#include "config.h"
#include "fetch.h"
#include "cache.h"
#include "deps.h"
#include "extract.h"
#include "provides.h"
#include "resolve.h"
#include "package.h"
#include "scan.h"
#include "stage.h"
#include "verify.h"
#include "sign.h"
#include "version.h"
#include "../backends/pacman.h"
#include "../backends/deb-version.h"
#include "../backends/apk-version.h"
#include "../backends/xbps-version.h"

#include <dirent.h>
#include <errno.h>
#include <elf.h>
#include <fcntl.h>
#include <inttypes.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static int same_identity(const struct holy_package_identity *a,
                         const struct holy_package_identity *b)
{
    return !strcmp(a->name, b->name) && !strcmp(a->version, b->version) &&
           !strcmp(a->release, b->release) && !strcmp(a->os, b->os) &&
           !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc);
}

static int same_slot(const struct holy_package_identity *a,
                     const struct holy_package_identity *b)
{
    return !strcmp(a->name, b->name) && !strcmp(a->os, b->os) &&
           !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc);
}

struct claim {
    char *kind, *name, *arch, *libc, *version, *evidence;
};

struct indexed_requirement { char *fields[10]; };
struct soname_fact { char *name, *arch, *libc, *path; };
struct version_fact { char *path, *name; };
struct export_fact {
    char *path, *name, *version;
    unsigned binding, type, visibility, hidden;
};

struct object {
    char *filename;
    struct holy_package_identity identity;
    int provider_match;
    struct claim *claims;
    size_t claim_count;
    struct indexed_requirement *requirements;
    size_t requirement_count;
    struct soname_fact *sonames;
    size_t soname_count;
    struct version_fact *versions;
    size_t version_count;
    struct export_fact *exports;
    size_t export_count;
    char **files;
    size_t file_count;
};

static void free_files(struct object *object)
{
    size_t i;
    for (i = 0; i < object->file_count; ++i) free(object->files[i]);
    free(object->files);
}

static int add_file(struct object *object, const char *path)
{
    char **next;
    char *copy;
    if (object->file_count == (size_t)-1 / sizeof *object->files) return 0;
    copy = strdup(path);
    if (!copy) return 0;
    next = realloc(object->files, (object->file_count + 1) * sizeof *object->files);
    if (!next) { free(copy); return 0; }
    object->files = next;
    object->files[object->file_count++] = copy;
    return 1;
}

static int collect_file(void *opaque, const struct holy_manifest_entry *entry)
{
    return entry->directory || add_file(opaque, entry->path);
}

struct file_cursor { struct object *object; size_t index; };

static int compare_file(void *opaque, const struct holy_manifest_entry *entry)
{
    struct file_cursor *cursor = opaque;
    if (entry->directory) return 1;
    return cursor->index < cursor->object->file_count &&
           !strcmp(cursor->object->files[cursor->index++], entry->path);
}

static void free_claims(struct object *object)
{
    size_t i;
    for (i = 0; i < object->claim_count; ++i) {
        struct claim *c = &object->claims[i];
        free(c->kind); free(c->name); free(c->arch);
        free(c->libc); free(c->version); free(c->evidence);
    }
    free(object->claims);
}

static int add_claim(struct object *object, const char *kind, const char *name,
                     const char *arch, const char *libc, const char *version,
                     const char *evidence)
{
    struct claim c = {0}, *next;
    if (object->claim_count == (size_t)-1 / sizeof *object->claims) return 0;
    c.kind = strdup(kind); c.name = strdup(name); c.arch = strdup(arch);
    c.libc = strdup(libc); c.version = strdup(version);
    c.evidence = strdup(evidence);
    if (!c.kind || !c.name || !c.arch || !c.libc || !c.version || !c.evidence) {
        free(c.kind); free(c.name); free(c.arch);
        free(c.libc); free(c.version); free(c.evidence);
        return 0;
    }
    next = realloc(object->claims,
                   (object->claim_count + 1) * sizeof *object->claims);
    if (!next) {
        free(c.kind); free(c.name); free(c.arch);
        free(c.libc); free(c.version); free(c.evidence);
        return 0;
    }
    object->claims = next;
    object->claims[object->claim_count++] = c;
    return 1;
}

static int collect_claim(void *opaque, const char *kind, const char *name,
                         const char *arch, const char *libc,
                         const char *version, const char *evidence)
{
    return add_claim(opaque, kind, name, arch, libc, version, evidence);
}

struct claim_cursor { struct object *object; size_t index; };

static int compare_claim(void *opaque, const char *kind, const char *name,
                         const char *arch, const char *libc,
                         const char *version, const char *evidence)
{
    struct claim_cursor *cursor = opaque;
    struct claim *c;
    if (cursor->index == cursor->object->claim_count) return 0;
    c = &cursor->object->claims[cursor->index++];
    return !strcmp(c->kind, kind) && !strcmp(c->name, name) &&
           !strcmp(c->arch, arch) && !strcmp(c->libc, libc) &&
           !strcmp(c->version, version) && !strcmp(c->evidence, evidence);
}

static void free_requirements(struct object *object)
{
    size_t i, j;
    for (i = 0; i < object->requirement_count; ++i)
        for (j = 0; j < 10; ++j) free(object->requirements[i].fields[j]);
    free(object->requirements);
}

static int add_requirement(struct object *object, const char *fields[10])
{
    struct indexed_requirement item = {{0}}, *next;
    size_t i;
    if (object->requirement_count >= 1024 * 1024 / 11) return 0;
    for (i = 0; i < 10; ++i) {
        item.fields[i] = strdup(fields[i]);
        if (!item.fields[i]) goto fail;
    }
    next = realloc(object->requirements,
                   (object->requirement_count + 1) * sizeof *next);
    if (!next) goto fail;
    object->requirements = next;
    object->requirements[object->requirement_count++] = item;
    return 1;
fail:
    for (i = 0; i < 10; ++i) free(item.fields[i]);
    return 0;
}

static int collect_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)
{
    const char *fields[] = {id, consumer, kind, name, arch, libc,
                            relation, version, original, evidence};
    return add_requirement(opaque, fields);
}

struct requirement_cursor { struct object *object; size_t index; };

static int compare_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 requirement_cursor *cursor = opaque;
    const char *fields[] = {id, consumer, kind, name, arch, libc,
                            relation, version, original, evidence};
    size_t i;
    if (cursor->index == cursor->object->requirement_count) return 0;
    for (i = 0; i < 10; ++i)
        if (strcmp(cursor->object->requirements[cursor->index].fields[i], fields[i]))
            return 0;
    ++cursor->index;
    return 1;
}

static void free_sonames(struct object *object)
{
    size_t i;
    for (i = 0; i < object->soname_count; ++i) {
        free(object->sonames[i].name); free(object->sonames[i].arch);
        free(object->sonames[i].libc); free(object->sonames[i].path);
    }
    free(object->sonames);
}

static int add_soname(struct object *object, const char *name,
                      const char *arch, const char *libc, const char *path)
{
    struct soname_fact fact = {0}, *next;
    if (object->soname_count >= 100000) return 0;
    fact.name = strdup(name); fact.arch = strdup(arch);
    fact.libc = strdup(libc); fact.path = strdup(path);
    if (!fact.name || !fact.arch || !fact.libc || !fact.path) goto fail;
    next = realloc(object->sonames, (object->soname_count + 1) * sizeof *next);
    if (!next) goto fail;
    object->sonames = next;
    object->sonames[object->soname_count++] = fact;
    return 1;
fail:
    free(fact.name); free(fact.arch); free(fact.libc); free(fact.path);
    return 0;
}

static int soname_order(const void *left, const void *right)
{
    const struct soname_fact *a = left, *b = right;
    int cmp = strcmp(a->path, b->path);
    return cmp ? cmp : strcmp(a->name, b->name);
}

static void free_versions(struct object *object)
{
    size_t i;
    for (i = 0; i < object->version_count; ++i) {
        free(object->versions[i].path);
        free(object->versions[i].name);
    }
    free(object->versions);
}

static int version_order(const void *left, const void *right)
{
    const struct version_fact *a = left, *b = right;
    int order = strcmp(a->path, b->path);
    return order ? order : strcmp(a->name, b->name);
}

static int add_version(struct object *object, const char *path, const char *name)
{
    struct version_fact fact = {0}, *next;
    if (object->version_count >= 100000) return 0;
    fact.path = strdup(path); fact.name = strdup(name);
    if (!fact.path || !fact.name) goto fail;
    next = realloc(object->versions, (object->version_count + 1) * sizeof *next);
    if (!next) goto fail;
    object->versions = next;
    object->versions[object->version_count++] = fact;
    return 1;
fail:
    free(fact.path); free(fact.name);
    return 0;
}

static void free_exports(struct object *object)
{
    size_t i;
    for (i = 0; i < object->export_count; ++i) {
        free(object->exports[i].path);
        free(object->exports[i].name);
        free(object->exports[i].version);
    }
    free(object->exports);
}

static int export_order(const void *left, const void *right)
{
    const struct export_fact *a = left, *b = right;
    int order = strcmp(a->path, b->path);
    if (!order) order = strcmp(a->name, b->name);
    if (!order) order = strcmp(a->version, b->version);
    if (!order && a->binding != b->binding) order = a->binding < b->binding ? -1 : 1;
    if (!order && a->type != b->type) order = a->type < b->type ? -1 : 1;
    if (!order && a->visibility != b->visibility)
        order = a->visibility < b->visibility ? -1 : 1;
    if (!order && a->hidden != b->hidden) order = a->hidden < b->hidden ? -1 : 1;
    return order;
}

static int add_export(struct object *object, const char *path,
                      const struct holy_elf_symbol *symbol)
{
    struct export_fact fact = {0}, *next;
    if (object->export_count >= 500000) return 0;
    fact.path = strdup(path);
    fact.name = strdup(symbol->name);
    fact.version = strdup(symbol->version ? symbol->version : "");
    fact.binding = symbol->binding; fact.type = symbol->type;
    fact.visibility = symbol->visibility;
    fact.hidden = !!symbol->version_hidden;
    if (!fact.path || !fact.name || !fact.version) goto fail;
    next = realloc(object->exports, (object->export_count + 1) * sizeof *next);
    if (!next) goto fail;
    object->exports = next;
    object->exports[object->export_count++] = fact;
    return 1;
fail:
    free(fact.path); free(fact.name); free(fact.version);
    return 0;
}

static int collect_sonames(const char *snapshot, struct object *object)
{
    struct holy_scan_result scan = {0};
    size_t i;
    int ok = holy_scan_collect(snapshot, &scan);
    for (i = 0; ok && i < scan.count; ++i) {
        const struct holy_scanned_file *file = &scan.files[i];
        if (file->elf.type == ET_DYN && !(file->elf.flags1 & DF_1_PIE) &&
            file->elf.soname) {
            size_t j;
            ok = add_soname(object, file->elf.soname,
                            holy_elf_machine(&file->elf), file->runtime,
                            file->path);
            for (j = 0; ok && j < file->elf.defined_version_count; ++j)
                ok = add_version(object, file->path,
                                 file->elf.defined_versions[j].name);
            for (j = 0; ok && j < file->elf.symbol_count; ++j) {
                const struct holy_elf_symbol *s = &file->elf.symbols[j];
                if (s->section && s->name[0] &&
                    (s->binding == STB_GLOBAL || s->binding == STB_WEAK ||
                     s->binding == STB_GNU_UNIQUE) &&
                    (s->visibility == STV_DEFAULT || s->visibility == STV_PROTECTED))
                    ok = add_export(object, file->path, s);
            }
        }
    }
    holy_scan_free(&scan);
    if (ok && object->soname_count)
        qsort(object->sonames, object->soname_count,
              sizeof *object->sonames, soname_order);
    if (ok && object->version_count) {
        qsort(object->versions, object->version_count,
              sizeof *object->versions, version_order);
        for (i = 1; i < object->version_count; ++i)
            if (!version_order(&object->versions[i - 1], &object->versions[i])) return 0;
    }
    if (ok && object->export_count)
        qsort(object->exports, object->export_count,
              sizeof *object->exports, export_order);
    return ok;
}

static int compare_sonames(const char *snapshot, struct object *object,
                           int versions, int exports)
{
    struct object actual = {0};
    size_t i;
    int ok = collect_sonames(snapshot, &actual) &&
             actual.soname_count == object->soname_count;
    for (i = 0; ok && i < actual.soname_count; ++i) {
        const struct soname_fact *a = &actual.sonames[i], *b = &object->sonames[i];
        ok = !strcmp(a->name, b->name) && !strcmp(a->arch, b->arch) &&
             !strcmp(a->libc, b->libc) && !strcmp(a->path, b->path);
    }
    if (versions && actual.version_count != object->version_count) ok = 0;
    for (i = 0; ok && versions && i < actual.version_count; ++i)
        ok = !strcmp(actual.versions[i].path, object->versions[i].path) &&
             !strcmp(actual.versions[i].name, object->versions[i].name);
    if (exports && actual.export_count != object->export_count) ok = 0;
    for (i = 0; ok && exports && i < actual.export_count; ++i)
        ok = !export_order(&actual.exports[i], &object->exports[i]);
    free_sonames(&actual);
    free_versions(&actual);
    free_exports(&actual);
    return ok;
}

static int compare_names(const void *left, const void *right)
{
    const struct object *a = left, *b = right;
    return strcmp(a->filename, b->filename);
}

static int quote(FILE *fp, const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    if (fputc('"', fp) == EOF) return 0;
    for (; *p; ++p) {
        if (*p == '"' || *p == '\\') {
            if (fputc('\\', fp) == EOF || fputc(*p, fp) == EOF) return 0;
        } else if (*p <= 32 || *p >= 127) {
            if (fprintf(fp, "\\x%02x", (unsigned int)*p) < 0) return 0;
        } else if (fputc(*p, fp) == EOF) return 0;
    }
    return fputc('"', fp) != EOF;
}

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

static void candidate_json(const struct object *object)
{
    const struct holy_package_identity *id = &object->identity;
    fputs("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"candidate\",\"name\":", stdout);
    json_string(id->name);
    fputs(",\"version\":", stdout);
    json_string(id->version);
    fputs(",\"release\":", stdout);
    json_string(id->release);
    fputs(",\"os\":", stdout);
    json_string(id->os);
    fputs(",\"arch\":", stdout);
    json_string(id->arch);
    fputs(",\"libc\":", stdout);
    json_string(id->libc);
    fputs(",\"filename\":", stdout);
    json_string(object->filename);
    printf(",\"sha256\":\"%s\",\"size\":%" PRIu64 "}\n", id->digest, id->size);
}

static int record(FILE *fp, const struct object *object)
{
    const struct holy_package_identity *id = &object->identity;
    const char *values[] = {id->name, id->version, id->release, id->os,
                            id->arch, id->libc, object->filename};
    size_t i;
    if (fputs("package", fp) == EOF) return 0;
    for (i = 0; i < sizeof values / sizeof *values; ++i) {
        if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0;
    }
    if (fprintf(fp, " %s %" PRIu64 " ", id->digest, id->size) < 0) return 0;
    return quote(fp, id->version_family ? id->version_family : "-") &&
           fputc('\n', fp) != EOF;
}

static int claim_record(FILE *fp, const struct object *object,
                        const struct claim *claim)
{
    const char *values[] = {claim->kind, claim->name, claim->arch,
                            claim->libc, claim->version, claim->evidence};
    size_t i;
    if (fprintf(fp, "claim %s", object->identity.digest) < 0) return 0;
    for (i = 0; i < sizeof values / sizeof *values; ++i)
        if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0;
    return fputc('\n', fp) != EOF;
}

static int requirement_record(FILE *fp, const struct object *object,
                              const struct indexed_requirement *requirement)
{
    size_t i;
    if (fprintf(fp, "require %s", object->identity.digest) < 0) return 0;
    for (i = 0; i < 10; ++i)
        if (fputc(' ', fp) == EOF || !quote(fp, requirement->fields[i])) return 0;
    return fputc('\n', fp) != EOF;
}

static int soname_record(FILE *fp, const struct object *object,
                         const struct soname_fact *fact)
{
    const char *values[] = {fact->name, fact->arch, fact->libc, fact->path};
    size_t i;
    if (fprintf(fp, "soname %s", object->identity.digest) < 0) return 0;
    for (i = 0; i < 4; ++i)
        if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0;
    return fputc('\n', fp) != EOF;
}

static int version_record(FILE *fp, const struct object *object,
                          const struct version_fact *fact)
{
    return fprintf(fp, "elf-version %s ", object->identity.digest) >= 0 &&
           quote(fp, fact->path) && fputc(' ', fp) != EOF &&
           quote(fp, fact->name) && fputc('\n', fp) != EOF;
}

static int export_record(FILE *fp, const struct object *object,
                         const struct export_fact *fact)
{
    return fprintf(fp, "elf-export %s ", object->identity.digest) >= 0 &&
           quote(fp, fact->path) && fputc(' ', fp) != EOF &&
           quote(fp, fact->name) && fputc(' ', fp) != EOF &&
           quote(fp, fact->version) &&
           fprintf(fp, " %u %u %u %u\n", fact->binding, fact->type,
                   fact->visibility, fact->hidden) >= 0;
}

static int file_record(FILE *fp, const struct object *object, const char *path)
{
    return fprintf(fp, "file %s ", object->identity.digest) >= 0 &&
           quote(fp, path) && fputc('\n', fp) != EOF;
}

static int digest_file(const char *path, char hex[65])
{
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    unsigned char hash[32], buffer[65536];
    unsigned int length;
    size_t n, i;
    FILE *fp = fopen(path, "rb");
    int ok = fp && ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1;
    while (ok && (n = fread(buffer, 1, sizeof buffer, fp)) > 0)
        ok = EVP_DigestUpdate(ctx, buffer, n) == 1;
    if (ok) ok = !ferror(fp) && EVP_DigestFinal_ex(ctx, hash, &length) == 1 &&
                 length == sizeof hash;
    if (ok) {
        for (i = 0; i < sizeof hash; ++i)
            snprintf(hex + i * 2, 3, "%02x", hash[i]);
        hex[64] = '\0';
    }
    if (fp) fclose(fp);
    EVP_MD_CTX_free(ctx);
    return ok;
}

static int read_current(int dir, char digest[65])
{
    char line[72];
    struct stat st;
    size_t i;
    int fd = openat(dir, "current", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (fd < 0) return errno == ENOENT ? 0 : -1;
    if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size != sizeof line ||
        pread(fd, line, sizeof line, 0) != sizeof line ||
        memcmp(line, "sha256 ", 7) || line[71] != '\n') {
        close(fd);
        return -1;
    }
    close(fd);
    for (i = 0; i < 64; ++i)
        if (!((line[i + 7] >= '0' && line[i + 7] <= '9') ||
              (line[i + 7] >= 'a' && line[i + 7] <= 'f'))) return -1;
    memcpy(digest, line + 7, 64);
    digest[64] = '\0';
    return 1;
}

int holy_repo_index(const char *directory)
{
    struct object *objects = NULL;
    DIR *listing = NULL;
    struct dirent *entry;
    struct stat st;
    size_t count = 0, i, j;
    int dir = -1, temp = -1, ok = 0;
    FILE *stream = NULL;
    char temporary[43] = {0};

    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || flock(dir, LOCK_EX) < 0) goto done;
    listing = fdopendir(dup(dir));
    if (!listing) goto done;
    errno = 0;
    while ((entry = readdir(listing)) != NULL) {
        size_t length = strlen(entry->d_name);
        struct object *next;
        if (length < 6 || strcmp(entry->d_name + length - 5, ".holy")) continue;
        if (count == (size_t)-1 / sizeof *objects) goto done;
        next = realloc(objects, (count + 1) * sizeof *objects);
        if (!next) goto done;
        objects = next;
        memset(&objects[count], 0, sizeof *objects);
        objects[count].filename = strdup(entry->d_name);
        if (!objects[count].filename) goto done;
        ++count;
        errno = 0;
    }
    if (errno) goto done;
    closedir(listing);
    listing = NULL;
    if (count) qsort(objects, count, sizeof *objects, compare_names);
    for (i = 0; i < count; ++i) {
        int input = openat(dir, objects[i].filename,
                           O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
        char *snapshot;
        if (input < 0) goto done;
        snapshot = holy_stage_fd(input, "holy-index");
        close(input);
        if (!snapshot) goto done;
        if (!holy_verify_with_output(snapshot, 0) ||
            !holy_scan_local_with_output(snapshot, 0) ||
            !holy_deps_local_with_output(snapshot, 0) ||
            !holy_provides_local(snapshot, 0) ||
            !holy_package_identity(snapshot, &objects[i].identity) ||
            !holy_provides_visit(snapshot, collect_claim, &objects[i]) ||
            !holy_deps_visit(snapshot, collect_requirement, &objects[i]) ||
            !collect_sonames(snapshot, &objects[i]) ||
            !holy_verify_visit(snapshot, collect_file, &objects[i])) {
            unlink(snapshot);
            free(snapshot);
            goto done;
        }
        unlink(snapshot);
        free(snapshot);
        for (j = 0; j < i; ++j) {
            struct holy_package_identity *a = &objects[i].identity;
            struct holy_package_identity *b = &objects[j].identity;
            if (same_identity(a, b)) {
                fprintf(stderr, "holypkg: duplicate package identity\n");
                goto done;
            }
        }
    }
    if (fstatat(dir, "index", &st, AT_SYMLINK_NOFOLLOW) == 0) {
        if (!S_ISREG(st.st_mode)) {
            fprintf(stderr, "holypkg: index is not a regular file\n");
            goto done;
        }
    } else if (errno != ENOENT) {
        goto done;
    }
    temp = holy_temporary_at(dir, temporary);
    if (temp < 0) goto done;
    stream = fdopen(temp, "w");
    if (!stream) goto done;
    temp = -1;
    if (fputs("format holy-index-prototype-8\ncoverage files complete\ncoverage dependencies complete\ncoverage elf-sonames complete\ncoverage elf-versions complete\ncoverage elf-exports complete\n", stream) == EOF) goto done;
    for (i = 0; i < count; ++i) {
        if (!record(stream, &objects[i])) goto done;
        for (j = 0; j < objects[i].claim_count; ++j)
            if (!claim_record(stream, &objects[i], &objects[i].claims[j])) goto done;
        for (j = 0; j < objects[i].requirement_count; ++j)
            if (!requirement_record(stream, &objects[i], &objects[i].requirements[j])) goto done;
        for (j = 0; j < objects[i].soname_count; ++j)
            if (!soname_record(stream, &objects[i], &objects[i].sonames[j])) goto done;
        for (j = 0; j < objects[i].version_count; ++j)
            if (!version_record(stream, &objects[i], &objects[i].versions[j])) goto done;
        for (j = 0; j < objects[i].export_count; ++j)
            if (!export_record(stream, &objects[i], &objects[i].exports[j])) goto done;
        for (j = 0; j < objects[i].file_count; ++j)
            if (!file_record(stream, &objects[i], objects[i].files[j])) goto done;
    }
    if (fflush(stream) || ftello(stream) < 0 ||
        ftello(stream) > 128LL * 1024 * 1024 ||
        fchmod(fileno(stream), 0644) || fsync(fileno(stream)))
        goto done;
    if (fclose(stream)) { stream = NULL; goto done; }
    stream = NULL;
    if (renameat(dir, temporary, dir, "index") || fsync(dir)) goto done;
    temporary[0] = '\0';
    printf("indexed %zu packages\n", count);
    ok = 1;
done:
    if (!ok) fprintf(stderr, "holypkg: repository index incomplete\n");
    if (stream) fclose(stream);
    if (temp >= 0) close(temp);
    if (temporary[0] && dir >= 0) unlinkat(dir, temporary, 0);
    if (listing) closedir(listing);
    for (i = 0; i < count; ++i) {
        free(objects[i].filename);
        holy_package_identity_free(&objects[i].identity);
        free_claims(&objects[i]);
        free_requirements(&objects[i]);
        free_sonames(&objects[i]);
        free_versions(&objects[i]);
        free_exports(&objects[i]);
        free_files(&objects[i]);
    }
    free(objects);
    if (dir >= 0) close(dir);
    return ok;
}

static int parse_record(char **v, size_t n, struct object *object)
{
    unsigned long long size;
    char *end;
    size_t i;
    const char *filename;
    if ((n != 10 && n != 11) || strcmp(v[0], "package") ||
        (n == 11 && (!v[10][0] || strlen(v[10]) > 128))) return 0;
    filename = v[7];
    i = strlen(filename);
    if (i < 6 || strcmp(filename + i - 5, ".holy") || strchr(filename, '/') ||
        strlen(v[8]) != 64 || !v[1][0] || !v[2][0] || !v[3][0]) return 0;
    for (i = 0; i < 64; ++i)
        if (!((v[8][i] >= '0' && v[8][i] <= '9') ||
              (v[8][i] >= 'a' && v[8][i] <= 'f'))) return 0;
    errno = 0;
    size = strtoull(v[9], &end, 10);
    if (errno || !v[9][0] || *end || v[9][0] == '-' || !size) return 0;
    object->filename = v[7]; v[7] = NULL;
    object->identity.name = v[1]; v[1] = NULL;
    object->identity.version = v[2]; v[2] = NULL;
    object->identity.release = v[3]; v[3] = NULL;
    object->identity.os = v[4]; v[4] = NULL;
    object->identity.arch = v[5]; v[5] = NULL;
    object->identity.libc = v[6]; v[6] = NULL;
    if (n == 11 && strcmp(v[10], "-")) {
        object->identity.version_family = v[10]; v[10] = NULL;
    }
    memcpy(object->identity.digest, v[8], 65);
    object->identity.size = size;
    return 1;
}

static int parse_claim(char **v, size_t n, struct object *object)
{
    size_t i;
    if (n != 8 || strcmp(v[0], "claim") ||
        strcmp(v[1], object->identity.digest) ||
        !holy_provides_claim_valid(v[2], v[3], v[4], v[5], v[6], v[7]))
        return 0;
    for (i = 0; i < object->claim_count; ++i) {
        const struct claim *c = &object->claims[i];
        if (!strcmp(c->kind, v[2]) && !strcmp(c->name, v[3]) &&
            !strcmp(c->arch, v[4]) && !strcmp(c->libc, v[5]) &&
            !strcmp(c->version, v[6])) return 0;
    }
    return add_claim(object, v[2], v[3], v[4], v[5], v[6], v[7]);
}

static int parse_requirement(char **v, size_t n, struct object *object)
{
    size_t i;
    const char *fields[10];
    if (n != 12 || strcmp(v[0], "require") ||
        strcmp(v[1], object->identity.digest) ||
        !holy_deps_record_valid(v[2], v[3], v[4], v[5], v[6], v[7],
                                v[8], v[9], v[10], v[11])) return 0;
    for (i = 0; i < object->requirement_count; ++i)
        if (!strcmp(object->requirements[i].fields[0], v[2])) return 0;
    for (i = 0; i < 10; ++i) fields[i] = v[i + 2];
    return add_requirement(object, fields);
}

static int safe_target_path(const char *path)
{
    size_t length = strlen(path);
    char *archive_path;
    int valid;
    if (!length || length > (size_t)-1 - 6 || path[length - 1] == '/') return 0;
    archive_path = malloc(length + 6);
    if (!archive_path) return 0;
    memcpy(archive_path, "DATA/", 5);
    memcpy(archive_path + 5, path, length + 1);
    valid = holy_safe_archive_path(archive_path);
    free(archive_path);
    return valid;
}

static int parse_file(char **v, size_t n, struct object *object)
{
    if (n != 3 || strcmp(v[0], "file") ||
        strcmp(v[1], object->identity.digest) ||
        !safe_target_path(v[2]) ||
        (object->file_count && strcmp(object->files[object->file_count - 1], v[2]) >= 0))
        return 0;
    return add_file(object, v[2]);
}

static int parse_soname(char **v, size_t n, struct object *object)
{
    struct soname_fact current;
    if (n != 6 || strcmp(v[0], "soname") ||
        strcmp(v[1], object->identity.digest) || !v[2][0] ||
        (strcmp(v[3], "x86") && strcmp(v[3], "x86_64")) ||
        (strcmp(v[4], "glibc") && strcmp(v[4], "musl")) ||
        !safe_target_path(v[5])) return 0;
    current.name = v[2]; current.arch = v[3];
    current.libc = v[4]; current.path = v[5];
    if (object->soname_count &&
        soname_order(&object->sonames[object->soname_count - 1], &current) >= 0)
        return 0;
    return add_soname(object, v[2], v[3], v[4], v[5]);
}

static int parse_version(char **v, size_t n, struct object *object)
{
    struct version_fact current;
    size_t i;
    if (n != 4 || strcmp(v[0], "elf-version") ||
        strcmp(v[1], object->identity.digest) ||
        !safe_target_path(v[2]) || !v[3][0]) return 0;
    for (i = 0; i < object->soname_count; ++i)
        if (!strcmp(object->sonames[i].path, v[2])) break;
    if (i == object->soname_count) return 0;
    current.path = v[2]; current.name = v[3];
    if (object->version_count &&
        version_order(&object->versions[object->version_count - 1], &current) >= 0)
        return 0;
    return add_version(object, v[2], v[3]);
}

static int parse_export(char **v, size_t n, struct object *object)
{
    struct holy_elf_symbol symbol = {0};
    struct export_fact current = {0};
    unsigned *values[] = {&symbol.binding, &symbol.type, &symbol.visibility,
                          &current.hidden};
    size_t i;
    if (n != 9 || strcmp(v[0], "elf-export") ||
        strcmp(v[1], object->identity.digest) ||
        !safe_target_path(v[2]) || !v[3][0]) return 0;
    for (i = 0; i < object->soname_count; ++i)
        if (!strcmp(object->sonames[i].path, v[2])) break;
    if (i == object->soname_count) return 0;
    for (i = 0; i < 4; ++i) {
        char *end;
        unsigned long value;
        errno = 0;
        value = strtoul(v[5 + i], &end, 10);
        if (errno || !v[5 + i][0] || *end || value > 255) return 0;
        *values[i] = (unsigned)value;
    }
    if ((symbol.binding != STB_GLOBAL && symbol.binding != STB_WEAK &&
         symbol.binding != STB_GNU_UNIQUE) ||
        (symbol.visibility != STV_DEFAULT && symbol.visibility != STV_PROTECTED) ||
        current.hidden > 1) return 0;
    symbol.name = v[3]; symbol.version = v[4];
    symbol.version_hidden = (int)current.hidden;
    current.path = v[2]; current.name = v[3]; current.version = v[4];
    current.binding = symbol.binding; current.type = symbol.type;
    current.visibility = symbol.visibility;
    if (object->export_count &&
        export_order(&object->exports[object->export_count - 1], &current) > 0)
        return 0;
    return add_export(object, v[2], &symbol);
}

static int indexed_file(const struct object *object, const char *path)
{
    size_t low = 0, high = object->file_count;
    while (low < high) {
        size_t middle = low + (high - low) / 2;
        int cmp = strcmp(object->files[middle], path);
        if (!cmp) return 1;
        if (cmp < 0) low = middle + 1;
        else high = middle;
    }
    return 0;
}

struct or_match { const struct object *object; int found; };

static int indexed_version_matches(const struct object *object,
                                   const char *candidate, const char *relation,
                                   const char *version, int identity)
{
    const char *family = object->identity.version_family;
    const char *actual = candidate;
    char *joined = NULL;
    int order, compared;
    if (!strcmp(relation, "any")) return 1;
    if (!family || !candidate || !strcmp(candidate, "-")) return 0;
    if (identity && !strcmp(family, "xbps") && object->identity.release) {
        size_t a = strlen(candidate), b = strlen(object->identity.release);
        if (a > SIZE_MAX - b - 2 || !(joined = malloc(a + b + 2))) return 0;
        snprintf(joined, a + b + 2, "%s_%s", candidate, object->identity.release);
        actual = joined;
    }
    compared = !strcmp(family, "pacman") ?
        holy_pacman_version_compare(actual, version, &order) :
        !strcmp(family, "deb") ?
        holy_deb_version_compare(actual, version, &order) :
        !strcmp(family, "holy") ?
        holy_version_compare(actual, version, &order) :
        !strcmp(family, "apk") ?
        holy_apk_version_compare(actual, version, &order) :
        !strcmp(family, "xbps") ?
        holy_xbps_version_compare(actual, version, &order) : 0;
    free(joined);
    if (!compared) return 0;
    return !strcmp(relation, "eq") ? order == 0 :
           !strcmp(relation, "ge") ? order >= 0 :
           !strcmp(relation, "gt") ? order > 0 :
           !strcmp(relation, "le") ? order <= 0 :
           !strcmp(relation, "lt") && order < 0;
}

static int indexed_or_branch(void *opaque, const char *name,
                             const char *relation, const char *version)
{
    struct or_match *match = opaque;
    size_t i;
    if (!strcmp(match->object->identity.name, name) &&
        indexed_version_matches(match->object, match->object->identity.version,
                                relation, version, 1)) match->found = 1;
    for (i = 0; i < match->object->claim_count; ++i)
        if (!strcmp(match->object->claims[i].kind, "package") &&
            !strcmp(match->object->claims[i].name, name) &&
            indexed_version_matches(match->object, match->object->claims[i].version,
                                    relation, version, 0)) match->found = 1;
    return 1;
}

static int indexed_provider(const struct object *object, const char *kind,
                            const char *name, int file_index, int soname_index)
{
    static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"};
    size_t i;
    if (!strcmp(kind, "package-or")) {
        struct or_match match = {object, 0};
        return holy_package_or_each(name, indexed_or_branch, &match) && match.found;
    }
    if (!strcmp(kind, "package") && !strcmp(object->identity.name, name)) return 1;
    if (!strcmp(kind, "soname")) {
        if (!soname_index) return 0;
        for (i = 0; i < object->soname_count; ++i)
            if (!strcmp(object->sonames[i].name, name)) return 1;
        return 0;
    }
    if (!strcmp(kind, "file"))
        return file_index && name[0] == '/' && indexed_file(object, name + 1);
    if (!strcmp(kind, "command")) {
        if (!file_index || !*name || strchr(name, '/')) return 0;
        for (i = 0; i < sizeof dirs / sizeof *dirs; ++i) {
            size_t a = strlen(dirs[i]), b = strlen(name);
            char *path;
            int found;
            if (a > SIZE_MAX - b - 1) return 0;
            path = malloc(a + b + 1);
            if (!path) return 0;
            memcpy(path, dirs[i], a);
            memcpy(path + a, name, b + 1);
            found = indexed_file(object, path);
            free(path);
            if (found) return 1;
        }
        return 0;
    }
    for (i = 0; i < object->claim_count; ++i)
        if (!strcmp(object->claims[i].kind, kind) &&
            !strcmp(object->claims[i].name, name)) return 1;
    return 0;
}

static unsigned char ascii_lower(unsigned char c)
{
    return c >= 'A' && c <= 'Z' ? (unsigned char)(c + 'a' - 'A') : c;
}

static int ascii_prefix(const char *text, const char *prefix)
{
    while (*prefix) {
        if (!*text || ascii_lower((unsigned char)*text++) !=
                      ascii_lower((unsigned char)*prefix++)) return 0;
    }
    return 1;
}

static int fuzzy_rank(const char *query, const char *target)
{
    size_t q = strlen(query), t = strlen(target), i, j;
    unsigned int prev[129], next[129], row_min;
    if (!q || !t || q > 256 || t > 256) return -1;
    if (ascii_prefix(target, query)) return q == t ? 0 : 1;
    for (i = 1; i < t; ++i)
        if (ascii_prefix(target + i, query)) return 2;
    if (q > 128 || t > 128 || q > t + 2 || t > q + 2) return -1;
    for (j = 0; j <= t; ++j) prev[j] = (unsigned int)j;
    for (i = 1; i <= q; ++i) {
        next[0] = (unsigned int)i;
        row_min = next[0];
        for (j = 1; j <= t; ++j) {
            unsigned int cost = ascii_lower((unsigned char)query[i - 1]) ==
                                ascii_lower((unsigned char)target[j - 1]) ? 0 : 1;
            unsigned int deletion = prev[j] + 1;
            unsigned int insertion = next[j - 1] + 1;
            unsigned int replacement = prev[j - 1] + cost;
            next[j] = deletion < insertion ? deletion : insertion;
            if (replacement < next[j]) next[j] = replacement;
            if (next[j] < row_min) row_min = next[j];
        }
        if (row_min > 2) return -1;
        memcpy(prev, next, (t + 1) * sizeof *prev);
    }
    return prev[t] == 1 ? 3 : prev[t] == 2 ? 4 : -1;
}

static int file_hint(const struct object *object, const char *query,
                     const char **matched)
{
    const char *basename = strrchr(query, '/');
    size_t i;
    int best = -1;
    basename = basename ? basename + 1 : query;
    *matched = NULL;
    if (!*basename) return -1;
    for (i = 0; i < object->file_count; ++i) {
        const char *path = object->files[i], *name = strrchr(path, '/');
        int score = fuzzy_rank(basename, name ? name + 1 : path);
        if (query[0] == '/' && score >= 0) {
            if (!strcmp(query + 1, path)) score = 0;
            else ++score;
        }
        if (score >= 0 && (best < 0 || score < best ||
            (score == best && strcmp(path, *matched) < 0))) {
            best = score;
            *matched = path;
        }
    }
    return best;
}

struct mirror {
    const char *base, *ca_file, *downloads;
    int status;
};

struct stage_request {
    const char *root;
    struct holy_repo_set *set;
    const struct holy_package_identity *slot;
    const char *digest;
    int index_only;
    int provider;
    struct holy_repo_slot_list *candidates;
};

static int copy_identity(struct holy_package_identity *to,
                         const struct holy_package_identity *from)
{
    memset(to, 0, sizeof *to);
    to->name = strdup(from->name);
    to->version = strdup(from->version);
    to->release = strdup(from->release);
    to->version_family = from->version_family ? strdup(from->version_family) : NULL;
    to->os = strdup(from->os);
    to->arch = strdup(from->arch);
    to->libc = strdup(from->libc);
    memcpy(to->digest, from->digest, sizeof to->digest);
    to->size = from->size;
    return to->name && to->version && to->release && to->os && to->arch &&
           to->libc && (!from->version_family || to->version_family);
}

struct closure {
    unsigned char *selected;
    size_t *queue;
    size_t count;
    struct provider_key *providers;
    size_t provider_count;
};

struct provider_key {
    const char *kind, *name;
    size_t index;
};

static int provider_key_order(const void *left, const void *right)
{
    const struct provider_key *a = left, *b = right;
    int order = strcmp(a->kind, b->kind);
    if (!order) order = strcmp(a->name, b->name);
    return order ? order : a->index < b->index ? -1 : a->index > b->index;
}

static int closure_index(const struct object *objects, size_t count,
                         struct closure *closure)
{
    static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"};
    size_t capacity = count, i, j, k, used = 0;
    struct provider_key *keys;
    for (i = 0; i < count; ++i) {
        const struct object *o = &objects[i];
        size_t extra;
        if (o->claim_count > SIZE_MAX - o->soname_count) return 0;
        extra = o->claim_count + o->soname_count;
        if (o->file_count > (SIZE_MAX - extra) / 2) return 0;
        extra += o->file_count * 2;
        if (capacity > SIZE_MAX - extra) return 0;
        capacity += extra;
    }
    if (capacity > SIZE_MAX / sizeof *keys) return 0;
    keys = calloc(capacity ? capacity : 1, sizeof *keys);
    if (!keys) return 0;
    for (i = 0; i < count; ++i) {
        const struct object *o = &objects[i];
        keys[used++] = (struct provider_key){"package", o->identity.name, i};
        for (j = 0; j < o->claim_count; ++j)
            keys[used++] = (struct provider_key){o->claims[j].kind,
                                                  o->claims[j].name, i};
        for (j = 0; j < o->soname_count; ++j)
            keys[used++] = (struct provider_key){"soname", o->sonames[j].name, i};
        for (j = 0; j < o->file_count; ++j) {
            const char *path = o->files[j];
            keys[used++] = (struct provider_key){"file", path, i};
            for (k = 0; k < sizeof dirs / sizeof *dirs; ++k)
                if (!strncmp(path, dirs[k], strlen(dirs[k])) &&
                    path[strlen(dirs[k])]) {
                    keys[used++] = (struct provider_key){"command",
                                   path + strlen(dirs[k]), i};
                    break;
                }
        }
    }
    qsort(keys, used, sizeof *keys, provider_key_order);
    closure->providers = keys;
    closure->provider_count = used;
    return 1;
}

static int closure_add(struct closure *closure, size_t index)
{
    if (!closure->selected[index]) {
        closure->selected[index] = 1;
        closure->queue[closure->count++] = index;
    }
    return 1;
}

static int closure_provider(struct closure *closure, const char *kind,
                            const char *name)
{
    size_t low = 0, high = closure->provider_count, i;
    const char *file = !strcmp(kind, "file") && name[0] == '/' ? name + 1 : name;
    while (low < high) {
        size_t middle = low + (high - low) / 2;
        const struct provider_key *key = &closure->providers[middle];
        int order = strcmp(key->kind, kind);
        if (!order) order = strcmp(key->name, file);
        if (order < 0) low = middle + 1;
        else high = middle;
    }
    for (i = low; i < closure->provider_count; ++i) {
        const struct provider_key *key = &closure->providers[i];
        if (strcmp(key->kind, kind) || strcmp(key->name, file)) break;
        if (!closure_add(closure, key->index)) return 0;
    }
    return 1;
}

static int candidate_symbols(int dir, const struct object *object,
                             const struct soname_fact *fact,
                             const struct holy_scanned_file *consumer,
                             const char *needed)
{
    struct holy_scan_result scan = {0};
    char *snapshot = NULL, actual[65];
    size_t i, j;
    int fd, result = 0, required = 0;
    for (i = 0; i < consumer->elf.symbol_count; ++i) {
        const struct holy_elf_symbol *symbol = &consumer->elf.symbols[i];
        if (!symbol->section && symbol->binding != STB_WEAK && symbol->provider &&
            !strcmp(symbol->provider, needed)) { required = 1; break; }
    }
    if (!required) return 1;
    fd = openat(dir, object->filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (fd < 0) return -1;
    snapshot = holy_stage_fd(fd, "holy-soname-probe");
    close(fd);
    if (!snapshot || !digest_file(snapshot, actual) ||
        strcmp(actual, object->identity.digest) ||
        !holy_scan_collect(snapshot, &scan)) { result = -1; goto done; }
    for (i = 0; i < scan.count; ++i) {
        const struct holy_scanned_file *file = &scan.files[i];
        if (strcmp(file->path, fact->path) || file->elf.type != ET_DYN ||
            (file->elf.flags1 & DF_1_PIE) || !file->elf.soname ||
            strcmp(file->elf.soname, needed) ||
            strcmp(holy_elf_machine(&file->elf), fact->arch) ||
            strcmp(file->runtime, fact->libc)) continue;
        result = 1;
        for (j = 0; j < consumer->elf.symbol_count; ++j) {
            const struct holy_elf_symbol *symbol = &consumer->elf.symbols[j];
            if (symbol->section || symbol->binding == STB_WEAK ||
                !symbol->provider || strcmp(symbol->provider, needed)) continue;
            if (!holy_elf_exports_symbol(&file->elf, symbol)) { result = 0; break; }
        }
        break;
    }
done:
    holy_scan_free(&scan);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    return result;
}

static int indexed_exports_symbol(const struct object *object, const char *path,
                                  const struct holy_elf_symbol *wanted)
{
    size_t i;
    for (i = 0; i < object->export_count; ++i) {
        const struct export_fact *s = &object->exports[i];
        if (strcmp(s->path, path) || strcmp(s->name, wanted->name) ||
            (wanted->type == STT_TLS) != (s->type == STT_TLS) ||
            (wanted->type == STT_FUNC && s->type != STT_FUNC && s->type != 10) ||
            (wanted->type == STT_OBJECT && s->type != STT_OBJECT) ||
            (wanted->version && strcmp(s->version, wanted->version)) ||
            (!wanted->version && s->hidden)) continue;
        return 1;
    }
    return 0;
}

static int candidate_exports(const struct object *object,
                             const struct soname_fact *fact,
                             const struct holy_scanned_file *consumer,
                             const char *needed)
{
    size_t i;
    for (i = 0; i < consumer->elf.symbol_count; ++i) {
        const struct holy_elf_symbol *want = &consumer->elf.symbols[i];
        if (want->section || want->binding == STB_WEAK ||
            !want->provider || strcmp(want->provider, needed)) continue;
        if (!indexed_exports_symbol(object, fact->path, want)) return 0;
    }
    return 1;
}

static int object_soname_matches(int dir, const struct object *object,
                                 const struct holy_scanned_file *consumer,
                                 const char *needed, int export_index)
{
    size_t j, k;
    for (j = 0; j < object->soname_count; ++j) {
        const struct soname_fact *fact = &object->sonames[j];
        int compatible = 1, symbols;
        if (strcmp(fact->name, needed) ||
            strcmp(fact->arch, holy_elf_machine(&consumer->elf)) ||
            strcmp(fact->libc, consumer->runtime)) continue;
        for (k = 0; k < consumer->elf.version_count; ++k) {
            const struct holy_elf_version *want = &consumer->elf.versions[k];
            size_t n;
            if (want->weak || strcmp(want->provider, needed)) continue;
            for (n = 0; n < object->version_count; ++n)
                if (!strcmp(object->versions[n].path, fact->path) &&
                    !strcmp(object->versions[n].name, want->name)) break;
            if (n == object->version_count) { compatible = 0; break; }
        }
        if (!compatible) continue;
        symbols = export_index ? candidate_exports(object, fact, consumer, needed) :
                                 candidate_symbols(dir, object, fact, consumer, needed);
        if (symbols) return symbols;
    }
    return 0;
}

static int closure_soname(int dir, const struct object *objects, struct closure *closure,
                          const struct holy_scanned_file *consumer,
                          const char *needed, int export_index)
{
    size_t low = 0, high = closure->provider_count, i;
    while (low < high) {
        size_t middle = low + (high - low) / 2;
        const struct provider_key *key = &closure->providers[middle];
        int order = strcmp(key->kind, "soname");
        if (!order) order = strcmp(key->name, needed);
        if (order < 0) low = middle + 1;
        else high = middle;
    }
    for (i = low; i < closure->provider_count; ++i) {
        const struct provider_key *key = &closure->providers[i];
        const struct object *object = &objects[key->index];
        if (strcmp(key->kind, "soname") || strcmp(key->name, needed)) break;
        int matches = object_soname_matches(dir, object, consumer, needed,
                                            export_index);
        if (matches < 0) return 0;
        if (matches && !closure_add(closure, key->index)) return 0;
    }
    return 1;
}

static int closure_or_provider(void *opaque, const char *name,
                               const char *relation, const char *version)
{
    (void)relation; (void)version;
    return closure_provider(opaque, "package", name);
}

static int closure_expand(int dir, const struct object *objects,
                          struct closure *closure, size_t index,
                          const char *snapshot, int version_index,
                          int export_index)
{
    const struct object *o = &objects[index];
    struct holy_scan_result scan = {0};
    size_t i, j;
    int ok = 1;
    for (i = 0; i < o->requirement_count && ok; ++i) {
        const char *kind = o->requirements[i].fields[2];
        const char *name = o->requirements[i].fields[3];
        ok = !strcmp(kind, "package-or") ?
             holy_package_or_each(name, closure_or_provider, closure) :
             closure_provider(closure, kind, name);
    }
    if (ok) ok = holy_scan_collect(snapshot, &scan);
    for (i = 0; i < scan.count && ok; ++i) {
        const struct holy_elf_info *elf = &scan.files[i].elf;
        if (elf->interpreter && elf->interpreter[0] == '/')
            ok = closure_provider(closure, "file", elf->interpreter);
        for (j = 0; j < elf->needed_count && ok; ++j)
            ok = version_index && elf->needed[j][0] != '/' ?
                 closure_soname(dir, objects, closure, &scan.files[i],
                                elf->needed[j], export_index) :
                 closure_provider(closure,
                                  elf->needed[j][0] == '/' ? "file" : "soname",
                                  elf->needed[j]);
    }
    for (i = 0; i < scan.script_count && ok; ++i)
        if (scan.scripts[i].kind == 1 && scan.scripts[i].interpreter[0] == '/')
            ok = closure_provider(closure, "file",
                                  scan.scripts[i].interpreter);
    for (i = 0; i < scan.symlink_count && ok; ++i) {
        char *path = scan.symlinks[i].target[0] == '/' ?
            strdup(scan.symlinks[i].target) :
            holy_relative_link_path(scan.symlinks[i].path,
                strlen(scan.symlinks[i].path), scan.symlinks[i].target, "");
        if (!path) { ok = 0; break; }
        ok = closure_provider(closure, "file", path);
        free(path);
    }
    holy_scan_free(&scan);
    return ok;
}

static int mirror_object(struct mirror *mirror, int dir, const struct object *object)
{
    char *url = holy_fetch_child_url(mirror->base, object->filename);
    char name[70];
    int downloads, ok;
    if (!url) { mirror->status = 2; return 0; }
    mirror->status = holy_fetch_https(url, object->identity.digest,
                                      mirror->downloads, mirror->ca_file, 0);
    free(url);
    if (mirror->status) return 0;
    downloads = open(mirror->downloads, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (downloads < 0) { mirror->status = 1; return 0; }
    snprintf(name, sizeof name, "%s.holy", object->identity.digest);
    ok = !linkat(downloads, name, dir, object->filename, 0) && !fsync(dir);
    if (ok) ok = !unlinkat(downloads, name, 0) && !fsync(downloads);
    close(downloads);
    if (!ok) mirror->status = 1;
    return ok;
}

static int list_probe(const char *directory, const char *query,
                 const char *forced_index, int lock, int emit,
                 const char *fetch_digest, const char *output,
                 const char *provider_kind, const char *provider_name,
                 const char *solve_name, const char *solve_choice,
                  int solve_json, int *solve_rc, struct mirror *mirror,
                  int extract_name, const struct stage_request *stage,
                  const char *file_query, const struct holy_scanned_file *probe)
{
    struct object *objects = NULL;
    char **candidate_snapshots = NULL;
    struct closure closure = {0};
    struct stat st;
    FILE *index = NULL;
    char *line = NULL, *error = NULL, *index_snapshot = NULL, *chosen = NULL;
    char expected[65], actual_digest[65], index_name[71];
    size_t capacity = 0, count = 0, i, j, number = 0, cursor;
    ssize_t length;
    int dir = -1, fd = -1, ok = 0, indexed = 0, file_index = 0;
    int dependency_index = 0, soname_index = 0, version_index = 0;
    int export_index = 0, comparator_index = 0;

    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || (lock && flock(dir, LOCK_SH) < 0)) goto done;
    if (forced_index) {
        if (!*forced_index || strlen(forced_index) >= sizeof index_name ||
            strchr(forced_index, '/')) goto done;
        strcpy(index_name, forced_index);
    } else {
        if (read_current(dir, expected) != 1) goto done;
        snprintf(index_name, sizeof index_name, "index.%s", expected);
    }
    fd = openat(dir, index_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size < 0 || st.st_size > 128LL * 1024 * 1024) goto done;
    index_snapshot = holy_stage_fd(fd, "holy-catalog");
    close(fd);
    fd = -1;
    if (!index_snapshot || (!forced_index &&
        (!digest_file(index_snapshot, actual_digest) ||
         strcmp(actual_digest, expected)))) goto done;
    index = fopen(index_snapshot, "r");
    if (!index) goto done;
    while ((length = getline(&line, &capacity, index)) >= 0) {
        char **v = NULL;
        size_t n = 0;
        struct object *next;
        ++number;
        if (length > 1024 * 1024 || memchr(line, '\0', (size_t)length) ||
            !holy_lex(line, (size_t)length, &v, &n, "index", number, &error)) {
            holy_tokens_free(v, n);
            goto done;
        }
        if (number == 1) {
            int valid = n == 2 && !strcmp(v[0], "format") &&
                        (!strcmp(v[1], "holy-index-prototype-1") ||
                         !strcmp(v[1], "holy-index-prototype-2") ||
                         !strcmp(v[1], "holy-index-prototype-3") ||
                         !strcmp(v[1], "holy-index-prototype-4") ||
                         !strcmp(v[1], "holy-index-prototype-5") ||
                         !strcmp(v[1], "holy-index-prototype-6") ||
                         !strcmp(v[1], "holy-index-prototype-7") ||
                         !strcmp(v[1], "holy-index-prototype-8"));
            if (valid) {
                indexed = strcmp(v[1], "holy-index-prototype-1") != 0;
                file_index = !strcmp(v[1], "holy-index-prototype-3") ||
                             !strcmp(v[1], "holy-index-prototype-4") ||
                             !strcmp(v[1], "holy-index-prototype-5") ||
                             !strcmp(v[1], "holy-index-prototype-6") ||
                             !strcmp(v[1], "holy-index-prototype-7") ||
                             !strcmp(v[1], "holy-index-prototype-8");
                dependency_index = !strcmp(v[1], "holy-index-prototype-4") ||
                                   !strcmp(v[1], "holy-index-prototype-5") ||
                                   !strcmp(v[1], "holy-index-prototype-6") ||
                                   !strcmp(v[1], "holy-index-prototype-7") ||
                                   !strcmp(v[1], "holy-index-prototype-8");
                soname_index = !strcmp(v[1], "holy-index-prototype-5") ||
                               !strcmp(v[1], "holy-index-prototype-6") ||
                                   !strcmp(v[1], "holy-index-prototype-7") ||
                                   !strcmp(v[1], "holy-index-prototype-8");
                version_index = !strcmp(v[1], "holy-index-prototype-6") ||
                                !strcmp(v[1], "holy-index-prototype-7") ||
                                !strcmp(v[1], "holy-index-prototype-8");
                export_index = !strcmp(v[1], "holy-index-prototype-7") ||
                               !strcmp(v[1], "holy-index-prototype-8");
                comparator_index = !strcmp(v[1], "holy-index-prototype-8");
            }
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (file_index && number == 2) {
            int valid = n == 3 && !strcmp(v[0], "coverage") &&
                        !strcmp(v[1], "files") && !strcmp(v[2], "complete");
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (dependency_index && number == 3) {
            int valid = n == 3 && !strcmp(v[0], "coverage") &&
                        !strcmp(v[1], "dependencies") && !strcmp(v[2], "complete");
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (soname_index && number == 4) {
            int valid = n == 3 && !strcmp(v[0], "coverage") &&
                        !strcmp(v[1], "elf-sonames") && !strcmp(v[2], "complete");
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (version_index && number == 5) {
            int valid = n == 3 && !strcmp(v[0], "coverage") &&
                        !strcmp(v[1], "elf-versions") && !strcmp(v[2], "complete");
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (export_index && number == 6) {
            int valid = n == 3 && !strcmp(v[0], "coverage") &&
                        !strcmp(v[1], "elf-exports") && !strcmp(v[2], "complete");
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (indexed && n && !strcmp(v[0], "claim")) {
            int valid = count && !objects[count - 1].file_count &&
                        !objects[count - 1].requirement_count &&
                        !objects[count - 1].soname_count &&
                        !objects[count - 1].version_count &&
                        !objects[count - 1].export_count &&
                        parse_claim(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (dependency_index && n && !strcmp(v[0], "require")) {
            int valid = count && !objects[count - 1].file_count &&
                        !objects[count - 1].soname_count &&
                        !objects[count - 1].version_count &&
                        !objects[count - 1].export_count &&
                        parse_requirement(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (soname_index && n && !strcmp(v[0], "soname")) {
            int valid = count && !objects[count - 1].file_count &&
                        !objects[count - 1].version_count &&
                        !objects[count - 1].export_count &&
                        parse_soname(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (version_index && n && !strcmp(v[0], "elf-version")) {
            int valid = count && !objects[count - 1].file_count &&
                        !objects[count - 1].export_count &&
                        parse_version(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (export_index && n && !strcmp(v[0], "elf-export")) {
            int valid = count && !objects[count - 1].file_count &&
                        parse_export(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (file_index && n && !strcmp(v[0], "file")) {
            int valid = count && parse_file(v, n, &objects[count - 1]);
            holy_tokens_free(v, n);
            if (!valid) goto done;
            continue;
        }
        if (count == (size_t)-1 / sizeof *objects) {
            holy_tokens_free(v, n);
            goto done;
        }
        next = realloc(objects, (count + 1) * sizeof *objects);
        if (!next) { holy_tokens_free(v, n); goto done; }
        objects = next;
        memset(&objects[count], 0, sizeof *objects);
        if ((comparator_index && n != 11) ||
            !parse_record(v, n, &objects[count])) {
            holy_tokens_free(v, n);
            goto done;
        }
        holy_tokens_free(v, n);
        ++count;
        for (i = 0; i + 1 < count; ++i)
            if (!strcmp(objects[i].filename, objects[count - 1].filename) ||
                same_identity(&objects[i].identity, &objects[count - 1].identity))
                goto done;
    }
    if (ferror(index) || !number || (file_index && number < 2) ||
        (dependency_index && number < 3) ||
        (soname_index && number < 4) ||
        (version_index && number < 5) ||
        (export_index && number < 6)) goto done;
    if (emit == 8 && (((!strcmp(provider_kind, "file") ||
                        !strcmp(provider_kind, "command")) && !file_index) ||
                      (!strcmp(provider_kind, "soname") && !soname_index) ||
                      (probe && !version_index) ||
                      !strcmp(provider_kind, "symbol-version"))) {
        *solve_rc = 6;
        ok = 1;
        goto done;
    }
    if (mirror) {
        for (i = 0; i < count; ++i)
            if (!mirror_object(mirror, dir, &objects[i])) goto done;
    }
    if (solve_name || (stage && stage->provider)) {
        candidate_snapshots = calloc(count ? count : 1, sizeof *candidate_snapshots);
        if (!candidate_snapshots) goto done;
    }
    if (stage && !stage->slot && !stage->index_only &&
        dependency_index && file_index && soname_index) {
        size_t root = count, roots = 0;
        if (!stage->provider) {
            for (i = 0; i < count; ++i)
                if (!strcmp(objects[i].identity.name, solve_name)) {
                    root = i;
                    ++roots;
                }
            if (roots != 1) {
                *solve_rc = roots ? 3 : 6;
                fprintf(stderr, "holypkg: repository root %s\n",
                        roots ? "requires package choice" : "not found");
                ok = 1; goto done;
            }
        }
        closure.selected = calloc(count, 1);
        closure.queue = calloc(count, sizeof *closure.queue);
        if (!closure.selected || !closure.queue ||
            !closure_index(objects, count, &closure)) goto done;
        if (stage->provider) {
            for (i = 0; i < count; ++i)
                if (indexed_provider(&objects[i], provider_kind, provider_name,
                                     file_index, soname_index)) closure_add(&closure, i);
            if (!closure.count) { *solve_rc = 4; ok = 1; goto done; }
        } else closure_add(&closure, root);
    }
    if (stage && stage->provider && !closure.selected) {
        *solve_rc = 6;
        ok = 1; goto done;
    }
    for (cursor = 0; cursor < (closure.selected ? closure.count : count); ++cursor) {
        struct holy_package_identity actual;
        char *snapshot;
        int input, matches;
        i = closure.selected ? closure.queue[cursor] : cursor;
        if (stage && (stage->index_only ||
            (stage->slot && !same_slot(&objects[i].identity, stage->slot)) ||
            (stage->digest && strcmp(objects[i].identity.digest, stage->digest))))
            continue;
        if (query && (emit == 1 || emit == 3) &&
            strcmp(query, objects[i].identity.name)) continue;
        if (fetch_digest && strcmp(fetch_digest, objects[i].identity.digest))
            continue;
        if (solve_name && output && !stage &&
            strcmp(solve_name, objects[i].identity.name)) continue;
        if (emit == 5 && query && fuzzy_rank(query, objects[i].identity.name) < 0)
            continue;
        if (file_query) {
            const char *matched;
            if (!file_index || (emit == 6 ?
                file_hint(&objects[i], file_query, &matched) < 0 :
                !indexed_file(&objects[i], file_query))) continue;
        }
        if (indexed && provider_kind && !(stage && stage->provider)) {
            size_t k;
            objects[i].provider_match = emit == 8 ?
                indexed_provider(&objects[i], provider_kind, provider_name,
                                 file_index, soname_index) :
                !strcmp(provider_kind, "package") &&
                !strcmp(provider_name, objects[i].identity.name);
            if (emit == 8 && probe && objects[i].provider_match) {
                int match = object_soname_matches(dir, &objects[i], probe,
                                                   provider_name, export_index);
                if (match < 0) goto done;
                objects[i].provider_match = match;
            }
            if (emit == 8) {
                if (!objects[i].provider_match) continue;
            } else if (soname_index && !strcmp(provider_kind, "soname")) {
                for (k = 0; k < objects[i].soname_count; ++k)
                    if (!strcmp(objects[i].sonames[k].name, provider_name))
                        objects[i].provider_match = 1;
            } else for (k = 0; k < objects[i].claim_count; ++k)
                if (!strcmp(objects[i].claims[k].kind, provider_kind) &&
                    !strcmp(objects[i].claims[k].name, provider_name))
                    objects[i].provider_match = 1;
            if (!objects[i].provider_match) continue;
        }
        if (emit == 7 || (emit == 8 && indexed)) continue;
        input = openat(dir, objects[i].filename,
                            O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
        if (input < 0) goto done;
        snapshot = holy_stage_fd(input, "holy-list");
        close(input);
        if (!snapshot) goto done;
        matches = holy_verify_with_output(snapshot, 0) &&
                  holy_scan_local_with_output(snapshot, 0) &&
                  holy_deps_local_with_output(snapshot, 0) &&
                  holy_provides_local(snapshot, 0) &&
                  holy_package_identity(snapshot, &actual);
        if (matches) {
            matches = same_identity(&objects[i].identity, &actual) &&
                      (!comparator_index ||
                       (!objects[i].identity.version_family == !actual.version_family &&
                        (!actual.version_family ||
                         !strcmp(objects[i].identity.version_family, actual.version_family)))) &&
                      !strcmp(objects[i].identity.digest, actual.digest) &&
                      objects[i].identity.size == actual.size;
            holy_package_identity_free(&actual);
        }
        if (!matches) {
            unlink(snapshot);
            free(snapshot);
            goto done;
        }
        if (indexed) {
            struct claim_cursor cursor = {&objects[i], 0};
            if (!holy_provides_visit(snapshot, compare_claim, &cursor) ||
                cursor.index != objects[i].claim_count) {
                unlink(snapshot);
                free(snapshot);
                goto done;
            }
        }
        if (dependency_index) {
            struct requirement_cursor cursor = {&objects[i], 0};
            if (!holy_deps_visit(snapshot, compare_requirement, &cursor) ||
                cursor.index != objects[i].requirement_count) {
                unlink(snapshot);
                free(snapshot);
                goto done;
            }
        }
        if (soname_index && !compare_sonames(snapshot, &objects[i],
                                             version_index, export_index)) {
            unlink(snapshot);
            free(snapshot);
            goto done;
        }
        if (file_index) {
            struct file_cursor cursor = {&objects[i], 0};
            if (!holy_verify_visit(snapshot, compare_file, &cursor) ||
                cursor.index != objects[i].file_count) {
                unlink(snapshot);
                free(snapshot);
                goto done;
            }
        }
        if (closure.selected &&
            !closure_expand(dir, objects, &closure, i, snapshot,
                            version_index, export_index)) {
            unlink(snapshot);
            free(snapshot);
            goto done;
        }
        if (provider_kind && !indexed && !(stage && stage->provider)) {
            int claim = 0;
            if (!holy_provides_match(snapshot, provider_kind,
                                     provider_name, &claim)) {
                unlink(snapshot);
                free(snapshot);
                goto done;
            }
            objects[i].provider_match = claim ||
                (!strcmp(provider_kind, "package") &&
                 !strcmp(provider_name, objects[i].identity.name));
        }
        if (solve_name || (stage && stage->provider)) candidate_snapshots[i] = snapshot;
        else if (fetch_digest && !strcmp(fetch_digest, objects[i].identity.digest))
            chosen = snapshot;
        else {
            unlink(snapshot);
            free(snapshot);
        }
    }
    if (fetch_digest && (!chosen || !holy_fetch_local(chosen, output))) goto done;
    if (solve_name) {
        const char **paths = NULL;
        size_t root = count, roots = 0, next = 1;
        for (i = 0; i < count; ++i) if (!strcmp(objects[i].identity.name, solve_name)) {
            root = i;
            ++roots;
        }
        if (roots != 1 && !(stage && stage->slot)) {
            *solve_rc = roots ? 3 : 6;
            fprintf(stderr, "holypkg: repository root %s\n",
                    roots ? "requires package choice" : "not found");
            if (solve_json)
                printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\"}\n",
                       roots ? "decision-required" : "unavailable-artifact");
        } else if (stage) {
            size_t position = 0;
            *solve_rc = 6;
            if (count > 100000) { ok = 1; goto done; }
            stage->set->digests = calloc(closure.selected ? closure.count : count,
                                         sizeof *stage->set->digests);
            if (!stage->set->digests) { *solve_rc = 1; ok = 1; goto done; }
            for (cursor = 0; cursor < (closure.selected ? closure.count : count); ++cursor) {
                i = closure.selected ? closure.queue[cursor] : cursor;
                if (stage->slot && !same_slot(&objects[i].identity, stage->slot))
                    continue;
                if (stage->digest && strcmp(objects[i].identity.digest, stage->digest))
                    continue;
                size_t selected = closure.selected ? i :
                                  stage->slot ? i : i ? (i <= root ? i - 1 : i) : root;
                char actual[65];
                if (position >= 10000) { *solve_rc = 6; ok = 1; goto done; }
                if (!holy_cache_stage_local_digest(candidate_snapshots[selected],
                                                   stage->root, actual)) {
                    *solve_rc = 1; ok = 1; goto done;
                }
                if (strcmp(actual, objects[selected].identity.digest)) {
                    *solve_rc = 4; ok = 1; goto done;
                }
                stage->set->digests[position] = strdup(actual);
                if (!stage->set->digests[position]) { *solve_rc = 1; ok = 1; goto done; }
                ++position;
                stage->set->count = position;
            }
            memcpy(stage->set->index, expected, 65);
            *solve_rc = position ? 0 : 6;
        } else if (output) {
            *solve_rc = (extract_name ?
                         holy_extract_local(candidate_snapshots[root], output) :
                         holy_fetch_local(candidate_snapshots[root], output)) ? 0 : 1;
        } else {
            paths = calloc(count, sizeof *paths);
            if (!paths) goto done;
            paths[0] = candidate_snapshots[root];
            for (i = 0; i < count; ++i)
                if (i != root) paths[next++] = candidate_snapshots[i];
            *solve_rc = holy_resolve_local(paths, count, solve_json, expected,
                                           solve_choice);
            free(paths);
        }
    }
    if (stage && stage->provider) {
        size_t position = 0;
        stage->set->digests = calloc(closure.count, sizeof *stage->set->digests);
        if (!stage->set->digests) { *solve_rc = 1; ok = 1; goto done; }
        for (cursor = 0; cursor < closure.count; ++cursor) {
            char actual[65];
            i = closure.queue[cursor];
            if (position >= 10000 || !candidate_snapshots[i] ||
                !holy_cache_stage_local_digest(candidate_snapshots[i], stage->root, actual)) {
                *solve_rc = 6; ok = 1; goto done;
            }
            if (strcmp(actual, objects[i].identity.digest)) {
                *solve_rc = 4; ok = 1; goto done;
            }
            stage->set->digests[position] = strdup(actual);
            if (!stage->set->digests[position]) { *solve_rc = 1; ok = 1; goto done; }
            stage->set->count = ++position;
        }
        memcpy(stage->set->index, expected, 65);
        *solve_rc = position ? 0 : 6;
    }
    if (emit == 7) {
        size_t found = count, matches = 0;
        if (!dependency_index) {
            fputs("status unknown: source has no complete dependency index\n", stdout);
            if (solve_rc) *solve_rc = 6;
        } else {
            for (j = 0; j < count; ++j)
                if (!strcmp(objects[j].identity.name, query)) {
                    found = j;
                    ++matches;
                }
            if (matches == 1) {
                if (!record(stdout, &objects[found])) goto done;
                for (j = 0; j < objects[found].requirement_count; ++j)
                    if (!requirement_record(stdout, &objects[found],
                                            &objects[found].requirements[j])) goto done;
                printf("requirements %zu\n", objects[found].requirement_count);
                if (solve_rc) *solve_rc = 0;
            } else {
                fprintf(stderr, "holypkg: repository package %s\n",
                        matches ? "requires an architecture/ABI choice" : "not found");
                if (solve_rc) *solve_rc = matches ? 3 : 6;
            }
        }
    } else if (emit == 5 || emit == 6) {
        size_t matches = 0, shown = 0;
        int rank;
        for (rank = 0; rank <= 5; ++rank) for (j = 0; j < count; ++j) {
            const char *matched = NULL;
            int score = emit == 5 ? fuzzy_rank(query, objects[j].identity.name) :
                        file_index ? file_hint(&objects[j], file_query, &matched) : -1;
            if (score != rank) continue;
            ++matches;
            if (shown == 20) continue;
            printf("suggestion score %d %s ", score, emit == 5 ? "name" : "path");
            if (!quote(stdout, emit == 5 ? objects[j].identity.name : matched) ||
                fputc('\n', stdout) == EOF || !record(stdout, &objects[j])) goto done;
            ++shown;
        }
        if (emit == 6) {
            printf("coverage files %s index %s time %lld\n",
                   file_index ? "complete" : "unavailable", expected,
                   (long long)st.st_mtime);
            if (!file_index) {
                fputs("status unknown: source has no complete file index\n", stdout);
                if (solve_rc) *solve_rc = 6;
            } else if (solve_rc) *solve_rc = 0;
        }
        printf("suggested %zu of %zu %s\n", shown, matches,
               emit == 5 ? "packages" : "file candidates");
    } else {
        size_t matches = 0, only = count;
        for (j = 0; j < count; ++j) {
            if (query && strcmp(objects[j].identity.name, query)) continue;
            if (provider_kind && !objects[j].provider_match) continue;
            if (file_query && (!file_index || !indexed_file(&objects[j], file_query)))
                continue;
            if (emit == 1 && !record(stdout, &objects[j])) goto done;
            if (emit == 2) candidate_json(&objects[j]);
            if (emit == 3) only = j;
            if (emit == 4 && !record(stdout, &objects[j])) goto done;
            ++matches;
        }
        if (emit == 1) printf("listed %zu %s\n", matches,
                              provider_kind ? "candidates" : "packages");
        if (emit == 2) printf("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"summary\",\"count\":%zu}\n", matches);
        if (emit == 3) {
            *solve_rc = matches > 1 ? 3 : matches ? 0 : 6;
            if (matches == 1 && !record(stdout, &objects[only])) goto done;
            if (matches != 1)
                fprintf(stderr, "holypkg: repository package %s\n",
                        matches ? "requires an architecture/ABI choice" : "not found");
        }
        if (emit == 8 && solve_rc) *solve_rc = matches ? 0 : 4;
        if (emit == 4) {
            printf("coverage files %s index %s time %lld\n",
                   file_index ? "complete" : "unavailable", expected,
                   (long long)st.st_mtime);
            printf("listed %zu file candidates\n", matches);
            if (!file_index) {
                fputs("status unknown: source has no complete file index\n", stdout);
                if (solve_rc) *solve_rc = 6;
            } else if (solve_rc) *solve_rc = 0;
        }
    }
    if (stage && stage->index_only) {
        memcpy(stage->set->index, expected, 65);
        if (stage->candidates) {
            struct holy_repo_slot_list *list = stage->candidates;
            size_t matches = 0;
            for (i = 0; i < count; ++i)
                if (same_slot(&objects[i].identity, stage->slot)) ++matches;
            list->items = calloc(matches ? matches : 1, sizeof *list->items);
            if (!list->items) goto done;
            for (i = 0; i < count; ++i) if (same_slot(&objects[i].identity, stage->slot)) {
                if (!copy_identity(&list->items[list->count], &objects[i].identity)) {
                    ++list->count;
                    goto done;
                }
                ++list->count;
            }
            memcpy(list->index, expected, 65);
            *solve_rc = matches ? 0 : 6;
        }
        if (stage->digest) {
            *solve_rc = 3;
            for (i = 0; i < count; ++i)
                if (!strcmp(objects[i].identity.digest, stage->digest) &&
                    same_slot(&objects[i].identity, stage->slot)) {
                    *solve_rc = 0;
                    break;
                }
        }
    }
    ok = 1;
done:
    if (!ok) {
        fprintf(stderr, "holypkg: invalid or stale repository index%s%s\n",
                error ? ": " : "", error ? error : "");
        if (emit == 2) puts("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"error\",\"code\":\"invalid-catalog\"}");
    }
    free(error);
    free(line);
    if (index) fclose(index);
    if (index_snapshot) { unlink(index_snapshot); free(index_snapshot); }
    if (chosen) { unlink(chosen); free(chosen); }
    if (candidate_snapshots) {
        for (i = 0; i < count; ++i)
            if (candidate_snapshots[i]) {
                unlink(candidate_snapshots[i]);
                free(candidate_snapshots[i]);
            }
        free(candidate_snapshots);
    }
    if (fd >= 0) close(fd);
    for (i = 0; i < count; ++i) {
        free(objects[i].filename);
        holy_package_identity_free(&objects[i].identity);
        free_claims(&objects[i]);
        free_requirements(&objects[i]);
        free_sonames(&objects[i]);
        free_versions(&objects[i]);
        free_exports(&objects[i]);
        free_files(&objects[i]);
    }
    free(objects);
    free(closure.selected);
    free(closure.queue);
    free(closure.providers);
    if (dir >= 0) close(dir);
    return ok;
}

static int list(const char *directory, const char *query,
                const char *forced_index, int lock, int emit,
                const char *fetch_digest, const char *output,
                const char *provider_kind, const char *provider_name,
                const char *solve_name, const char *solve_choice,
                int solve_json, int *solve_rc, struct mirror *mirror,
                int extract_name, const struct stage_request *stage,
                const char *file_query)
{
    return list_probe(directory, query, forced_index, lock, emit,
                      fetch_digest, output, provider_kind, provider_name,
                      solve_name, solve_choice, solve_json, solve_rc, mirror,
                      extract_name, stage, file_query, NULL);
}

int holy_repo_list(const char *directory)
{
    return list(directory, NULL, NULL, 1, 1, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL);
}

int holy_repo_search(const char *directory, const char *query)
{
    if (!query || !*query) {
        fprintf(stderr, "holypkg: package name required\n");
        return 0;
    }
    return list(directory, query, NULL, 1, 1, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL);
}

int holy_repo_search_fuzzy(const char *directory, const char *query)
{
    if (!query || !*query) return 2;
    return list(directory, query, NULL, 1, 5, NULL, NULL, NULL, NULL,
                NULL, NULL, 0, NULL, NULL, 0, NULL, NULL) ? 0 : 6;
}

int holy_repo_search_file(const char *directory, const char *query)
{
    int result = 6;
    if (!query || query[0] != '/' || !query[1] ||
        !safe_target_path(query + 1)) {
        fprintf(stderr, "holypkg: absolute file path required\n");
        return 2;
    }
    if (!list(directory, NULL, NULL, 1, 4, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, NULL, query + 1)) return 6;
    return result;
}

int holy_repo_search_file_fuzzy(const char *directory, const char *query)
{
    int result = 6;
    if (!query || !*query || query[strlen(query) - 1] == '/' ||
        (query[0] != '/' && strchr(query, '/'))) return 2;
    if (query[0] == '/' && !safe_target_path(query + 1)) return 2;
    if (!list(directory, NULL, NULL, 1, 6, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, NULL, query)) return 6;
    return result;
}

int holy_repo_info_name(const char *directory, const char *name)
{
    int result = 6;
    if (!name || !*name) return 2;
    if (!list(directory, name, NULL, 1, 3, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6;
    return result;
}

int holy_repo_requirements(const char *directory, const char *name)
{
    int result = 6;
    if (!name || !*name) return 2;
    if (!list(directory, name, NULL, 1, 7, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6;
    return result;
}

int holy_repo_providers(const char *directory, const char *kind,
                        const char *name, int json)
{
    if (!holy_provides_kind(kind) || !name || !*name) {
        fprintf(stderr, "holypkg: provider kind and exact name required\n");
        if (json) puts("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"error\",\"code\":\"invalid-query\"}");
        return 0;
    }
    return list(directory, NULL, NULL, 1, json ? 2 : 1,
                NULL, NULL, kind, name, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL);
}

int holy_repo_has_provider(const char *directory, const char *kind,
                           const char *name)
{
    int result = 6;
    if ((!holy_provides_kind(kind) && strcmp(kind, "package-or")) ||
        !name || !*name ||
        (!strcmp(kind, "package-or") && !holy_package_or_each(name, NULL, NULL))) return 2;
    if (!list(directory, NULL, NULL, 1, 8, NULL, NULL, kind, name,
              NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6;
    return result;
}

int holy_repo_has_compatible_soname(const char *directory, const char *name,
                                    const char *root, const char *consumer_digest,
                                    const char *consumer_path)
{
    struct holy_scan_result scan = {0};
    const struct holy_scanned_file *consumer = NULL;
    char *snapshot;
    size_t i, j;
    int result = 6;
    if (!directory || !name || !*name || strchr(name, '/') || !root ||
        !consumer_digest || strlen(consumer_digest) != 64 || !consumer_path)
        return 2;
    snapshot = holy_cache_snapshot(consumer_digest, root);
    if (!snapshot) return 6;
    if (!holy_scan_collect(snapshot, &scan)) goto done;
    for (i = 0; i < scan.count; ++i) {
        const struct holy_scanned_file *file = &scan.files[i];
        if (strcmp(file->path, consumer_path)) continue;
        for (j = 0; j < file->elf.needed_count; ++j)
            if (!strcmp(file->elf.needed[j], name)) break;
        if (j == file->elf.needed_count || consumer) goto done;
        consumer = file;
    }
    if (!consumer) goto done;
    if (!list_probe(directory, NULL, NULL, 1, 8, NULL, NULL, "soname", name,
                    NULL, NULL, 0, &result, NULL, 0, NULL, NULL, consumer)) result = 6;
done:
    holy_scan_free(&scan);
    unlink(snapshot);
    free(snapshot);
    return result;
}

int holy_repo_solve(const char *directory, const char *name,
                    const char *choice, int json)
{
    int result = 6;
    if (!name || !*name) {
        fprintf(stderr, "holypkg: repository package name required\n");
        if (json) puts("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"invalid-query\"}");
        return 2;
    }
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL,
              NULL, NULL, name, choice, json, &result, NULL, 0, NULL, NULL)) {
        if (json) puts("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"invalid-catalog\"}");
        return 6;
    }
    return result;
}

int holy_repo_fetch(const char *directory, const char *digest, const char *output)
{
    size_t i;
    if (!digest || strlen(digest) != 64) goto invalid;
    for (i = 0; i < 64; ++i)
        if (!((digest[i] >= '0' && digest[i] <= '9') ||
              (digest[i] >= 'a' && digest[i] <= 'f'))) goto invalid;
    return list(directory, NULL, NULL, 1, 0, digest, output, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL);
invalid:
    fprintf(stderr, "holypkg: expected a lowercase SHA-256 digest\n");
    return 0;
}

int holy_repo_fetch_name(const char *directory, const char *name,
                         const char *output, int extract)
{
    int result = 6;
    if (!name || !*name || !output || !*output) return 2;
    if (!list(directory, NULL, NULL, 1, 0, NULL, output, NULL, NULL,
              name, NULL, 0, &result, NULL, extract, NULL, NULL)) return 6;
    return result;
}

void holy_repo_set_free(struct holy_repo_set *set)
{
    size_t i;
    for (i = 0; i < set->count; ++i) free(set->digests[i]);
    free(set->digests);
    memset(set, 0, sizeof *set);
}

int holy_repo_stage_set(const char *directory, const char *name,
                        const char *root, struct holy_repo_set *set)
{
    struct stage_request stage = {root, set, NULL, NULL, 0, 0, NULL};
    int result = 6;
    memset(set, 0, sizeof *set);
    if (!name || !*name || !root || !*root) return 2;
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
              name, NULL, 0, &result, NULL, 0, &stage, NULL)) {
        holy_repo_set_free(set);
        return 6;
    }
    if (result) holy_repo_set_free(set);
    return result;
}

int holy_repo_stage_provider(const char *directory, const char *kind,
                             const char *name, const char *root,
                             struct holy_repo_set *set)
{
    struct stage_request stage = {root, set, NULL, NULL, 0, 1, NULL};
    int result = 6;
    memset(set, 0, sizeof *set);
    if (!kind || (strcmp(kind, "package") && strcmp(kind, "package-or") && strcmp(kind, "file") &&
                  strcmp(kind, "command") && strcmp(kind, "soname")) ||
        !name || !*name || !root || !*root ||
        (!strcmp(kind, "package-or") && !holy_package_or_each(name, NULL, NULL))) return 2;
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, kind, name,
              NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) {
        holy_repo_set_free(set);
        return 6;
    }
    if (result) holy_repo_set_free(set);
    return result;
}

int holy_repo_stage_slot(const char *directory, const char *root,
                         const struct holy_package_identity *slot,
                         struct holy_repo_set *set)
{
    struct stage_request stage = {root, set, slot, NULL, 0, 0, NULL};
    int result = 6;
    memset(set, 0, sizeof *set);
    if (!root || !*root || !slot || !slot->name || !slot->os ||
        !slot->arch || !slot->libc) return 2;
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
              slot->name, NULL, 0, &result, NULL, 0, &stage, NULL)) {
        holy_repo_set_free(set);
        return 6;
    }
    if (result) holy_repo_set_free(set);
    return result;
}

void holy_repo_slot_list_free(struct holy_repo_slot_list *list)
{
    size_t i;
    if (!list) return;
    for (i = 0; i < list->count; ++i) holy_package_identity_free(&list->items[i]);
    free(list->items);
    memset(list, 0, sizeof *list);
}

int holy_repo_slot_candidates(const char *directory,
                              const struct holy_package_identity *slot,
                              struct holy_repo_slot_list *out)
{
    struct holy_repo_set index = {0};
    struct stage_request stage = {NULL, &index, slot, NULL, 1, 0, out};
    int result = 6;
    if (!directory || !slot || !slot->name || !slot->os || !slot->arch ||
        !slot->libc || !out) return 2;
    memset(out, 0, sizeof *out);
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) result = 6;
    if (result) holy_repo_slot_list_free(out);
    return result;
}

int holy_repo_stage_slot_digest(const char *directory, const char *root,
                                const struct holy_package_identity *slot,
                                const char *digest, struct holy_repo_set *set)
{
    struct stage_request stage = {root, set, slot, digest, 0, 0, NULL};
    int result = 6;
    if (!directory || !root || !slot || !slot->name || !slot->os ||
        !slot->arch || !slot->libc || !digest || strlen(digest) != 64 ||
        strspn(digest, "0123456789abcdef") != 64 || !set) return 2;
    memset(set, 0, sizeof *set);
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
              slot->name, NULL, 0, &result, NULL, 0, &stage, NULL)) result = 6;
    if (result) holy_repo_set_free(set);
    return result;
}

int holy_repo_source_catalog(const char *directory, const char *source_id,
                             const char *url, const char *public_key)
{
    struct stat st;
    char digest[65], key_hash[65], *record = NULL, *expected = NULL;
    size_t used = 0, expected_size = 0;
    int dir = -1, fd = -1, ok = 0;
    FILE *stream = NULL;
    if (!source_id || strlen(source_id) != 64 ||
        strspn(source_id, "0123456789abcdef") != 64 || !url) return 0;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || flock(dir, LOCK_SH) || read_current(dir, digest) != 1) goto done;
    if (public_key && !holy_verify_index_keyhash(dir, digest, public_key, key_hash)) goto done;
    fd = openat(dir, "mirror-origin", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        (st.st_mode & 0022) || (st.st_uid != geteuid() && st.st_uid != 0) ||
        st.st_size <= 0 || st.st_size > 65536) goto done;
    record = malloc((size_t)st.st_size + 1);
    if (!record) goto done;
    while (used < (size_t)st.st_size) {
        ssize_t n = pread(fd, record + used, (size_t)st.st_size - used, (off_t)used);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) goto done;
        used += (size_t)n;
    }
    record[used] = 0;
    stream = open_memstream(&expected, &expected_size);
    if (!stream) goto done;
    {
        int wrote = fputs("format holy-mirror-1\nurl ", stream) != EOF &&
            quote(stream, url) &&
            fprintf(stream, "\nindex-sha256 %s\nverification %s\n", digest,
                    public_key ? "ed25519-pinned-key" : "digest-pinned-unsigned") >= 0 &&
            (!public_key || fprintf(stream, "public-key-sha256 %s\n", key_hash) >= 0) &&
            fprintf(stream, "source-id %s\n", source_id) >= 0 && !ferror(stream);
        if (fclose(stream)) wrote = 0;
        stream = NULL;
        if (!wrote) goto done;
    }
    ok = (used == expected_size && !memcmp(record, expected, used)) ||
         (!public_key && used == expected_size + sizeof "selection current-accepted-unsigned\n" - 1 &&
          !memcmp(record, expected, expected_size) &&
          !memcmp(record + expected_size, "selection current-accepted-unsigned\n",
                  sizeof "selection current-accepted-unsigned\n" - 1));
done:
    if (stream) fclose(stream);
    free(record); free(expected);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    return ok;
}

int holy_repo_catalog_index(const char *directory, char digest[65])
{
    int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    int ok = 0;
    digest[0] = 0;
    if (dir >= 0 && !flock(dir, LOCK_SH) && read_current(dir, digest) == 1 &&
        list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, NULL,
             NULL, 0, NULL, NULL, 0, NULL, NULL)) ok = 1;
    if (dir >= 0) close(dir);
    if (!ok) digest[0] = 0;
    return ok;
}

int holy_repo_catalog_index_fast(const char *directory, char digest[65])
{
    struct holy_repo_set index = {0};
    struct stage_request stage = {NULL, &index, NULL, NULL, 1, 0, NULL};
    int ok = list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
                  NULL, NULL, 0, NULL, NULL, 0, &stage, NULL);
    if (ok) memcpy(digest, index.index, 65);
    else digest[0] = 0;
    return ok;
}

int holy_repo_catalog_slot_digest(const char *directory,
                                  const struct holy_package_identity *slot,
                                  const char *artifact, char index_digest[65])
{
    struct holy_repo_set index = {0};
    struct stage_request stage = {NULL, &index, slot, artifact, 1, 0, NULL};
    int result = 6;
    if (!slot || !artifact || strlen(artifact) != 64 ||
        strspn(artifact, "0123456789abcdef") != 64) return 2;
    if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL,
              NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) return 6;
    memcpy(index_digest, index.index, 65);
    return result;
}

static int seal(const char *directory, const char *expected, const char *private_key)
{
    char temporary[43] = {0}, pointer_temp[43] = {0};
    char index_name[71], digest[65], previous[65], line[73];
    char buffer[65536];
    char *snapshot = NULL;
    struct stat st;
    int dir = -1, input = -1, output = -1, pointer = -1, ok = 0;
    ssize_t got;
    size_t written;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || flock(dir, LOCK_EX) < 0) goto done;
    if (read_current(dir, previous) < 0) goto done;
    input = openat(dir, "index", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (input < 0 || fstat(input, &st) || !S_ISREG(st.st_mode) ||
        st.st_size < 0 || st.st_size > 128LL * 1024 * 1024) goto done;
    output = holy_temporary_at(dir, temporary);
    if (output < 0) goto done;
    while (lseek(output, 0, SEEK_CUR) < st.st_size) {
        off_t position = lseek(output, 0, SEEK_CUR);
        size_t amount;
        if (position < 0) goto done;
        amount = st.st_size - position < (off_t)sizeof buffer ?
                 (size_t)(st.st_size - position) : sizeof buffer;
        got = pread(input, buffer, amount, position);
        size_t offset = 0;
        if (got < 0) { if (errno == EINTR) continue; goto done; }
        if (!got) goto done;
        while (offset < (size_t)got) {
            ssize_t sent = write(output, buffer + offset, (size_t)got - offset);
            if (sent < 0 && errno == EINTR) continue;
            if (sent <= 0) goto done;
            offset += (size_t)sent;
        }
    }
    if (fstat(input, &st) || st.st_size < 0 ||
        lseek(output, 0, SEEK_CUR) != st.st_size) goto done;
    if (fchmod(output, 0644) || fsync(output)) goto done;
    close(output);
    output = -1;
    close(input);
    input = -1;
    if (!list(directory, NULL, temporary, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL)) goto done;
    input = openat(dir, temporary, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (input < 0) goto done;
    snapshot = holy_stage_fd(input, "holy-seal");
    if (!snapshot || !digest_file(snapshot, digest) ||
        (expected && strcmp(digest, expected))) goto done;
    snprintf(index_name, sizeof index_name, "index.%s", digest);
    if (linkat(dir, temporary, dir, index_name, 0)) {
        if (errno != EEXIST) goto done;
        close(input);
        input = openat(dir, index_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
        if (input < 0) goto done;
        unlink(snapshot);
        free(snapshot);
        snapshot = holy_stage_fd(input, "holy-seal");
        if (!snapshot || !digest_file(snapshot, previous) ||
            strcmp(previous, digest)) goto done;
    }
    if (fsync(dir)) goto done;
    if (private_key && !holy_sign_index(dir, digest, private_key)) goto done;
    if (fstatat(dir, "current", &st, AT_SYMLINK_NOFOLLOW) == 0) {
        if (!S_ISREG(st.st_mode)) goto done;
    } else if (errno != ENOENT) goto done;
    pointer = holy_temporary_at(dir, pointer_temp);
    if (pointer < 0) goto done;
    snprintf(line, sizeof line, "sha256 %s\n", digest);
    written = 0;
    while (written < 72) {
        ssize_t sent = write(pointer, line + written, 72 - written);
        if (sent < 0 && errno == EINTR) continue;
        if (sent <= 0) goto done;
        written += (size_t)sent;
    }
    if (fchmod(pointer, 0644) || fsync(pointer)) goto done;
    close(pointer);
    pointer = -1;
    if (renameat(dir, pointer_temp, dir, "current") || fsync(dir)) goto done;
    pointer_temp[0] = '\0';
    printf("sealed %s\n", digest);
    ok = 1;
done:
    if (!ok) fprintf(stderr, "holypkg: repository seal incomplete\n");
    if (snapshot) { unlink(snapshot); free(snapshot); }
    if (pointer >= 0) close(pointer);
    if (output >= 0) close(output);
    if (input >= 0) close(input);
    if (dir >= 0) {
        if (temporary[0]) unlinkat(dir, temporary, 0);
        if (pointer_temp[0]) unlinkat(dir, pointer_temp, 0);
        close(dir);
    }
    return ok;
}

int holy_repo_seal(const char *directory)
{
    return seal(directory, NULL, NULL);
}

int holy_repo_seal_signed(const char *directory, const char *private_key)
{
    return private_key && *private_key && seal(directory, NULL, private_key);
}

int holy_repo_verify_signature(const char *directory, const char *public_key)
{
    char digest[65];
    int dir, ok;
    if (!public_key || !*public_key) return 2;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) return 1;
    ok = !flock(dir, LOCK_SH) && holy_repo_catalog_index(directory, digest) &&
         holy_verify_index(dir, digest, public_key);
    close(dir);
    if (ok) printf("verified %s\n", digest);
    else fputs("holypkg: repository signature invalid or unavailable\n", stderr);
    return ok ? 0 : 4;
}

static int mirror_source(const char *base, const char *digest, const char *output,
                         const char *ca_file, const char *source_id,
                         int current_accepted, const char *public_key)
{
    struct mirror mirror = { base, ca_file, NULL, 0 };
    char index_name[71], signature_name[75], key_hash[65] = {0};
    char *url = NULL, *downloads = NULL;
    unsigned char signature[64];
    size_t i, length = strlen(output);
    int dir = -1, provenance = -1, sidecar = -1, result = 1;
    FILE *record = NULL;
    if (strlen(digest) != 64) return 2;
    for (i = 0; i < 64; ++i)
        if (!((digest[i] >= '0' && digest[i] <= '9') ||
              (digest[i] >= 'a' && digest[i] <= 'f'))) return 2;
    if (current_accepted && !source_id) return 2;
    if (source_id) {
        if (strlen(source_id) != 64) return 2;
        for (i = 0; i < 64; ++i)
            if (!((source_id[i] >= '0' && source_id[i] <= '9') ||
                  (source_id[i] >= 'a' && source_id[i] <= 'f'))) return 2;
    }
    snprintf(index_name, sizeof index_name, "index.%s", digest);
    url = holy_fetch_child_url(base, index_name);
    if (!url) return 2;
    if (length > (size_t)-1 - 12 || !(downloads = malloc(length + 12))) goto done;
    snprintf(downloads, length + 12, "%s/.downloads", output);
    mirror.downloads = downloads;
    if (mkdir(output, 0700)) goto done;
    dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || flock(dir, LOCK_EX) || mkdirat(dir, ".downloads", 0700)) goto done;
    result = holy_fetch_https_data(url, digest, output, ca_file);
    if (result) goto done;
    if (public_key) {
        result = holy_fetch_https_signature(base, digest, ca_file, signature);
        if (result) goto done;
        result = 4;
        if (linkat(dir, digest, dir, index_name, 0) ||
            !holy_verify_index_bytes(dir, digest, public_key, signature, key_hash)) goto done;
        snprintf(signature_name, sizeof signature_name, "signature.%s", digest);
        sidecar = openat(dir, signature_name,
                         O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0644);
        if (sidecar < 0) { result = 1; goto done; }
        for (i = 0; i < sizeof signature;) {
            ssize_t sent = write(sidecar, signature + i, sizeof signature - i);
            if (sent < 0 && errno == EINTR) continue;
            if (sent <= 0) { result = 1; goto done; }
            i += (size_t)sent;
        }
        if (fsync(sidecar)) { result = 1; goto done; }
        if (close(sidecar)) { sidecar = -1; result = 1; goto done; }
        sidecar = -1;
        if (fsync(dir)) { result = 1; goto done; }
    }
    if (!list(output, NULL, digest, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, &mirror, 0, NULL, NULL)) {
        result = mirror.status ? mirror.status : 4;
        goto done;
    }
    result = 1;
    if (linkat(dir, digest, dir, "index", 0)) goto done;
    provenance = openat(dir, "mirror-origin", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (provenance < 0 || !(record = fdopen(provenance, "w"))) goto done;
    fputs("format holy-mirror-1\nurl ", record);
    if (!quote(record, base) || fprintf(record, "\nindex-sha256 %s\nverification %s\n", digest,
                                      public_key ? "ed25519-pinned-key" : "digest-pinned-unsigned") < 0 ||
        (public_key && fprintf(record, "public-key-sha256 %s\n", key_hash) < 0) ||
        (source_id && fprintf(record, "source-id %s\n", source_id) < 0) ||
        (current_accepted && fputs("selection current-accepted-unsigned\n", record) == EOF) ||
        fflush(record) || fsync(provenance)) goto done;
    if (fclose(record)) { record = NULL; provenance = -1; goto done; }
    record = NULL; provenance = -1;
    if (unlinkat(dir, digest, 0) || unlinkat(dir, ".downloads", AT_REMOVEDIR) || fsync(dir)) goto done;
    /* seal takes its own exclusive lock after the private download phase. */
    close(dir); dir = -1;
    result = seal(output, digest, NULL) ? 0 : 4;
done:
    if (result) fprintf(stderr, "holypkg: HTTPS catalog mirror incomplete (status %d)\n", result);
    if (record) fclose(record);
    else if (provenance >= 0) close(provenance);
    if (sidecar >= 0) close(sidecar);
    if (dir >= 0) close(dir);
    free(downloads);
    free(url);
    return result;
}

int holy_repo_mirror_source(const char *base, const char *digest, const char *output,
                            const char *ca_file, const char *source_id,
                            int current_accepted)
{
    return mirror_source(base, digest, output, ca_file, source_id,
                         current_accepted, NULL);
}

int holy_repo_mirror_source_signed(const char *base, const char *digest,
                                   const char *output, const char *ca_file,
                                   const char *source_id, const char *public_key)
{
    if (!public_key || !*public_key) return 2;
    return mirror_source(base, digest, output, ca_file, source_id, 0, public_key);
}

int holy_repo_mirror(const char *base, const char *digest, const char *output,
                     const char *ca_file)
{
    return mirror_source(base, digest, output, ca_file, NULL, 0, NULL);
}

int holy_repo_mirror_signed(const char *base, const char *digest, const char *output,
                            const char *ca_file, const char *public_key)
{
    if (!public_key || !*public_key) return 2;
    return mirror_source(base, digest, output, ca_file, NULL, 0, public_key);
}