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

file backends/xbps.c

#define _XOPEN_SOURCE 700
#include "xbps.h"
#include "../src/fetch.h"
#include "../src/stage.h"
#include "../src/source.h"
#include "../src/state.h"
#include "../src/import.h"
#include "../src/package.h"
#include "../src/provides.h"

#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#ifdef __TINYC__
#define __llvm__ 1
#endif
#include <plist/plist.h>
#ifdef __TINYC__
#undef __llvm__
#endif
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

struct xbps_row {
    char *name, *version, *arch, *hash;
    char **sonames;
    size_t soname_count;
    unsigned long long size;
};

struct xbps_capability {
    const char *soname;
    const struct xbps_row *row;
};

static int row_order(const void *left, const void *right);

static int label(const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    if (!p || !*p || strlen(value) > 255 || !isalnum(*p)) return 0;
    for (; *p; ++p)
        if (!isalnum(*p) && *p != '.' && *p != '_' && *p != '+' && *p != '-' && *p != '~') return 0;
    return 1;
}

static int digest_label(const char *value)
{
    return value && strlen(value) == 64 && strspn(value, "0123456789abcdef") == 64;
}

static int hash_fd(int fd, char result[65])
{
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    unsigned char digest[32], buffer[65536];
    unsigned length;
    ssize_t got;
    size_t i;
    int ok = 0;
    if (!ctx || lseek(fd, 0, SEEK_SET) < 0 || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
    while ((got = read(fd, buffer, sizeof buffer)) > 0)
        if (EVP_DigestUpdate(ctx, buffer, (size_t)got) != 1) goto done;
    if (got || EVP_DigestFinal_ex(ctx, digest, &length) != 1 || length != 32) goto done;
    for (i = 0; i < 32; ++i) snprintf(result + i * 2, 3, "%02x", digest[i]);
    ok = 1;
done:
    EVP_MD_CTX_free(ctx);
    return ok;
}

static char *string(plist_t dict, const char *key)
{
    plist_t node = plist_dict_get_item(dict, key);
    char *value = NULL;
    if (node && plist_get_node_type(node) == PLIST_STRING) plist_get_string_val(node, &value);
    return value;
}

static EVP_PKEY *public_key_bytes(const char *data, size_t size)
{
    BIO *bio;
    EVP_PKEY *key;
    if (!data || !size || size > 16384) return NULL;
    bio = BIO_new_mem_buf(data, (int)size);
    if (!bio) return NULL;
    key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
    BIO_free(bio);
    if (key && EVP_PKEY_base_id(key) != EVP_PKEY_RSA) {
        EVP_PKEY_free(key); return NULL;
    }
    return key;
}

static EVP_PKEY *public_key_file(const char *path)
{
    struct stat st;
    char *data = NULL;
    EVP_PKEY *key = NULL;
    size_t used = 0;
    int fd = open(path, 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 > 16384) goto done;
    data = malloc((size_t)st.st_size);
    if (!data) goto done;
    while (used < (size_t)st.st_size) {
        ssize_t got = read(fd, data + used, (size_t)st.st_size - used);
        if (got < 0 && errno == EINTR) continue;
        if (got <= 0) goto done;
        used += (size_t)got;
    }
    key = public_key_bytes(data, used);
done:
    if (fd >= 0) close(fd);
    free(data);
    return key;
}

static int key_digest(EVP_PKEY *key, char output[65])
{
    unsigned char *der = NULL, *cursor, hash[32];
    unsigned length;
    int size = i2d_PUBKEY(key, NULL), ok = 0;
    size_t i;
    if (size <= 0 || size > 16384 || !(der = malloc((size_t)size))) return 0;
    cursor = der;
    if (i2d_PUBKEY(key, &cursor) != size ||
        EVP_Digest(der, (size_t)size, hash, &length, EVP_sha256(), NULL) != 1 ||
        length != 32) goto done;
    for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", hash[i]);
    ok = 1;
done:
    free(der);
    return ok;
}

int holy_xbps_key_fingerprint(const char *path, char output[65])
{
    EVP_PKEY *key = public_key_file(path);
    int ok = key && key_digest(key, output);
    EVP_PKEY_free(key);
    return ok;
}

static int index_key(const char *xml, size_t size, const char *trusted_path, char hash[65])
{
    plist_t root = NULL, node, bits_node;
    EVP_PKEY *advertised = NULL, *trusted = NULL;
    char *algorithm = NULL;
    const char *data;
    uint64_t length = 0, bits = 0;
    int ok = 0;
    if (size > UINT32_MAX || plist_from_xml(xml, (uint32_t)size, &root) != PLIST_ERR_SUCCESS ||
        !root || plist_get_node_type(root) != PLIST_DICT) goto done;
    node = plist_dict_get_item(root, "public-key");
    if (!node) { ok = !trusted_path; goto done; }
    if (plist_get_node_type(node) != PLIST_DATA) goto done;
    algorithm = string(root, "signature-type");
    bits_node = plist_dict_get_item(root, "public-key-size");
    if (bits_node && plist_get_node_type(bits_node) == PLIST_INT)
        plist_get_uint_val(bits_node, &bits);
    data = plist_get_data_ptr(node, &length);
    if (!algorithm || strcmp(algorithm, "rsa") || !data || length > 16384 ||
        !(advertised = public_key_bytes(data, (size_t)length)) ||
        !bits_node || bits != (uint64_t)EVP_PKEY_bits(advertised) ||
        !key_digest(advertised, hash)) goto done;
    if (!trusted_path) { ok = 1; goto done; }
    trusted = public_key_file(trusted_path);
    ok = trusted && EVP_PKEY_eq(advertised, trusted) == 1;
done:
    if (!ok) hash[0] = 0;
    free(algorithm); EVP_PKEY_free(advertised); EVP_PKEY_free(trusted); plist_free(root);
    return ok;
}

static int verify_signature(const char *package, const char *signature, EVP_PKEY *key)
{
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    struct stat st;
    unsigned char *sig = NULL, buffer[65536];
    size_t used = 0;
    ssize_t got;
    int package_fd = -1, signature_fd = -1, ok = 0;
    signature_fd = open(signature, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    package_fd = open(package, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (!ctx || signature_fd < 0 || package_fd < 0 ||
        fstat(signature_fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size <= 0 || st.st_size > 16384 ||
        !(sig = malloc((size_t)st.st_size)) ||
        EVP_DigestVerifyInit(ctx, NULL, EVP_sha256(), NULL, key) != 1) goto done;
    while (used < (size_t)st.st_size) {
        got = read(signature_fd, sig + used, (size_t)st.st_size - used);
        if (got < 0 && errno == EINTR) continue;
        if (got <= 0) goto done;
        used += (size_t)got;
    }
    while ((got = read(package_fd, buffer, sizeof buffer)) > 0)
        if (EVP_DigestVerifyUpdate(ctx, buffer, (size_t)got) != 1) goto done;
    if (got || EVP_DigestVerifyFinal(ctx, sig, used) != 1) goto done;
    ok = 1;
done:
    if (signature_fd >= 0) close(signature_fd);
    if (package_fd >= 0) close(package_fd);
    EVP_MD_CTX_free(ctx); free(sig);
    return ok;
}

static void free_rows(struct xbps_row *rows, size_t count)
{
    size_t i, j;
    for (i = 0; i < count; ++i) {
        free(rows[i].name); free(rows[i].version);
        free(rows[i].arch); free(rows[i].hash);
        for (j = 0; j < rows[i].soname_count; ++j) free(rows[i].sonames[j]);
        free(rows[i].sonames);
    }
    free(rows);
}

static int soname_label(const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    size_t length;
    if (!p || !(length = strlen(value)) || length > 255) return 0;
    for (; *p; ++p) if (*p < 33 || *p > 126) return 0;
    return 1;
}

static int capability_order(const void *left, const void *right)
{
    const struct xbps_capability *a = left, *b = right;
    int result = strcmp(a->soname, b->soname);
    return result ? result : row_order(a->row, b->row);
}

static int row_order(const void *left, const void *right)
{
    const struct xbps_row *a = left, *b = right;
    int result = strcmp(a->name, b->name);
    if (!result) result = strcmp(a->version, b->version);
    if (!result) result = strcmp(a->arch, b->arch);
    return result;
}

static int parse_rows(const char *xml, size_t size, struct xbps_row **out, size_t *count)
{
    plist_t root = NULL, entry = NULL;
    plist_dict_iter iter = NULL;
    struct xbps_row *rows = NULL;
    char *key = NULL;
    size_t used = 0, capacity = 0, i, total_sonames = 0;
    int ok = 0;
    if (size > UINT32_MAX || plist_from_xml(xml, (uint32_t)size, &root) != PLIST_ERR_SUCCESS ||
        !root || plist_get_node_type(root) != PLIST_DICT) goto done;
    plist_dict_new_iter(root, &iter);
    if (!iter) goto done;
    for (;;) {
        struct xbps_row *row;
        char *pkgver = NULL;
        plist_t bytes;
        uint64_t length = 0;
        plist_dict_next_item(root, iter, &key, &entry);
        if (!key) break;
        if (!label(key) || !entry || plist_get_node_type(entry) != PLIST_DICT || used == 100000) goto done;
        if (used == capacity) {
            size_t next = capacity ? capacity * 2 : 256;
            struct xbps_row *grown = realloc(rows, next * sizeof *rows);
            if (!grown) goto done;
            rows = grown; capacity = next;
        }
        row = &rows[used++]; memset(row, 0, sizeof *row);
        row->name = key; key = NULL;
        pkgver = string(entry, "pkgver");
        row->arch = string(entry, "architecture");
        row->hash = string(entry, "filename-sha256");
        bytes = plist_dict_get_item(entry, "filename-size");
        if (bytes && plist_get_node_type(bytes) == PLIST_INT) plist_get_uint_val(bytes, &length);
        if (!pkgver || strlen(pkgver) <= strlen(row->name) + 1 ||
            strncmp(pkgver, row->name, strlen(row->name)) ||
            pkgver[strlen(row->name)] != '-' ||
            !(row->version = strdup(pkgver + strlen(row->name) + 1)) ||
            !label(row->version) || !label(row->arch) || !digest_label(row->hash) ||
            !bytes || plist_get_node_type(bytes) != PLIST_INT || !length ||
            length > 1024ULL * 1024 * 1024 * 1024) { free(pkgver); goto done; }
        row->size = length;
        {
            plist_t claims = plist_dict_get_item(entry, "shlib-provides");
            if (claims) {
                uint32_t j, amount;
                if (plist_get_node_type(claims) != PLIST_ARRAY ||
                    (amount = plist_array_get_size(claims)) > 1024 ||
                    total_sonames > 1000000 - amount) { free(pkgver); goto done; }
                row->sonames = calloc(amount ? amount : 1, sizeof *row->sonames);
                if (!row->sonames) { free(pkgver); goto done; }
                for (j = 0; j < amount; ++j) {
                    plist_t claim = plist_array_get_item(claims, j);
                    if (!claim || plist_get_node_type(claim) != PLIST_STRING) {
                        free(pkgver); goto done;
                    }
                    plist_get_string_val(claim, &row->sonames[j]);
                    if (!soname_label(row->sonames[j])) {
                        free(row->sonames[j]); row->sonames[j] = NULL;
                        free(pkgver); goto done;
                    }
                    ++row->soname_count;
                }
                total_sonames += amount;
            }
        }
        free(pkgver);
    }
    if (!used) goto done;
    qsort(rows, used, sizeof *rows, row_order);
    for (i = 1; i < used; ++i)
        if (!row_order(&rows[i-1], &rows[i])) goto done;
    *out = rows; *count = used; rows = NULL; used = 0; ok = 1;
done:
    free(key); free(iter); plist_free(root); free_rows(rows, used);
    return ok;
}

static int read_repodata(const char *path, char **xml, size_t *size,
                         char **meta, size_t *meta_size)
{
    struct archive *archive = archive_read_new();
    struct archive_entry *entry;
    int status, found = 0, metadata = 0, ok = 0;
    if (!archive) return 0;
    if (archive_read_support_filter_zstd(archive) != ARCHIVE_OK ||
        archive_read_support_filter_gzip(archive) != ARCHIVE_OK ||
        archive_read_support_filter_xz(archive) != ARCHIVE_OK ||
        archive_read_support_filter_bzip2(archive) != ARCHIVE_OK ||
        archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
        archive_read_support_format_tar(archive) != ARCHIVE_OK ||
        archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
        const char *name = archive_entry_pathname(entry);
        la_int64_t length = archive_entry_size(entry);
        if (!name || archive_entry_filetype(entry) != AE_IFREG ||
            archive_entry_hardlink(entry) || archive_entry_xattr_count(entry) ||
            archive_entry_acl_types(entry)) goto done;
        if (!strcmp(name, "index.plist")) {
            size_t used = 0;
            if (found || length <= 0 || length > 64LL * 1024 * 1024) goto done;
            *xml = malloc((size_t)length + 1);
            if (!*xml) goto done;
            while (used < (size_t)length) {
                la_ssize_t got = archive_read_data(archive, *xml + used, (size_t)length - used);
                if (got <= 0) goto done;
                used += (size_t)got;
            }
            if (memchr(*xml, 0, used)) goto done;
            (*xml)[used] = 0; *size = used; found = 1;
        } else if (!strcmp(name, "index-meta.plist")) {
            size_t used = 0;
            if (metadata++ || length <= 0 || length > 1024 * 1024) goto done;
            *meta = malloc((size_t)length + 1);
            if (!*meta) goto done;
            while (used < (size_t)length) {
                la_ssize_t got = archive_read_data(archive, *meta + used, (size_t)length - used);
                if (got <= 0) goto done;
                used += (size_t)got;
            }
            if (memchr(*meta, 0, used)) goto done;
            (*meta)[used] = 0; *meta_size = used;
        } else if (!strcmp(name, "stage.plist")) {
            if (length != 0 || archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
        } else goto done;
    }
    ok = status == ARCHIVE_EOF && found && metadata == 1;
done:
    archive_read_free(archive);
    return ok;
}

static int package_identity(const char *path, const char *name,
                            const char *version, const char *arch)
{
    struct archive *archive = archive_read_new();
    struct archive_entry *entry;
    plist_t props = NULL;
    char *xml = NULL, *pkgname = NULL, *pkgver = NULL, *source_arch = NULL;
    int found = 0, status, ok = 0;
    if (!archive) return 0;
    if (archive_read_support_filter_gzip(archive) != ARCHIVE_OK ||
        archive_read_support_filter_zstd(archive) != ARCHIVE_OK ||
        archive_read_support_filter_xz(archive) != ARCHIVE_OK ||
        archive_read_support_filter_bzip2(archive) != ARCHIVE_OK ||
        archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
        archive_read_support_format_tar(archive) != ARCHIVE_OK ||
        archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
        const char *entry_name = archive_entry_pathname(entry);
        la_int64_t length = archive_entry_size(entry);
        if (entry_name && (!strcmp(entry_name, "props.plist") ||
                           !strcmp(entry_name, "./props.plist"))) {
            size_t used = 0;
            if (found || archive_entry_filetype(entry) != AE_IFREG ||
                length <= 0 || length > 1024 * 1024) goto done;
            xml = malloc((size_t)length + 1);
            if (!xml) goto done;
            while (used < (size_t)length) {
                la_ssize_t got = archive_read_data(archive, xml + used, (size_t)length - used);
                if (got <= 0) goto done;
                used += (size_t)got;
            }
            if (memchr(xml, 0, used) ||
                plist_from_xml(xml, (uint32_t)used, &props) != PLIST_ERR_SUCCESS ||
                !props || plist_get_node_type(props) != PLIST_DICT) goto done;
            found = 1;
        } else if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
    }
    if (status != ARCHIVE_EOF || !found) goto done;
    pkgname = string(props, "pkgname");
    pkgver = string(props, "pkgver");
    source_arch = string(props, "architecture");
    if (pkgname && pkgver && source_arch && !strcmp(pkgname, name) &&
        !strcmp(source_arch, arch) && strlen(pkgver) == strlen(name) + strlen(version) + 1 &&
        !strncmp(pkgver, name, strlen(name)) && pkgver[strlen(name)] == '-' &&
        !strcmp(pkgver + strlen(name) + 1, version)) ok = 1;
done:
    free(pkgname); free(pkgver); free(source_arch); free(xml);
    plist_free(props); archive_read_free(archive);
    return ok;
}

static int write_capabilities(int dir, const struct xbps_row *rows, size_t count,
                              char output[65])
{
    struct xbps_capability *caps = NULL;
    FILE *stream = NULL;
    size_t total = 0, used = 0, i, j;
    int fd = -1, ok = 0;
    for (i = 0; i < count; ++i) {
        if (rows[i].soname_count > (size_t)-1 - total) return 0;
        total += rows[i].soname_count;
    }
    caps = calloc(total ? total : 1, sizeof *caps);
    if (!caps) return 0;
    for (i = 0; i < count; ++i)
        for (j = 0; j < rows[i].soname_count; ++j) {
            caps[used].soname = rows[i].sonames[j];
            caps[used++].row = &rows[i];
        }
    qsort(caps, used, sizeof *caps, capability_order);
    fd = openat(dir, "capabilities", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (fd < 0 || !(stream = fdopen(fd, "w"))) goto done;
    fd = -1;
    fputs("format holy-xbps-capabilities-1\n", stream);
    for (i = 0; i < used; ++i) {
        const struct xbps_row *row = caps[i].row;
        if (i && !capability_order(&caps[i-1], &caps[i])) continue;
        fprintf(stream, "soname %s %s %s %s %s\n", caps[i].soname,
                row->name, row->version, row->arch, row->hash);
    }
    {
        int failed = ferror(stream) || fflush(stream) || fsync(fileno(stream));
        if (fclose(stream)) failed = 1;
        stream = NULL;
        if (failed) goto done;
    }
    fd = openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd >= 0) ok = hash_fd(fd, output);
done:
    if (stream) fclose(stream);
    if (fd >= 0) close(fd);
    free(caps);
    return ok;
}

int holy_xbps_index(const char *input, const char *source, const char *base,
                    const char *output, const char *expected, const char *public_key,
                    const char *source_id)
{
    struct xbps_row *rows = NULL;
    struct stat st;
    char *xml = NULL, *meta = NULL, *url = NULL;
    char filename[70], original[65], catalog_hash[65], capabilities_hash[65];
    char key_hash[65] = {0};
    size_t size = 0, meta_size = 0, count = 0, i;
    FILE *catalog = NULL, *record = NULL;
    int dir = -1, fd = -1, result = 1;
    if (!input || !source || !label(source) || !strcmp(source, "local") || !base || !output ||
        !digest_label(expected) || (source_id && !digest_label(source_id)) ||
        !(url = holy_fetch_child_url(base, "repodata"))) {
        free(url); return 2;
    }
    free(url);
    if (mkdir(output, 0700)) return 1;
    dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() ||
        (st.st_mode & 0777) != 0700 || !holy_fetch_at(input, dir, expected, filename) ||
        renameat(dir, filename, dir, "repodata")) goto done;
    fd = openat(dir, "repodata", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 64LL * 1024 * 1024 ||
        !hash_fd(fd, original) || strcmp(original, expected)) goto done;
    {
        char descriptor[64];
        snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fd);
        result = 2;
        if (!read_repodata(descriptor, &xml, &size, &meta, &meta_size) ||
            !parse_rows(xml, size, &rows, &count)) goto done;
    }
    free(xml); xml = NULL;
    if (!index_key(meta, meta_size, public_key, key_hash)) { result = 4; goto done; }
    result = 1;
    {
        int catalog_fd = openat(dir, "catalog", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (catalog_fd < 0) goto done;
        catalog = fdopen(catalog_fd, "w");
        if (!catalog) { close(catalog_fd); goto done; }
    }
    fputs("format holy-xbps-catalog-1\n", catalog);
    for (i = 0; i < count; ++i)
        fprintf(catalog, "package %s %s %s %s %llu\n", rows[i].name, rows[i].version,
                rows[i].arch, rows[i].hash, rows[i].size);
    if (ferror(catalog) || fflush(catalog) || fsync(fileno(catalog))) goto done;
    if (fclose(catalog)) { catalog = NULL; goto done; }
    catalog = NULL;
    {
        int catalog_fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
        if (catalog_fd < 0) goto done;
        result = hash_fd(catalog_fd, catalog_hash) ? 1 : 2;
        close(catalog_fd);
        if (!result) goto done;
    }
    if (!write_capabilities(dir, rows, count, capabilities_hash)) goto done;
    {
        int record_fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (record_fd < 0) goto done;
        record = fdopen(record_fd, "w");
        if (!record) { close(record_fd); goto done; }
    }
    fprintf(record, "format holy-xbps-index-record-1\nsource %s\nbase %s\noriginal-sha256 %s\ncatalog-sha256 %s\ncapabilities-sha256 %s\nverification %s\n",
            source, base, original, catalog_hash, capabilities_hash,
            public_key ? "key-matched" : "hash-pinned");
    if (source_id) fprintf(record, "source-id %s\n", source_id);
    if (key_hash[0]) fprintf(record, "public-key-sha256 %s\n", key_hash);
    fprintf(record, "package-coverage complete\nfile-coverage unavailable\npackages %zu\nstate complete\n", count);
    if (ferror(record) || fflush(record) || fsync(fileno(record))) goto done;
    if (fclose(record)) { record = NULL; goto done; }
    record = NULL;
    if (fsync(dir)) goto done;
    printf("indexed %zu XBPS packages %s\n", count, original);
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: XBPS index incomplete (status %d)\n", result);
    if (record) fclose(record);
    if (catalog) fclose(catalog);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    free(xml); free(meta); free_rows(rows, count);
    return result;
}

struct catalog_state {
    FILE *catalog;
    char source[256], base[2048], original[65], catalog_hash[65];
    char capabilities_hash[65], key_hash[65], source_id[65];
};

static int open_catalog(const char *directory, struct catalog_state *state)
{
    struct stat st;
    FILE *record = NULL;
    char line[4096], actual[65];
    int dir = -1, fd, complete = 0, ok = 0;
    memset(state, 0, sizeof *state);
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || fstat(dir, &st) || !S_ISDIR(st.st_mode) ||
        st.st_uid != geteuid() ||
        (st.st_mode & 0022)) goto done;
    fd = openat(dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) goto done;
    record = fdopen(fd, "r");
    if (!record) { close(fd); goto done; }
    if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096 ||
        !fgets(line, sizeof line, record) ||
        strcmp(line, "format holy-xbps-index-record-1\n")) goto done;
    while (fgets(line, sizeof line, record)) {
        size_t length = strlen(line);
        if (!length || line[length - 1] != '\n') goto done;
        line[length - 1] = 0;
        if (!strncmp(line, "source ", 7)) {
            if (state->source[0] || strlen(line + 7) >= sizeof state->source) goto done;
            strcpy(state->source, line + 7);
        } else if (!strncmp(line, "source-id ", 10)) {
            if (state->source_id[0] || strlen(line + 10) >= sizeof state->source_id) goto done;
            strcpy(state->source_id, line + 10);
        } else if (!strncmp(line, "base ", 5)) {
            if (state->base[0] || strlen(line + 5) >= sizeof state->base) goto done;
            strcpy(state->base, line + 5);
        } else if (!strncmp(line, "original-sha256 ", 16)) {
            if (state->original[0] || strlen(line + 16) >= sizeof state->original) goto done;
            strcpy(state->original, line + 16);
        } else if (!strncmp(line, "catalog-sha256 ", 15)) {
            if (state->catalog_hash[0] || strlen(line + 15) >= sizeof state->catalog_hash) goto done;
            strcpy(state->catalog_hash, line + 15);
        } else if (!strncmp(line, "capabilities-sha256 ", 20)) {
            if (state->capabilities_hash[0] ||
                strlen(line + 20) >= sizeof state->capabilities_hash) goto done;
            strcpy(state->capabilities_hash, line + 20);
        } else if (!strncmp(line, "public-key-sha256 ", 18)) {
            if (state->key_hash[0] || strlen(line + 18) >= sizeof state->key_hash) goto done;
            strcpy(state->key_hash, line + 18);
        } else if (!strcmp(line, "state complete")) complete = 1;
    }
    if (ferror(record) || !complete || !label(state->source) ||
        !digest_label(state->original) || !digest_label(state->catalog_hash) ||
        (state->capabilities_hash[0] && !digest_label(state->capabilities_hash)) ||
        (state->key_hash[0] && !digest_label(state->key_hash)) ||
        (state->source_id[0] && !digest_label(state->source_id))) goto done;
    {
        char *url = holy_fetch_child_url(state->base, "fixture.xbps");
        if (!url) goto done;
        free(url);
    }
    fd = openat(dir, "repodata", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || !hash_fd(fd, actual) || strcmp(actual, state->original)) {
        if (fd >= 0) close(fd);
        goto done;
    }
    close(fd);
    fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) goto done;
    if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 32LL * 1024 * 1024 ||
        !hash_fd(fd, actual) || strcmp(actual, state->catalog_hash) ||
        lseek(fd, 0, SEEK_SET) < 0) { close(fd); goto done; }
    state->catalog = fdopen(fd, "r");
    if (!state->catalog) { close(fd); goto done; }
    if (!fgets(line, sizeof line, state->catalog) ||
        strcmp(line, "format holy-xbps-catalog-1\n")) goto done;
    if (state->capabilities_hash[0]) {
        fd = openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
        if (fd < 0) goto done;
        if (fstat(fd, &st) || !S_ISREG(st.st_mode) ||
            st.st_uid != geteuid() || (st.st_mode & 0022) ||
            st.st_size <= 0 || st.st_size > 128LL * 1024 * 1024 ||
            !hash_fd(fd, actual) || strcmp(actual, state->capabilities_hash)) {
            close(fd); goto done;
        }
        close(fd);
    }
    ok = 1;
done:
    if (!ok && state->catalog) { fclose(state->catalog); state->catalog = NULL; }
    if (record) fclose(record);
    if (dir >= 0) close(dir);
    return ok;
}

int holy_xbps_source_catalog(const char *directory, const char *id,
                             const char *base, const char *key)
{
    struct catalog_state state;
    int ok;
    if (!directory || !id || !base || !key ||
        !open_catalog(directory, &state)) return 6;
    ok = !strcmp(state.source_id, id) &&
         !strcmp(state.base, base) &&
         (!key[0] || !strcmp(state.key_hash, key));
    fclose(state.catalog);
    return ok ? 0 : 6;
}

static int xbps_binding_name(const char *id, const char *arch, char name[130])
{
    unsigned char digest[32];
    unsigned length;
    size_t i;
    if (!digest_label(id) || !label(arch) ||
        EVP_Digest(arch, strlen(arch), digest, &length, EVP_sha256(), NULL) != 1 ||
        length != 32) return 0;
    memcpy(name, id, 64);
    name[64] = '.';
    for (i = 0; i < 32; ++i) snprintf(name + 65 + 2 * i, 3, "%02x", digest[i]);
    return 1;
}

static int xbps_binding_dir(int database, int create)
{
    struct stat st;
    int dir;
    if (create && mkdirat(database, "xbps-catalogs", 0700) && errno != EEXIST) return -1;
    dir = openat(database, "xbps-catalogs", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) return -1;
    if (fstat(dir, &st) || !S_ISDIR(st.st_mode) || st.st_uid != geteuid() ||
        (st.st_mode & 0022)) { close(dir); return -1; }
    return dir;
}

int holy_xbps_bind(const char *root, const char *source, const char *arch,
                   const char *catalog)
{
    char id[65], key[65], name[130], digest[65], temporary[43] = {0};
    char *base = NULL, *trust = NULL, *path = NULL, *root_path = NULL, *record = NULL;
    const char *stored_path, *path_key;
    unsigned long long generation;
    size_t size, used = 0;
    int database = -1, dir = -1, fd = -1, catalog_dir = -1, result;
    result = holy_source_xbps(root, source, id, &base, &trust, key);
    if (result) goto done;
    if (!xbps_binding_name(id, arch, name) || !catalog ||
        !(path = realpath(catalog, NULL))) { result = 2; goto done; }
    if (strchr(path, '\n') || strlen(path) > 1024 * 1024) { result = 2; goto done; }
    result = holy_xbps_source_catalog(path, id, base, key);
    if (result) goto done;
    root_path = realpath(root, NULL);
    if (!root_path) { result = 6; goto done; }
    path_key = "path";
    stored_path = path;
    if (strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
        path[strlen(root_path)] == '/') {
        path_key = "root-path";
        stored_path = path + strlen(root_path);
    }
    catalog_dir = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    fd = catalog_dir < 0 ? -1 : openat(catalog_dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || !hash_fd(fd, digest)) { result = 6; goto done; }
    close(fd); fd = -1;
    size = strlen(path) + strlen(arch) + 256;
    record = malloc(size);
    if (!record) { result = 1; goto done; }
    snprintf(record, size, "format holy-xbps-binding-1\nsource-id %s\narch %s\nconversion-sha256 %s\n%s %s\n",
             id, arch, digest, path_key, stored_path);
    size = strlen(record);
    database = holy_state_lock(root, 1, &generation, &result);
    if (database < 0) goto done;
    dir = xbps_binding_dir(database, 1);
    if (dir < 0 || (fd = holy_temporary_at(dir, temporary)) < 0) { result = 1; goto done; }
    while (used < size) {
        ssize_t written = write(fd, record + used, size - used);
        if (written < 0 && errno == EINTR) continue;
        if (written <= 0) { result = 1; goto done; }
        used += (size_t)written;
    }
    if (fsync(fd) || renameat(dir, temporary, dir, name) || fsync(dir)) {
        result = 1; goto done;
    }
    result = 0;
done:
    if (fd >= 0) close(fd);
    if (dir >= 0) {
        if (temporary[0]) unlinkat(dir, temporary, 0);
        close(dir);
    }
    if (catalog_dir >= 0) close(catalog_dir);
    if (database >= 0) close(database);
    free(base); free(trust); free(path); free(root_path); free(record);
    return result;
}

int holy_xbps_catalog_path(const char *root, const char *source,
                           const char *arch, char **catalog)
{
    struct stat st;
    char id[65], key[65], name[130], saved_id[65], saved_arch[256];
    char saved_digest[65], actual[65], *base = NULL, *trust = NULL;
    char *line = NULL, *path = NULL, *root_path = NULL, *candidate = NULL;
    size_t capacity = 0;
    unsigned long long generation;
    FILE *record = NULL;
    int database = -1, dir = -1, fd = -1, catalog_dir = -1, result;
    *catalog = NULL;
    result = holy_source_xbps(root, source, id, &base, &trust, key);
    if (result) goto done;
    if (!xbps_binding_name(id, arch, name)) { result = 2; goto done; }
    database = holy_state_lock(root, 0, &generation, &result);
    if (database < 0) goto done;
    dir = xbps_binding_dir(database, 0);
    fd = dir < 0 ? -1 : openat(dir, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size <= 0 || st.st_size > 1024 * 1024 + 512 ||
        st.st_uid != geteuid() || (st.st_mode & 0022)) { result = 6; goto done; }
    record = fdopen(fd, "r");
    if (!record) { result = 1; goto done; }
    fd = -1;
    if (getline(&line, &capacity, record) < 0 ||
        strcmp(line, "format holy-xbps-binding-1\n") ||
        getline(&line, &capacity, record) < 0 ||
        sscanf(line, "source-id %64s\n", saved_id) != 1 || strcmp(saved_id, id) ||
        getline(&line, &capacity, record) < 0 ||
        sscanf(line, "arch %255s\n", saved_arch) != 1 || strcmp(saved_arch, arch) ||
        getline(&line, &capacity, record) < 0 ||
        sscanf(line, "conversion-sha256 %64s\n", saved_digest) != 1 ||
        !digest_label(saved_digest) || getline(&line, &capacity, record) < 0 ||
        (strncmp(line, "path ", 5) && strncmp(line, "root-path ", 10)) ||
        strlen(line) > 1024 * 1024 + 12 ||
        line[strlen(line) - 1] != '\n' ||
        fgetc(record) != EOF || ferror(record)) { result = 6; goto done; }
    line[strlen(line) - 1] = 0;
    if (!strncmp(line, "root-path ", 10)) {
        root_path = realpath(root, NULL);
        if (!root_path || line[10] != '/' ||
            strlen(root_path) > (size_t)-1 - strlen(line + 10) - 1) {
            result = 6; goto done;
        }
        candidate = malloc(strlen(root_path) + strlen(line + 10) + 1);
        if (!candidate) { result = 1; goto done; }
        if (!strcmp(root_path, "/")) strcpy(candidate, line + 10);
        else sprintf(candidate, "%s%s", root_path, line + 10);
    } else candidate = strdup(line + 5);
    if (!candidate) { result = 1; goto done; }
    path = realpath(candidate, NULL);
    if (!path || strcmp(path, candidate)) { result = 6; goto done; }
    catalog_dir = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    fd = catalog_dir < 0 ? -1 : openat(catalog_dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || !hash_fd(fd, actual) || strcmp(actual, saved_digest)) {
        result = 6; goto done;
    }
    result = holy_xbps_source_catalog(path, id, base, key);
    if (!result) { *catalog = path; path = NULL; }
done:
    if (fd >= 0) close(fd);
    if (record) fclose(record);
    if (catalog_dir >= 0) close(catalog_dir);
    if (dir >= 0) close(dir);
    if (database >= 0) close(database);
    free(base); free(trust); free(line); free(path); free(root_path); free(candidate);
    return result;
}

static int next_row(FILE *catalog, struct xbps_row *row)
{
    char line[1024], name[256], version[256], arch[256], hash[65];
    unsigned long long size;
    int end = 0;
    if (!fgets(line, sizeof line, catalog)) return ferror(catalog) ? -1 : 0;
    if (sscanf(line, "package %255s %255s %255s %64s %llu%n",
               name, version, arch, hash, &size, &end) != 5 ||
        !end || line[end] != '\n' || line[end + 1] ||
        !label(name) || !label(version) || !label(arch) ||
        !digest_label(hash) || !size || size > 1024ULL * 1024 * 1024 * 1024) return -1;
    row->name = strdup(name); row->version = strdup(version);
    row->arch = strdup(arch); row->hash = strdup(hash); row->size = size;
    return row->name && row->version && row->arch && row->hash ? 1 : -1;
}

int holy_xbps_providers(const char *directory, const char *soname)
{
    struct catalog_state state;
    struct stat st;
    char line[1200], actual[65], *matches = NULL;
    size_t matches_size = 0, count = 0;
    FILE *input = NULL, *out = NULL;
    int dir = -1, fd = -1, result = 6;
    if (!soname_label(soname) || !open_catalog(directory, &state)) return 6;
    fclose(state.catalog);
    if (!state.capabilities_hash[0]) goto done;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    fd = dir < 0 ? -1 : openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_uid != geteuid() || (st.st_mode & 0022) ||
        st.st_size <= 0 || st.st_size > 128LL * 1024 * 1024 ||
        !hash_fd(fd, actual) || strcmp(actual, state.capabilities_hash) ||
        lseek(fd, 0, SEEK_SET) < 0 || !(input = fdopen(fd, "r"))) goto done;
    fd = -1;
    if (!fgets(line, sizeof line, input) ||
        strcmp(line, "format holy-xbps-capabilities-1\n")) goto done;
    out = open_memstream(&matches, &matches_size);
    if (!out) { result = 1; goto done; }
    while (fgets(line, sizeof line, input)) {
        char found[256], name[256], version[256], arch[256], hash[65];
        int end = 0;
        if (sscanf(line, "soname %255s %255s %255s %255s %64s%n",
                   found, name, version, arch, hash, &end) != 5 ||
            !end || line[end] != '\n' || line[end + 1] ||
            !soname_label(found) || !label(name) || !label(version) ||
            !label(arch) || !digest_label(hash)) { result = 2; goto done; }
        if (strcmp(found, soname)) continue;
        if (++count > 10000) { result = 6; goto done; }
        fprintf(out, "candidate %s %s %s %s index-hint\n",
                name, version, arch, hash);
    }
    {
        int failed = ferror(input) || ferror(out);
        if (fclose(out)) failed = 1;
        out = NULL;
        if (failed) { result = 1; goto done; }
    }
    if (matches_size && fwrite(matches, 1, matches_size, stdout) != matches_size) {
        result = 1; goto done;
    }
    printf("listed %zu candidates coverage partial\n", count);
    result = 0;
done:
    if (out) fclose(out);
    if (input) fclose(input);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    free(matches);
    return result;
}

int holy_xbps_query(const char *directory, const char *query, int info)
{
    struct catalog_state state;
    struct xbps_row row = {0};
    size_t matches = 0;
    int status, result = 2;
    if (!directory || !query || !*query || !open_catalog(directory, &state)) return 6;
    while ((status = next_row(state.catalog, &row)) > 0) {
        if ((info && !strcmp(query, row.name)) || (!info && strstr(row.name, query))) {
            ++matches;
            if (info) printf("package %s\nversion %s\narch %s\nsha256 %s\nsize %llu\n",
                             row.name, row.version, row.arch, row.hash, row.size);
            else printf("%s %s %s\n", row.name, row.version, row.arch);
        }
        free(row.name); free(row.version); free(row.arch); free(row.hash);
        memset(&row, 0, sizeof row);
    }
    result = status < 0 ? 2 : !matches ? 4 : info && matches > 1 ? 3 : 0;
    free(row.name); free(row.version); free(row.arch); free(row.hash);
    fclose(state.catalog);
    return result;
}

static int imported_arch_matches(const char *source, const char *output)
{
    if (!strcmp(output, "noarch")) return 1;
    if (!strncmp(source, "x86_64", 6))
        return !strcmp(output, "x86_64") || !strcmp(output, "x86");
    return !strncmp(source, "i686", 4) && !strcmp(output, "x86");
}

static int imported_claim(const char *directory, const char *name, const char *version,
                          const char *arch, const char *soname)
{
    DIR *dir = opendir(directory);
    struct dirent *entry;
    const char *release = strrchr(version, '_');
    size_t count = 0;
    int found = 0, ok = 0;
    if (!dir || !release || release == version || !release[1]) goto done;
    errno = 0;
    while ((entry = readdir(dir))) {
        struct holy_package_identity identity = {0};
        size_t length = strlen(entry->d_name);
        char *path;
        int matched = 0;
        if (length < 6 || strcmp(entry->d_name + length - 5, ".holy")) continue;
        path = malloc(strlen(directory) + length + 2);
        if (!path) goto done;
        sprintf(path, "%s/%s", directory, entry->d_name);
        if (!holy_package_identity(path, &identity) ||
            strcmp(identity.name, name) ||
            strlen(identity.version) != (size_t)(release - version) ||
            strncmp(identity.version, version, (size_t)(release - version)) ||
            strcmp(identity.release, release + 1) ||
            !imported_arch_matches(arch, identity.arch) ||
            (soname && !holy_provides_match(path, "soname", soname, &matched))) {
            holy_package_identity_free(&identity); free(path); goto done;
        }
        if (matched) found = 1;
        holy_package_identity_free(&identity); free(path);
        ++count;
        errno = 0;
    }
    ok = count && !errno && (!soname || found);
done:
    if (dir) closedir(dir);
    return ok;
}

int holy_xbps_fetch(const char *directory, const char *name, const char *version,
                    const char *arch, const char *output, const char *ca_file,
                    const char *public_key, int source_checked, int import,
                    const char *required_soname)
{
    struct catalog_state state;
    struct xbps_row row = {0}, selected = {0};
    EVP_PKEY *key = NULL;
    char digest[65] = {0}, signature_digest[65] = {0}, key_hash[65] = {0};
    char *filename = NULL, *url = NULL, *download = NULL, *converted = NULL;
    char *signature_name = NULL, *signature_url = NULL, *signature_path = NULL;
    FILE *record = NULL;
    struct stat st;
    size_t matches = 0, length;
    int status, result = 1, dir = -1;
    if (!directory || !label(name) || !label(version) || !label(arch) || !output ||
        (required_soname && (!import || !soname_label(required_soname)))) return 2;
    if (!open_catalog(directory, &state)) return 6;
    while ((status = next_row(state.catalog, &row)) > 0) {
        if (!strcmp(row.name, name) && !strcmp(row.version, version) && !strcmp(row.arch, arch)) {
            if (!matches) { selected = row; memset(&row, 0, sizeof row); }
            ++matches;
        }
        free(row.name); free(row.version); free(row.arch); free(row.hash);
        memset(&row, 0, sizeof row);
        if (matches > 1) break;
    }
    fclose(state.catalog);
    if (status < 0) { result = 2; goto done; }
    if (!matches) { result = 4; goto done; }
    if (matches > 1) { result = 3; goto done; }
    if (public_key) {
        key = public_key_file(public_key);
        if (!key || !key_digest(key, key_hash) ||
            !state.key_hash[0] || strcmp(key_hash, state.key_hash)) {
            result = 4; goto done;
        }
    }
    if (selected.size > 1024ULL * 1024 * 1024) { result = 6; goto done; }
    length = strlen(name) + strlen(version) + strlen(arch) + 8;
    filename = malloc(length);
    if (!filename) goto done;
    snprintf(filename, length, "%s-%s.%s.xbps", name, version, arch);
    url = holy_fetch_child_url(state.base, filename);
    if (!url) { result = 2; goto done; }
    if (mkdir(output, 0700)) goto done;
    dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() ||
        (st.st_mode & 0777) != 0700) goto done;
    result = holy_fetch_https_foreign_limited(url, output, ca_file, digest, selected.size);
    if (result) goto done;
    if (strcmp(digest, selected.hash)) { result = 4; goto done; }
    download = malloc(strlen(output) + 66);
    if (!download) { result = 1; goto done; }
    sprintf(download, "%s/%s", output, digest);
    if (stat(download, &st) || !S_ISREG(st.st_mode) ||
        (unsigned long long)st.st_size != selected.size ||
        !package_identity(download, name, version, arch)) {
        result = 4; goto done;
    }
    if (key) {
        signature_name = malloc(strlen(filename) + 6);
        signature_path = malloc(strlen(output) + 66);
        if (!signature_name || !signature_path) { result = 1; goto done; }
        sprintf(signature_name, "%s.sig2", filename);
        signature_url = holy_fetch_child_url(state.base, signature_name);
        if (!signature_url) { result = 2; goto done; }
        result = holy_fetch_https_foreign_limited(signature_url, output, ca_file,
                                                  signature_digest, 16384);
        if (result) goto done;
        sprintf(signature_path, "%s/%s", output, signature_digest);
        if (!verify_signature(download, signature_path, key)) {
            result = 4; goto done;
        }
    }
    if (import) {
        converted = malloc(strlen(output) + sizeof "/converted");
        if (!converted) { result = 1; goto done; }
        sprintf(converted, "%s/converted", output);
        result = holy_import_xbps_verified(download, state.source, converted, digest,
                                           key ? "rsa-sha256" : "hash-pinned",
                                           key ? key_hash : NULL,
                                           key ? signature_digest : NULL,
                                           state.original, state.base);
        if (result) goto done;
        if (!imported_claim(converted, name, version, arch, required_soname)) {
            if (required_soname)
                fprintf(stderr, "holypkg: XBPS payload does not provide SONAME %s\n", required_soname);
            result = 4; goto done;
        }
    }
    {
        int fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (fd < 0) { result = 1; goto done; }
        record = fdopen(fd, "w");
        if (!record) { close(fd); result = 1; goto done; }
    }
    fprintf(record, "format holy-xbps-fetch-record-1\nsource %s\nindex-sha256 %s\npackage %s\nversion %s\narch %s\nsha256 %s\nsize %llu\nverification %s\n",
            state.source, state.original, name, version, arch, digest, selected.size,
            key ? "rsa-sha256" : "hash-pinned");
    if (key) fprintf(record, "public-key-sha256 %s\nsignature-sha256 %s\n",
                     key_hash, signature_digest);
    if (source_checked && state.source_id[0])
        fprintf(record, "source-id %s\nsource-binding checked\n", state.source_id);
    if (required_soname) fprintf(record, "required-soname %s\nsoname-provider verified-payload\n", required_soname);
    fprintf(record, "imported %s\n", import ? "yes" : "no");
    fputs("state complete\n", record);
    if (ferror(record) || fflush(record) || fsync(fileno(record))) { result = 1; goto done; }
    if (fclose(record)) { record = NULL; result = 1; goto done; }
    record = NULL;
    if (fsync(dir)) { result = 1; goto done; }
    printf("fetched %s sha256 %s%s\n", filename, digest, import ? " imported" : "");
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: XBPS fetch incomplete (status %d)\n", result);
    if (record) fclose(record);
    if (dir >= 0) close(dir);
    free(selected.name); free(selected.version); free(selected.arch); free(selected.hash);
    EVP_PKEY_free(key);
    free(filename); free(url); free(download); free(converted);
    free(signature_name); free(signature_url); free(signature_path);
    return result;
}

int holy_xbps_sync(const char *base, const char *arch, const char *source,
                   const char *output, const char *expected, const char *ca_file,
                   const char *public_key, const char *source_id)
{
    char template[] = "/tmp/holy-xbps-sync-XXXXXX";
    char *url = NULL, *path = NULL, *filename = NULL;
    int result;
    if (!base || !arch || !label(arch) || !source || !label(source) ||
        !strcmp(source, "local") || !output || !digest_label(expected)) return 2;
    filename = malloc(strlen(arch) + 10);
    if (!filename) return 1;
    sprintf(filename, "%s-repodata", arch);
    url = holy_fetch_child_url(base, filename);
    free(filename);
    if (!url) return 2;
    if (!mkdtemp(template)) { free(url); return 1; }
    result = holy_fetch_https_data(url, expected, template, ca_file);
    free(url);
    if (!result) {
        path = malloc(strlen(template) + 66);
        if (!path) result = 1;
        else {
            sprintf(path, "%s/%s", template, expected);
            result = holy_xbps_index(path, source, base, output, expected, public_key, source_id);
        }
    }
    if (path) { unlink(path); free(path); }
    rmdir(template);
    return result;
}