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

file backends/rpm-md.c

#define _XOPEN_SOURCE 700
#include "rpm-md.h"

#ifndef HOLY_HAVE_RPMMD
#include <stdio.h>
int holy_rpm_md_index(const char *a, const char *b, const char *c, const char *d,
                      const char *e, const char *f, const char *g)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g;
  fputs("holypkg: rpm-md requires librpm and libxml2 at build time\n", stderr); return 6; }
int holy_rpm_md_sync(const char *a, const char *b, const char *c, const char *d,
                     const char *e, const char *f)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; return 6; }
int holy_rpm_md_query(const char *a, const char *b, int c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_providers(const char *a, const char *b)
{ (void)a; (void)b; return 6; }
int holy_rpm_md_fetch(const char *a, const char *b, const char *c, const char *d,
                      const char *e, const char *f, int g)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; return 6; }
int holy_rpm_md_source_catalog(const char *a, const char *b, const char *c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_bind(const char *a, const char *b, const char *c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_catalog_path(const char *a, const char *b, char **c)
{ (void)a; (void)b; (void)c; return 6; }
#else

#include "rpm-version.h"
#include "../src/fetch.h"
#include "../src/import.h"
#include "../src/source.h"
#include "../src/stage.h"
#include "../src/state.h"

#include <archive.h>
#include <libxml/xmlreader.h>
#include <openssl/evp.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

struct rpmmd_row {
    char name[256], evr[256], arch[64], hash[65], href[1024];
    unsigned long long size;
    char **provides;
    size_t provide_count;
};

/* rpm declares capabilities such as libfoo.so.1(N) or plain names. */
static int provide_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;
}

struct rpmmd_catalog {
    FILE *file;
    char source[256], base[2048], repomd[65], primary[65], catalog[65];
    char capabilities[65], source_id[65];
};

static int digest(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 int label(const char *value, size_t limit)
{
    const unsigned char *p = (const unsigned char *)value;
    size_t n = value ? strlen(value) : 0;
    if (!n || n >= limit || !isalnum(*p)) return 0;
    for (; *p; ++p)
        if (!isalnum(*p) && *p != '.' && *p != '_' && *p != '+' && *p != '-') return 0;
    return 1;
}

static int href_valid(const char *href)
{
    const char *p = href, *part = href;
    size_t n = href ? strlen(href) : 0;
    if (!n || n >= 1024 || *p == '/') return 0;
    for (; *p; ++p) {
        if (*p == '/') {
            size_t size = (size_t)(p - part);
            if (!size || (size == 1 && *part == '.') ||
                (size == 2 && !memcmp(part, "..", 2))) return 0;
            part = p + 1;
        } else if (!isalnum((unsigned char)*p) && *p != '.' && *p != '_' &&
                   *p != '-' && *p != '+') return 0;
    }
    return p != part && strcmp(part, ".") && strcmp(part, "..");
}

static int number(const char *value, unsigned long long *out)
{
    char *end;
    unsigned long long n;
    if (!value || !*value || *value == '-') return 0;
    errno = 0;
    n = strtoull(value, &end, 10);
    if (errno || *end) return 0;
    *out = n;
    return 1;
}

static int hash_file(const char *path, char hash[65], off_t limit)
{
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    struct stat st;
    unsigned char bytes[65536], digest_bytes[32];
    unsigned digest_size;
    ssize_t got;
    size_t i;
    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK), ok = 0;
    if (!ctx || fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size < 0 || st.st_size > limit ||
        EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
    while ((got = read(fd, bytes, sizeof bytes)) > 0)
        if (EVP_DigestUpdate(ctx, bytes, (size_t)got) != 1) goto done;
    if (got || EVP_DigestFinal_ex(ctx, digest_bytes, &digest_size) != 1 || digest_size != 32) goto done;
    for (i = 0; i < 32; ++i) snprintf(hash + i * 2, 3, "%02x", digest_bytes[i]);
    ok = 1;
done:
    if (fd >= 0) close(fd);
    EVP_MD_CTX_free(ctx);
    return ok;
}

static int attr(xmlTextReaderPtr reader, const char *name, char *out, size_t capacity)
{
    xmlChar *value = xmlTextReaderGetAttribute(reader, (const xmlChar *)name);
    size_t length;
    if (!value) return 0;
    length = strlen((const char *)value);
    if (!length || length >= capacity) { xmlFree(value); return 0; }
    memcpy(out, value, length + 1);
    xmlFree(value);
    return 1;
}

static int element(xmlTextReaderPtr reader, const char *name)
{
    const xmlChar *current = xmlTextReaderConstLocalName(reader);
    return current && !xmlStrcmp(current, (const xmlChar *)name);
}

/* primary metadata carries capability entries in the rpm namespace. */
static int rpm_element(xmlTextReaderPtr reader)
{
    const xmlChar *prefix = xmlTextReaderConstPrefix(reader);
    return prefix && !xmlStrcmp(prefix, (const xmlChar *)"rpm");
}

static int contents(xmlTextReaderPtr reader, char *out, size_t capacity)
{
    xmlChar *value = xmlTextReaderReadString(reader);
    size_t length;
    if (!value) return 0;
    length = strlen((const char *)value);
    if (!length || length >= capacity) { xmlFree(value); return 0; }
    memcpy(out, value, length + 1);
    xmlFree(value);
    return 1;
}

static int repomd_primary(const char *path, char href[1024], char hash[65])
{
    xmlTextReaderPtr reader = xmlReaderForFile(path, NULL, XML_PARSE_NONET | XML_PARSE_COMPACT |
                                              XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
    int step, in_primary = 0, found = 0, root = 0, ok = 0;
    if (!reader) return 0;
    while ((step = xmlTextReaderRead(reader)) == 1) {
        int type = xmlTextReaderNodeType(reader), depth = xmlTextReaderDepth(reader);
        if (type == XML_READER_TYPE_DOCUMENT_TYPE) goto done;
        if (type == XML_READER_TYPE_ELEMENT && depth == 0) {
            if (root++ || !element(reader, "repomd")) goto done;
        } else if (type == XML_READER_TYPE_ELEMENT && depth == 1 && element(reader, "data")) {
            char kind[64] = {0};
            if (!attr(reader, "type", kind, sizeof kind)) goto done;
            in_primary = !strcmp(kind, "primary");
            if (in_primary && found++) goto done;
        } else if (in_primary && type == XML_READER_TYPE_ELEMENT && depth == 2) {
            if (element(reader, "checksum")) {
                char kind[32];
                if (hash[0] || !attr(reader, "type", kind, sizeof kind) ||
                    strcmp(kind, "sha256") || !contents(reader, hash, 65)) goto done;
            } else if (element(reader, "location")) {
                if (href[0] || !attr(reader, "href", href, 1024)) goto done;
            }
        } else if (type == XML_READER_TYPE_END_ELEMENT && depth == 1 &&
                   element(reader, "data")) in_primary = 0;
    }
    ok = step == 0 && root == 1 && found == 1 && digest(hash) && href_valid(href);
done:
    xmlFreeTextReader(reader);
    return ok;
}

static int unpack_primary(const char *path, FILE *out)
{
    struct archive *archive = archive_read_new();
    struct archive_entry *entry;
    char bytes[65536];
    la_ssize_t got;
    unsigned long long total = 0;
    int ok = 0;
    if (!archive) return 0;
    if (archive_read_support_filter_all(archive) != ARCHIVE_OK ||
        archive_read_support_format_raw(archive) != ARCHIVE_OK ||
        archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK ||
        archive_read_next_header(archive, &entry) != ARCHIVE_OK) goto done;
    while ((got = archive_read_data(archive, bytes, sizeof bytes)) > 0) {
        if (total > 1024ULL * 1024 * 1024 - (unsigned long long)got ||
            fwrite(bytes, 1, (size_t)got, out) != (size_t)got) goto done;
        total += (unsigned long long)got;
    }
    if (got || !total || archive_read_next_header(archive, &entry) != ARCHIVE_EOF ||
        fflush(out) || fseeko(out, 0, SEEK_SET)) goto done;
    ok = 1;
done:
    archive_read_free(archive);
    return ok;
}

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

struct rpmmd_capability {
    const char *provides;
    const struct rpmmd_row *row;
};

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

static int write_capabilities(int dir, const struct rpmmd_row *rows, size_t count,
                              char output[65])
{
    struct rpmmd_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].provide_count > (size_t)-1 - total) return 0;
        total += rows[i].provide_count;
    }
    caps = calloc(total ? total : 1, sizeof *caps);
    if (!caps) return 0;
    for (i = 0; i < count; ++i)
        for (j = 0; j < rows[i].provide_count; ++j) {
            caps[used].provides = rows[i].provides[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-rpm-md-capabilities-1\n", stream);
    for (i = 0; i < used; ++i) {
        const struct rpmmd_row *row = caps[i].row;
        if (i && !capability_order(&caps[i-1], &caps[i])) continue;
        fprintf(stream, "provides %s %s %s %s %s\n", caps[i].provides,
                row->name, row->evr, 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;
}

static int parse_primary(FILE *primary, struct rpmmd_row **result, size_t *count)
{
    xmlTextReaderPtr reader;
    struct rpmmd_row *rows = NULL, row = {0};
    size_t used = 0, capacity = 0;
    unsigned long long declared = 0;
    int fd = dup(fileno(primary)), step, root = 0, in_package = 0, in_provides = 0, ok = 0;
    if (fd < 0) return 0;
    reader = xmlReaderForFd(fd, NULL, NULL, XML_PARSE_NONET | XML_PARSE_COMPACT |
                           XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
    if (!reader) { close(fd); return 0; }
    while ((step = xmlTextReaderRead(reader)) == 1) {
        int type = xmlTextReaderNodeType(reader), depth = xmlTextReaderDepth(reader);
        if (type == XML_READER_TYPE_DOCUMENT_TYPE) goto done;
        if (type == XML_READER_TYPE_ELEMENT && depth == 0) {
            char packages[32];
            if (root++ || !element(reader, "metadata") ||
                !attr(reader, "packages", packages, sizeof packages) ||
                !number(packages, &declared) || declared > 100000) goto done;
        } else if (type == XML_READER_TYPE_ELEMENT && depth == 1 && element(reader, "package")) {
            char kind[16];
            if (in_package || !attr(reader, "type", kind, sizeof kind) || strcmp(kind, "rpm")) goto done;
            memset(&row, 0, sizeof row);
            in_package = 1;
        } else if (in_package && type == XML_READER_TYPE_ELEMENT && depth == 2) {
            if (element(reader, "name")) {
                if (row.name[0] || !contents(reader, row.name, sizeof row.name)) goto done;
            } else if (element(reader, "arch")) {
                if (row.arch[0] || !contents(reader, row.arch, sizeof row.arch)) goto done;
            } else if (element(reader, "version")) {
                char epoch[32] = "0", version[128], release[64];
                unsigned long long epoch_number;
                int length;
                if (row.evr[0] || !attr(reader, "ver", version, sizeof version) ||
                    !attr(reader, "rel", release, sizeof release)) goto done;
                if (xmlTextReaderHasAttributes(reader)) {
                    xmlChar *value = xmlTextReaderGetAttribute(reader, (const xmlChar *)"epoch");
                    if (value) {
                        size_t n = strlen((const char *)value);
                        if (!n || n >= sizeof epoch) { xmlFree(value); goto done; }
                        memcpy(epoch, value, n + 1); xmlFree(value);
                    }
                }
                if (!number(epoch, &epoch_number)) goto done;
                length = epoch_number ? snprintf(row.evr, sizeof row.evr, "%llu:%s-%s",
                    epoch_number, version, release) :
                    snprintf(row.evr, sizeof row.evr, "%s-%s", version, release);
                if (length < 0 || (size_t)length >= sizeof row.evr ||
                    !holy_rpm_version_valid(row.evr)) goto done;
            } else if (element(reader, "checksum")) {
                char kind[32];
                if (row.hash[0] || !attr(reader, "type", kind, sizeof kind) ||
                    strcmp(kind, "sha256") || !contents(reader, row.hash, sizeof row.hash)) goto done;
            } else if (element(reader, "size")) {
                char size[32];
                if (row.size || !attr(reader, "package", size, sizeof size) ||
                    !number(size, &row.size)) goto done;
            } else if (element(reader, "location")) {
                if (row.href[0] || !attr(reader, "href", row.href, sizeof row.href)) goto done;
            } else if (element(reader, "provides") && rpm_element(reader)) {
                in_provides = 1;
            }
        } else if (in_package && in_provides && type == XML_READER_TYPE_ELEMENT &&
                   element(reader, "entry") && rpm_element(reader)) {
            char capability[256];
            char **grown;
            if (row.provide_count >= 4096 || !attr(reader, "name", capability, sizeof capability) ||
                !provide_label(capability)) goto done;
            grown = realloc(row.provides, (row.provide_count + 1) * sizeof *grown);
            if (!grown) goto done;
            row.provides = grown;
            row.provides[row.provide_count] = strdup(capability);
            if (!row.provides[row.provide_count]) goto done;
            ++row.provide_count;
        } else if (in_package && type == XML_READER_TYPE_END_ELEMENT && depth == 2 &&
                   element(reader, "provides") && rpm_element(reader)) {
            in_provides = 0;
        } else if (in_package && type == XML_READER_TYPE_END_ELEMENT && depth == 1 &&
                   element(reader, "package")) {
            struct rpmmd_row *grown;
            if (!label(row.name, sizeof row.name) || !label(row.arch, sizeof row.arch) ||
                !digest(row.hash) || !href_valid(row.href) ||
                !row.size || row.size > 1024ULL * 1024 * 1024 || used >= 100000) goto done;
            if (used == capacity) {
                size_t next = capacity ? capacity * 2 : 64;
                grown = realloc(rows, next * sizeof *rows);
                if (!grown) goto done;
                rows = grown; capacity = next;
            }
            rows[used++] = row;
            memset(&row, 0, sizeof row);
            in_package = 0;
        }
    }
    if (step || root != 1 || in_package || in_provides || used != declared) goto done;
    qsort(rows, used, sizeof *rows, row_order);
    for (size_t i = 1; i < used; ++i)
        if (!row_order(&rows[i-1], &rows[i])) {
            for (i = 0; i < used; ++i) {
                size_t j;
                for (j = 0; j < rows[i].provide_count; ++j) free(rows[i].provides[j]);
                free(rows[i].provides);
            }
            free(rows);
            return 0;
        }
    *result = rows; *count = used; rows = NULL; ok = 1;
done:
    xmlFreeTextReader(reader);
    if (rows) {
        size_t i, j;
        for (i = 0; i < used; ++i) {
            for (j = 0; j < rows[i].provide_count; ++j) free(rows[i].provides[j]);
            free(rows[i].provides);
        }
        free(rows);
    }
    for (size_t j = 0; j < row.provide_count; ++j) free(row.provides[j]);
    free(row.provides);
    return ok;
}

static char *path_join(const char *directory, const char *name)
{
    size_t a = strlen(directory), b = strlen(name);
    char *out;
    if (a > SIZE_MAX - b - 2 || !(out = malloc(a + b + 2))) return NULL;
    memcpy(out, directory, a);
    out[a] = '/'; memcpy(out + a + 1, name, b + 1);
    return out;
}

static char *child_url(const char *base, const char *href)
{
    char *copy = NULL, *save = NULL, *part, *current = NULL;
    if (!href_valid(href) || !(copy = strdup(href)) || !(current = strdup(base))) goto done;
    for (part = strtok_r(copy, "/", &save); part; part = strtok_r(NULL, "/", &save)) {
        char *next = holy_fetch_child_url(current, part);
        free(current); current = next;
        if (!current) goto done;
        if (save && *save) {
            size_t length = strlen(current);
            next = realloc(current, length + 2);
            if (!next) { free(current); current = NULL; goto done; }
            current = next; current[length] = '/'; current[length + 1] = 0;
        }
    }
done:
    free(copy);
    return current;
}

int holy_rpm_md_index(const char *repomd, const char *primary, const char *expected,
                      const char *source, const char *base, const char *output,
                      const char *source_id)
{
    struct rpmmd_row *rows = NULL;
    struct stat st;
    char href[1024] = {0}, primary_hash[65] = {0}, actual[65], catalog_hash[65];
    char capabilities_hash[65] = {0};
    char staged[70], *repomd_path = NULL, *primary_path = NULL, *catalog_path = NULL;
    char *test_url = NULL;
    FILE *expanded = NULL, *catalog = NULL, *record = NULL;
    size_t count = 0, i, provides = 0;
    int dir = -1, result = 1;
    if (!repomd || !primary || !expected || !digest(expected) ||
        !source || !label(source, 256) || !strcmp(source, "local") ||
        (source_id && !digest(source_id)) ||
        !base || !output || !(test_url = child_url(base, "repodata/repomd.xml"))) {
        free(test_url); return 2;
    }
    free(test_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(repomd, dir, expected, staged) ||
        renameat(dir, staged, dir, "repomd")) goto done;
    repomd_path = path_join(output, "repomd");
    primary_path = path_join(output, "primary");
    catalog_path = path_join(output, "catalog");
    result = 2;
    if (!repomd_path || !primary_path || !catalog_path ||
        !hash_file(repomd_path, actual, 16 * 1024 * 1024) || strcmp(actual, expected) ||
        !repomd_primary(repomd_path, href, primary_hash) ||
        !holy_fetch_at(primary, dir, primary_hash, staged) ||
        renameat(dir, staged, dir, "primary") ||
        !hash_file(primary_path, actual, 128 * 1024 * 1024) || strcmp(actual, primary_hash)) goto done;
    expanded = tmpfile();
    if (!expanded || !unpack_primary(primary_path, expanded) ||
        !parse_primary(expanded, &rows, &count)) goto done;
    result = 1;
    {
        int fd = openat(dir, "catalog", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (fd < 0) goto done;
        catalog = fdopen(fd, "w");
        if (!catalog) { close(fd); goto done; }
    }
    fputs("format holy-rpm-md-catalog-1\n", catalog);
    for (i = 0; i < count; ++i)
        fprintf(catalog, "package %s %s %s %s %llu %s\n", rows[i].name, rows[i].evr,
                rows[i].arch, rows[i].hash, rows[i].size, rows[i].href);
    if (ferror(catalog) || fflush(catalog) || fsync(fileno(catalog))) goto done;
    if (fclose(catalog)) { catalog = NULL; goto done; }
    catalog = NULL;
    if (!hash_file(catalog_path, catalog_hash, 256 * 1024 * 1024)) goto done;
    if (!write_capabilities(dir, rows, count, capabilities_hash)) goto done;
    for (i = 0; i < count; ++i) provides += rows[i].provide_count;
    {
        int fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (fd < 0) goto done;
        record = fdopen(fd, "w");
        if (!record) { close(fd); goto done; }
    }
    fprintf(record, "format holy-rpm-md-index-1\nsource %s\nbase %s\nrepomd-sha256 %s\nprimary-sha256 %s\ncatalog-sha256 %s\ncapabilities-sha256 %s\n",
            source, base, expected, primary_hash, catalog_hash, capabilities_hash);
    if (source_id) fprintf(record, "source-id %s\n", source_id);
    fprintf(record, "packages %zu\nprovides %zu\nfile-coverage unavailable\ndependency-coverage partial\nstate complete\n",
            count, provides);
    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 RPM packages %s\n", count, expected);
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: rpm-md index incomplete (status %d)\n", result);
    if (catalog) fclose(catalog);
    if (record) fclose(record);
    if (expanded) fclose(expanded);
    if (dir >= 0) close(dir);
    free(rows); free(repomd_path); free(primary_path); free(catalog_path);
    return result;
}

static int open_catalog(const char *directory, struct rpmmd_catalog *state)
{
    char *record_path = path_join(directory, "conversion");
    char *catalog_path = path_join(directory, "catalog");
    char *capabilities_path = path_join(directory, "capabilities");
    char *repomd_path = path_join(directory, "repomd");
    char *primary_path = path_join(directory, "primary");
    char line[4096], actual[65];
    struct stat st;
    FILE *record = NULL;
    int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    int fd, complete = 0, ok = 0;
    memset(state, 0, sizeof *state);
    if (dir < 0 || !record_path || !catalog_path || !capabilities_path ||
        !repomd_path || !primary_path ||
        fstat(dir, &st) || 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-rpm-md-index-1\n")) goto done;
    while (fgets(line, sizeof line, record)) {
        char *value, *target = NULL;
        size_t capacity = 0, length = strlen(line);
        if (!length || line[length-1] != '\n') goto done;
        line[length-1] = 0;
        value = strchr(line, ' ');
        if (value) *value++ = 0;
        if (!strcmp(line, "source")) { target = state->source; capacity = sizeof state->source; }
        else if (!strcmp(line, "base")) { target = state->base; capacity = sizeof state->base; }
        else if (!strcmp(line, "repomd-sha256")) { target = state->repomd; capacity = sizeof state->repomd; }
        else if (!strcmp(line, "primary-sha256")) { target = state->primary; capacity = sizeof state->primary; }
        else if (!strcmp(line, "catalog-sha256")) { target = state->catalog; capacity = sizeof state->catalog; }
        else if (!strcmp(line, "capabilities-sha256")) {
            target = state->capabilities; capacity = sizeof state->capabilities;
        }
        else if (!strcmp(line, "source-id")) { target = state->source_id; capacity = sizeof state->source_id; }
        else if (!strcmp(line, "state") && value && !strcmp(value, "complete") && !complete) {
            complete = 1; continue;
        } else if (!strcmp(line, "packages") || !strcmp(line, "provides") ||
                   !strcmp(line, "file-coverage") ||
                   !strcmp(line, "dependency-coverage")) continue;
        else goto done;
        if (!value || !*value || strlen(value) >= capacity || target[0]) goto done;
        strcpy(target, value);
    }
    if (ferror(record) || !complete || !label(state->source, sizeof state->source) ||
        !digest(state->repomd) || !digest(state->primary) || !digest(state->catalog) ||
        !digest(state->capabilities) ||
        (state->source_id[0] && !digest(state->source_id)) ||
        !hash_file(repomd_path, actual, 16 * 1024 * 1024) || strcmp(actual, state->repomd) ||
        !hash_file(primary_path, actual, 128 * 1024 * 1024) || strcmp(actual, state->primary) ||
        !hash_file(catalog_path, actual, 256 * 1024 * 1024) || strcmp(actual, state->catalog) ||
        !hash_file(capabilities_path, actual, 256 * 1024 * 1024) ||
        strcmp(actual, state->capabilities)) goto done;
    {
        char *url = child_url(state->base, "repodata/repomd.xml");
        if (!url) goto done;
        free(url);
    }
    fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) goto done;
    state->file = fdopen(fd, "r");
    if (!state->file) { close(fd); goto done; }
    if (!fgets(line, sizeof line, state->file) || strcmp(line, "format holy-rpm-md-catalog-1\n")) goto done;
    ok = 1;
done:
    if (!ok && state->file) { fclose(state->file); state->file = NULL; }
    if (record) fclose(record);
    if (dir >= 0) close(dir);
    free(record_path); free(catalog_path); free(capabilities_path);
    free(repomd_path); free(primary_path);
    return ok;
}

static int next_row(FILE *file, struct rpmmd_row *row)
{
    char line[2048], extra;
    int parsed;
    if (!fgets(line, sizeof line, file)) return ferror(file) ? -1 : 0;
    if (!strchr(line, '\n')) return -1;
    memset(row, 0, sizeof *row);
    parsed = sscanf(line, "package %255s %255s %63s %64s %llu %1023s %c",
                    row->name, row->evr, row->arch, row->hash, &row->size, row->href, &extra);
    return parsed == 6 && label(row->name, sizeof row->name) &&
           holy_rpm_version_valid(row->evr) && label(row->arch, sizeof row->arch) &&
           digest(row->hash) && href_valid(row->href) && row->size > 0 &&
           row->size <= 1024ULL * 1024 * 1024 ? 1 : -1;
}

/* exact capability lookup; an absent hint is not proof of absence in the source. */
int holy_rpm_md_providers(const char *directory, const char *capability)
{
    struct rpmmd_catalog catalog;
    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 (!capability || !provide_label(capability) || !open_catalog(directory, &catalog)) return 6;
    fclose(catalog.file);
    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 > 256LL * 1024 * 1024 ||
        !hash_fd(fd, actual) || strcmp(actual, catalog.capabilities) ||
        lseek(fd, 0, SEEK_SET) < 0 || !(input = fdopen(fd, "r"))) goto done;
    fd = -1;
    if (!fgets(line, sizeof line, input) ||
        strcmp(line, "format holy-rpm-md-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], evr[256], arch[64], hash[65];
        int end = 0;
        if (sscanf(line, "provides %255s %255s %255s %63s %64s%n",
                   found, name, evr, arch, hash, &end) != 5 ||
            !end || line[end] != '\n' || line[end + 1] ||
            !provide_label(found) || !label(name, 256) ||
            !holy_rpm_version_valid(evr) || !label(arch, 64) || !digest(hash)) {
            result = 2; goto done;
        }
        if (strcmp(found, capability)) continue;
        if (++count > 10000) { result = 6; goto done; }
        fprintf(out, "candidate %s %s %s %s index-hint\n", name, evr, 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_rpm_md_query(const char *directory, const char *query, int info)
{
    struct rpmmd_catalog catalog;
    struct rpmmd_row row;
    size_t matches = 0;
    int status;
    if (!directory || !query || !*query) return 2;
    if (!open_catalog(directory, &catalog)) return 6;
    while ((status = next_row(catalog.file, &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\npath %s\n",
                             row.name, row.evr, row.arch, row.hash, row.size, row.href);
            else printf("%s %s %s\n", row.name, row.evr, row.arch);
        }
    }
    fclose(catalog.file);
    return status < 0 ? 2 : !matches ? 4 : info && matches > 1 ? 3 : 0;
}

/* the bound catalog must name this exact source-id and mirror base. */
int holy_rpm_md_source_catalog(const char *directory, const char *id, const char *base)
{
    struct rpmmd_catalog state;
    int ok;
    if (!directory || !digest(id) || !base || !open_catalog(directory, &state)) return 6;
    ok = !strcmp(state.source_id, id) && !strcmp(state.base, base);
    fclose(state.file);
    return ok ? 0 : 6;
}

static int binding_name(const char *id, char name[130])
{
    unsigned char sum[32];
    unsigned length;
    size_t i;
    if (!digest(id) ||
        EVP_Digest(id, strlen(id), sum, &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", sum[i]);
    return 1;
}

static int binding_dir(int database, int create)
{
    struct stat st;
    int dir;
    if (create && mkdirat(database, "rpm-md-catalogs", 0700) && errno != EEXIST) return -1;
    dir = openat(database, "rpm-md-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_rpm_md_bind(const char *root, const char *source, const char *catalog)
{
    char id[65], name[130], digest_value[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_rpm_md(root, source, id, &base, &trust);
    if (result) goto done;
    if (!binding_name(id, name) || !catalog || !(path = realpath(catalog, NULL))) {
        result = 2; goto done;
    }
    if (strchr(path, '\n') || strlen(path) > 1024 * 1024) { result = 2; goto done; }
    result = holy_rpm_md_source_catalog(path, id, base);
    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_value)) { result = 6; goto done; }
    close(fd); fd = -1;
    size = strlen(path) + 256;
    record = malloc(size);
    if (!record) { result = 1; goto done; }
    snprintf(record, size, "format holy-rpm-md-binding-1\nsource-id %s\nconversion-sha256 %s\n%s %s\n",
             id, digest_value, path_key, stored_path);
    size = strlen(record);
    database = holy_state_lock(root, 1, &generation, &result);
    if (database < 0) goto done;
    dir = 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_rpm_md_catalog_path(const char *root, const char *source, char **catalog)
{
    struct stat st;
    char id[65], name[130], saved_id[65], saved_digest[65], actual[65];
    char *base = NULL, *trust = NULL, *line = NULL, *path = NULL;
    char *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_rpm_md(root, source, id, &base, &trust);
    if (result) goto done;
    if (!binding_name(id, name)) { result = 2; goto done; }
    database = holy_state_lock(root, 0, &generation, &result);
    if (database < 0) goto done;
    dir = 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-rpm-md-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, "conversion-sha256 %64s\n", saved_digest) != 1 ||
        !digest(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_rpm_md_source_catalog(path, id, base);
    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;
}

int holy_rpm_md_sync(const char *base, const char *expected, const char *source,
                     const char *output, const char *ca_file, const char *source_id)
{
    char temporary[] = "/tmp/holy-rpm-md-XXXXXX", href[1024] = {0};
    char hash[65] = {0}, received[65] = {0};
    char *repomd_url = NULL, *primary_url = NULL, *repomd = NULL, *primary = NULL;
    int result = 2;
    if (!base || !digest(expected) || !source || !label(source, 256) ||
        (source_id && !digest(source_id)) || !output ||
        !(repomd_url = child_url(base, "repodata/repomd.xml"))) goto done;
    if (!mkdtemp(temporary)) { result = 1; goto done; }
    repomd = path_join(temporary, expected);
    if (!repomd) { result = 1; goto cleanup; }
    result = holy_fetch_https_data(repomd_url, expected, temporary, ca_file);
    if (result) goto cleanup;
    result = 2;
    if (!repomd_primary(repomd, href, hash) || !(primary_url = child_url(base, href))) goto cleanup;
    result = holy_fetch_https_foreign_limited(primary_url, temporary, ca_file, received,
                                              128ULL * 1024 * 1024);
    if (result) goto cleanup;
    if (strcmp(hash, received)) { result = 4; goto cleanup; }
    primary = path_join(temporary, hash);
    if (!primary) { result = 1; goto cleanup; }
    result = holy_rpm_md_index(repomd, primary, expected, source, base, output, source_id);
cleanup:
    if (primary) unlink(primary);
    if (repomd) unlink(repomd);
    rmdir(temporary);
done:
    free(repomd_url); free(primary_url); free(repomd); free(primary);
    return result;
}

int holy_rpm_md_fetch(const char *directory, const char *name, const char *evr,
                      const char *arch, const char *output, const char *ca_file,
                      int import)
{
    struct rpmmd_catalog catalog;
    struct rpmmd_row row, selected = {0};
    struct stat st;
    char digest_bytes[65] = {0};
    char *url = NULL, *download = NULL, *converted = NULL;
    size_t matches = 0;
    int status, result = 1;
    if (!directory || !label(name, 256) || !holy_rpm_version_valid(evr) ||
        !label(arch, 64) || !output) return 2;
    if (!open_catalog(directory, &catalog)) return 6;
    while ((status = next_row(catalog.file, &row)) > 0) {
        if (!strcmp(name, row.name) && !strcmp(evr, row.evr) && !strcmp(arch, row.arch)) {
            if (!matches) selected = row;
            ++matches;
        }
    }
    fclose(catalog.file);
    if (status < 0) return 2;
    if (!matches) return 4;
    if (matches > 1) return 3;
    url = child_url(catalog.base, selected.href);
    if (!url) { result = 2; goto done; }
    if (mkdir(output, 0700)) goto done;
    result = holy_fetch_https_foreign_limited(url, output, ca_file, digest_bytes, selected.size);
    if (result) goto done;
    if (strcmp(digest_bytes, selected.hash)) { result = 4; goto done; }
    download = path_join(output, digest_bytes);
    if (!download) { result = 1; goto done; }
    if (stat(download, &st) || !S_ISREG(st.st_mode) ||
        (unsigned long long)st.st_size != selected.size) { result = 4; goto done; }
    if (import) {
        converted = path_join(output, "converted");
        if (!converted) { result = 1; goto done; }
        result = holy_import_rpm(download, catalog.source, converted);
        if (result) goto done;
    }
    printf("fetched %s %s %s %s\n", selected.name, selected.evr, selected.arch, selected.hash);
    result = 0;
done:
    free(url); free(download); free(converted);
    return result;
}

#endif